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.
Files changed (47) hide show
  1. package/CHANGELOG.md +17 -1
  2. package/assets/base.css +24 -0
  3. package/assets/{packs/lagoon → extensions/palettes}/lagoon/extension.toml +3 -0
  4. package/assets/{packs/aurora → extensions/palettes}/neon/extension.toml +3 -0
  5. package/assets/{packs/academic → extensions/palettes}/nordic/extension.toml +3 -0
  6. package/assets/{packs/minimal → extensions/palettes}/paper/extension.toml +3 -0
  7. package/assets/{packs/neue → extensions/palettes}/swiss/extension.toml +3 -0
  8. package/assets/{packs/academic → extensions/themes}/academic/extension.toml +3 -0
  9. package/assets/{packs/aurora → extensions/themes}/aurora/extension.toml +3 -0
  10. package/assets/{packs/minimal → extensions/themes}/minimal/extension.toml +3 -0
  11. package/assets/{packs/neue → extensions/themes}/neue/extension.toml +3 -0
  12. package/docs/reference/authoring.md +16 -3
  13. package/docs/reference/extensions.md +42 -35
  14. package/docs/reference/palettes.md +1 -1
  15. package/docs/reference/themes.md +3 -3
  16. package/docs/site/content/appearance.md +11 -9
  17. package/docs/site/content/commands.md +2 -1
  18. package/docs/site/content/theme-authoring.md +1 -1
  19. package/docs/site/content/theme-repository.md +36 -28
  20. package/package.json +2 -2
  21. package/skills/write-slides/SKILL.md +1 -1
  22. package/src/build/editorPlugin.js +22 -17
  23. package/src/build/themeCheck.js +14 -3
  24. package/src/cli/args.js +2 -1
  25. package/src/cli/index.js +7 -6
  26. package/src/cli/themes.js +99 -92
  27. package/src/editor/api.js +11 -10
  28. package/src/editor/components/DesignApp.jsx +9 -11
  29. package/src/editor/components/ExtensionEditor.jsx +10 -11
  30. package/src/editor/components/ExtensionsPanel.jsx +24 -28
  31. package/src/extensions/discover.js +28 -35
  32. package/src/extensions/installed.js +73 -48
  33. package/src/extensions/manifest.js +20 -3
  34. package/src/extensions/moved.js +14 -7
  35. package/src/extensions/packages.js +318 -0
  36. package/src/paths.js +0 -2
  37. package/src/runtime/markedSetup.js +7 -0
  38. package/assets/packs/academic/pack.toml +0 -8
  39. package/assets/packs/aurora/pack.toml +0 -8
  40. package/assets/packs/lagoon/pack.toml +0 -8
  41. package/assets/packs/minimal/pack.toml +0 -8
  42. package/assets/packs/neue/pack.toml +0 -8
  43. package/src/extensions/packs.js +0 -357
  44. /package/assets/{packs/academic → extensions/themes}/academic/styles.css +0 -0
  45. /package/assets/{packs/aurora → extensions/themes}/aurora/styles.css +0 -0
  46. /package/assets/{packs/minimal → extensions/themes}/minimal/styles.css +0 -0
  47. /package/assets/{packs/neue → extensions/themes}/neue/styles.css +0 -0
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSy
5
5
  import { resolve, dirname, relative } from 'node:path'
6
6
  import { homedir } from 'node:os'
7
7
  import { MANIFEST_FILENAME, ManifestError, KINDS, parseManifestText, validateManifest } from './manifest.js'
8
- import { builtinExtensionsRoot, bundledPacksRoot } from '../paths.js'
8
+ import { builtinExtensionsRoot } from '../paths.js'
9
9
 
10
10
  const SKIP = name => name.startsWith('.') || name === 'node_modules'
11
11
 
@@ -20,36 +20,31 @@ export function userExtensionsDir() {
20
20
  return resolve(mdeckHome(), 'extensions')
21
21
  }
22
22
 
23
- // The bundled packs this user removed (`mdeck themes remove <pack> --global`):
24
- // they stay in mdeck's folder but are not offered.
23
+ // The built-in themes and palettes this user removed (`mdeck themes remove
24
+ // aurora`): they stay in mdeck's folder but are not offered. Kept as
25
+ // "theme:aurora" and "palette:neon".
25
26
  const removedFile = () => resolve(mdeckHome(), 'removed.json')
26
- export function removedPacks() {
27
- try { const packs = JSON.parse(readFileSync(removedFile(), 'utf8')).packs; return Array.isArray(packs) ? packs : [] } catch { return [] }
27
+ export function removedBuiltIns() {
28
+ let saved = {}
29
+ try { saved = JSON.parse(readFileSync(removedFile(), 'utf8')) } catch {}
30
+ return (Array.isArray(saved.hidden) ? saved.hidden : []).filter(key => /^(theme|palette):[a-z][a-z0-9-]*$/.test(key))
28
31
  }
29
- export function setRemovedPacks(packs) {
32
+ export function setRemovedBuiltIns(keys) {
30
33
  mkdirSync(mdeckHome(), { recursive: true })
31
- writeFileSync(removedFile(), JSON.stringify({ packs: [...new Set(packs)].sort() }, null, 2) + '\n')
34
+ writeFileSync(removedFile(), JSON.stringify({ hidden: [...new Set(keys)].sort() }, null, 2) + '\n')
32
35
  }
33
36
 
34
- // The packs that come with mdeck: { id: folder }, all or only those offered
35
- // (not `hidden`, by default those this user removed).
36
- export function bundledPacks({ root = bundledPacksRoot, all = false, hidden = all ? [] : removedPacks() } = {}) {
37
- if (!existsSync(root)) return {}
38
- const removed = hidden
39
- return Object.fromEntries(sortedEntries(root).map(entry => entry.name).filter(id => !removed.includes(id)).map(id => [id, resolve(root, id)]))
40
- }
41
-
42
- // Built-in layouts and the bundled packs, then the user's, then the deck's
37
+ // mdeck's own layouts, themes and palettes, then the user's, then the deck's
43
38
  // own. For themes and palettes a later place replaces an earlier one with the
44
- // same id: an installed newer version replaces the bundled one, and a deck's
45
- // copy replaces both.
46
- // `userRoot: null` leaves out what this user installed and removed, for
47
- // results that must not depend on the computer (checks, the repository).
48
- export function extensionRoots(slidesPath, { builtinRoot = builtinExtensionsRoot, packsRoot = bundledPacksRoot, userRoot = userExtensionsDir() } = {}) {
39
+ // same id: an installed newer version replaces the built-in one, and a
40
+ // deck's copy replaces both. Built-in themes and palettes this user removed
41
+ // are left out. `userRoot: null` leaves out what this user installed and
42
+ // removed, for results that must not depend on the computer (checks, the
43
+ // repository).
44
+ export function extensionRoots(slidesPath, { builtinRoot = builtinExtensionsRoot, userRoot = userExtensionsDir() } = {}) {
49
45
  const deckDir = dirname(resolve(slidesPath))
50
46
  return [
51
- { dir: builtinRoot, source: 'built-in' },
52
- ...Object.entries(bundledPacks({ root: packsRoot, hidden: userRoot ? removedPacks() : [] })).map(([pack, dir]) => ({ dir, source: 'built-in', pack })),
47
+ { dir: builtinRoot, source: 'built-in', hidden: userRoot ? removedBuiltIns() : [] },
53
48
  ...(userRoot ? [{ dir: userRoot, source: 'user' }] : []),
54
49
  { dir: resolve(deckDir, 'extensions'), source: 'local' },
55
50
  ]
@@ -80,21 +75,17 @@ function readLayout(record) {
80
75
  function loadManifest({ dir, file, folderName }) {
81
76
  const raw = parseManifestText(readFileSync(file, 'utf8'), file)
82
77
  const record = validateManifest(raw, { file, dir, folderName })
83
- // An installed pack leaves a note in each folder saying which pack it is.
84
- let pack = null
85
- try { pack = JSON.parse(readFileSync(resolve(dir, '.mdeck-pack.json'), 'utf8')).pack ?? null } catch {}
86
- const withPack = pack ? { ...record, pack } : record
87
- return record.kind === 'layout' ? readLayout(withPack) : withPack
78
+ return record.kind === 'layout' ? readLayout(record) : record
88
79
  }
89
80
 
90
81
  export function discoverExtensions(roots) {
91
82
  const registry = { layouts: {}, themes: {}, palettes: {}, records: [], warnings: [] }
92
83
  const byKey = new Map()
93
84
  const RANK = { 'built-in': 0, user: 1, local: 2 }
94
- const add = (record, source, pack) => {
85
+ const add = (record, source) => {
95
86
  const key = `${record.kind}:${record.id}`
96
87
  const existing = byKey.get(key)
97
- const entry = { ...record, source, ...(pack ? { pack } : {}) }
88
+ const entry = { ...record, source }
98
89
  // Themes and palettes may be replaced from a later place; layouts and two
99
90
  // with one id in the same place may not.
100
91
  const replaces = existing && record.kind !== 'layout' && RANK[source] > RANK[existing.source]
@@ -108,7 +99,10 @@ export function discoverExtensions(roots) {
108
99
  }
109
100
  for (const root of roots) {
110
101
  if (!existsSync(root.dir) || !statSync(root.dir).isDirectory()) continue
111
- for (const found of walk(root.dir)) { const record = loadManifest(found); add(record, root.source, root.pack ?? record.pack) }
102
+ for (const found of walk(root.dir)) {
103
+ const record = loadManifest(found)
104
+ if (!root.hidden?.includes(`${record.kind}:${record.id}`)) add(record, root.source)
105
+ }
112
106
  }
113
107
  // Themes and palettes in name order, wherever they come from.
114
108
  for (const kind of ['themes', 'palettes']) registry[kind] = Object.fromEntries(Object.entries(registry[kind]).sort(([a], [b]) => a.localeCompare(b, 'en')))
@@ -135,10 +129,9 @@ function checkPaletteReferences(registry) {
135
129
  }
136
130
  }
137
131
 
138
- // The extensions that come with mdeck, alone: layouts and every bundled pack,
139
- // removed or not.
132
+ // The extensions that come with mdeck, alone, removed or not.
140
133
  export function builtInRegistry() {
141
- return discoverExtensions([{ dir: builtinExtensionsRoot, source: 'built-in' }, ...Object.entries(bundledPacks({ all: true })).map(([pack, dir]) => ({ dir, source: 'built-in', pack }))])
134
+ return discoverExtensions([{ dir: builtinExtensionsRoot, source: 'built-in' }])
142
135
  }
143
136
 
144
137
  export function loadRegistry(slidesPath, options) {
@@ -158,7 +151,7 @@ export function serializeRegistry(registry, { relativeTo } = {}) {
158
151
  const out = { schema: 2, warnings: registry.warnings }
159
152
  for (const kind of KINDS) {
160
153
  out[`${kind}s`] = Object.values(registry[`${kind}s`]).map(record => ({
161
- ...record.manifest, kind, source: record.source, ...(record.pack ? { pack: record.pack } : {}),
154
+ ...record.manifest, kind, source: record.source, ...(record.package?.version ? { version: record.package.version } : {}),
162
155
  file: relativeTo ? relative(relativeTo, record.file) : record.file,
163
156
  }))
164
157
  }
@@ -1,63 +1,88 @@
1
- // Where packs are installed: for every deck (the user's folder, where the
2
- // bundled packs are offered too) or beside one deck. Shared by mdeck themes
3
- // and the design page.
4
- import { existsSync, statSync } from 'node:fs'
1
+ // Where themes and palettes are installed: for every deck (the user's
2
+ // folder, where the built-in ones are offered too) or beside one deck.
3
+ // Shared by mdeck themes / mdeck palettes and the design page.
4
+ import { existsSync, rmSync, statSync } from 'node:fs'
5
5
  import { dirname, resolve } from 'node:path'
6
- import { bundledPacks, loadRegistry, mdeckHome, removedPacks, setRemovedPacks, userExtensionsDir } from './discover.js'
7
- import { installedPacks, installWithRequirements, PackError, removePack } from './packs.js'
6
+ import { loadRegistry, mdeckHome, removedBuiltIns, setRemovedBuiltIns, userExtensionsDir } from './discover.js'
7
+ import { installedPackages, installPackage, keyOf, obtainPackage, PackageError, removePackage, satisfies, withDependencies } from './packages.js'
8
+
9
+ const fail = message => { throw new PackageError(message) }
8
10
 
9
11
  // Where a command works: for every deck (the user's folder), or beside a
10
12
  // deck when one is named or --local is given.
11
13
  export function target(where, local) {
12
- if (!where && !local) return { user: true, root: userExtensionsDir(), deck: resolve('slides.md'), label: 'for every deck' }
14
+ if (!where && !local) return { user: true, root: userExtensionsDir(), deck: resolve(mdeckHome(), 'no-deck', 'slides.md'), label: 'for every deck' }
13
15
  const given = resolve(where ?? '.')
14
- if (!existsSync(given) && !/\.md$/i.test(given)) throw new PackError(`${where} does not exist; name a slides file or the folder it is in`)
16
+ if (!existsSync(given) && !/\.md$/i.test(given)) fail(`${where} does not exist; name a slides file or the folder it is in`)
15
17
  const folder = existsSync(given) && statSync(given).isDirectory() ? given : dirname(given)
16
18
  return { user: false, root: resolve(folder, 'extensions'), deck: existsSync(given) && !statSync(given).isDirectory() ? given : resolve(folder, 'slides.md'), label: `in ${resolve(folder, 'extensions')}` }
17
19
  }
18
20
 
19
- // The packs that `where` has: installed there, and for the user's folder
20
- // also the bundled packs that are offered.
21
- export function packsAt(where) {
22
- const installed = installedPacks(where.root)
23
- if (!where.user) return installed
24
- const offered = Object.fromEntries(Object.keys(bundledPacks()).map(id => [id, { bundled: true, requires: [] }]))
25
- return { ...offered, ...installed }
26
- }
27
-
28
- // The packs at `where` that require `id`.
29
- export function requiredBy(id, where, catalogue) {
30
- return Object.entries(packsAt(where)).filter(([other, pack]) => {
31
- if (other === id) return false
32
- const requires = pack.bundled ? catalogue.packs.find(p => p.id === other)?.requires ?? [] : pack.requires
33
- return requires.includes(id)
34
- }).map(([other]) => other)
35
- }
21
+ // The themes and palettes `where` sees, as a registry: for every deck what
22
+ // this user has, beside a deck also what that deck has.
23
+ const registryAt = where => loadRegistry(where.deck)
36
24
 
37
- // Installs a pack with what it requires. For every deck, a bundled pack is
38
- // offered again instead of copied.
39
- export async function installHere(catalogue, id, where, { mdeckVersion, force = false }) {
40
- const removed = removedPacks()
41
- const has = pack => Object.hasOwn(packsAt(where), pack)
42
- const bundledHere = where.user ? entry => { setRemovedPacks(removedPacks().filter(other => other !== entry.id)); return true } : null
43
- const done = await installWithRequirements(catalogue, id, { root: where.root, mdeckVersion, force, has, bundledHere })
44
- return done.map(step => ({ ...step, wasRemoved: step.bundled && removed.includes(step.id) }))
25
+ // Installs a theme or palette with what it needs. What `where` has already
26
+ // stays as it is. A built-in one is not copied: for every deck it is offered
27
+ // again if it was removed. A theme and its own palette may need each other,
28
+ // so the registry is checked before anything is written and once all is
29
+ // there; if anything fails, what this call installed is taken out again.
30
+ export async function installHere(catalogue, kind, id, where, { mdeckVersion, force = false }) {
31
+ const before = registryAt(where)
32
+ const plan = withDependencies(catalogue, kind, id)
33
+ const done = [], written = []
34
+ try {
35
+ for (const entry of plan) {
36
+ const key = keyOf(entry.kind, entry.id)
37
+ const requested = entry.kind === kind && entry.id === id
38
+ if (entry.builtIn) {
39
+ const hidden = removedBuiltIns()
40
+ const wasRemoved = hidden.includes(key)
41
+ if (wasRemoved) setRemovedBuiltIns(hidden.filter(other => other !== key))
42
+ done.push({ kind: entry.kind, id: entry.id, version: entry.version, builtIn: true, wasRemoved, requested })
43
+ continue
44
+ }
45
+ const have = installedPackages(where.root)[key]
46
+ // Beside a deck, what is installed only for every deck is copied too,
47
+ // so the slide folder carries it.
48
+ const there = before[`${entry.kind}s`][entry.id]
49
+ if (!requested && (have || (there && (where.user || there.source !== 'user')))) { done.push({ kind: entry.kind, id: entry.id, version: have?.version ?? null, kept: true, requested }); continue }
50
+ if (!satisfies(entry.mdeck, mdeckVersion)) fail(`The ${entry.kind} ${entry.id} ${entry.version} needs mdeck ${entry.mdeck}; this is ${mdeckVersion}. Update mdeck first: npm install -g mdeck`)
51
+ const { bundle, url, digest } = await obtainPackage(catalogue, entry)
52
+ const result = installPackage(bundle, { root: where.root, url, digest, force })
53
+ if (!result.replaced) written.push(result.dir)
54
+ done.push({ kind: entry.kind, id: entry.id, version: entry.version, replaced: result.replaced, requested })
55
+ }
56
+ registryAt(where)
57
+ } catch (error) {
58
+ for (const dir of written) rmSync(dir, { recursive: true, force: true })
59
+ throw error
60
+ }
61
+ return done
45
62
  }
46
63
 
47
- // Removes a pack. For every deck, a bundled pack is hidden (and a newer
48
- // version installed over it removed too); install brings it back.
49
- export function removeHere(catalogue, id, where, { force = false } = {}) {
50
- const needed = requiredBy(id, where, catalogue)
51
- if (needed.length && !force) throw new PackError(`${needed.join(', ')} ${needed.length > 1 ? 'require' : 'requires'} ${id}; remove ${needed.length > 1 ? 'them' : 'it'} first, or add --force`)
52
- const installed = installedPacks(where.root)[id]
53
- const isBundled = Object.hasOwn(bundledPacks({ all: true }), id)
54
- if (!installed && !(where.user && isBundled)) throw new PackError(`The pack ${id} is not installed ${where.label}`)
55
- // Every deck needs a theme: the last one stays, even with --force.
56
- if (where.user) {
57
- const themes = Object.values(loadRegistry(resolve(mdeckHome(), 'no-deck', 'slides.md')).themes)
58
- if (themes.length && themes.every(theme => theme.pack === id)) throw new PackError(`${id} has the last theme installed for every deck (${themes.map(theme => theme.id).join(', ')}); install another theme first, for example mdeck themes install neue`)
64
+ // Removes a theme or palette. A built-in one is hidden instead, for every
65
+ // deck; installing it brings it back. A palette a theme here uses by default
66
+ // stays; a theme takes the palettes that belong only to it along; the last
67
+ // theme stays.
68
+ export function removeHere(kind, id, where, { force = false } = {}) {
69
+ const registry = registryAt(where)
70
+ const record = registry[`${kind}s`][id]
71
+ const installed = installedPackages(where.root)
72
+ const key = keyOf(kind, id)
73
+ if (!installed[key] && !(where.user && record?.source === 'built-in')) fail(`The ${kind} ${id} is not installed ${where.label}`)
74
+ if (kind === 'palette') {
75
+ const users = Object.values(registry.themes).filter(theme => theme.manifest.palette === id).map(theme => theme.id)
76
+ if (users.length) fail(`${users.join(', ')} ${users.length > 1 ? 'use' : 'uses'} the palette ${id} by default; remove ${users.length > 1 ? 'those themes' : 'that theme'} first`)
77
+ }
78
+ if (kind === 'theme' && Object.keys(registry.themes).length === 1) fail(`${id} is the last theme ${where.label}; install another theme first (mdeck themes search lists them)`)
79
+ const removed = []
80
+ const take = (k, i) => {
81
+ const r = registry[`${k}s`][i]
82
+ if (installedPackages(where.root)[keyOf(k, i)]) { removePackage(k, i, { root: where.root, force }); removed.push({ kind: k, id: i }) }
83
+ else if (where.user && r?.source === 'built-in') { setRemovedBuiltIns([...removedBuiltIns(), keyOf(k, i)]); removed.push({ kind: k, id: i, hidden: true }) }
59
84
  }
60
- const pack = installed ? removePack(id, { root: where.root, force }) : null
61
- if (where.user && isBundled) setRemovedPacks([...removedPacks(), id])
62
- return { folders: pack?.folders ?? [], hidden: where.user && isBundled }
85
+ take(kind, id)
86
+ if (kind === 'theme') for (const palette of Object.values(registry.palettes)) if (palette.manifest.theme === id) take('palette', palette.id)
87
+ return removed
63
88
  }
@@ -15,11 +15,16 @@ export const MANIFEST_FILENAME = 'extension.toml'
15
15
  const FRAMES = ['standard', 'title', 'chapter', 'none']
16
16
  const PROPERTY_TYPES = ['string', 'number', 'integer', 'boolean', 'array', 'object']
17
17
  const SHARED_KEYS = ['schema', 'kind', 'id', 'title', 'description']
18
+ // A theme or palette is also a package that can be installed: its version,
19
+ // who made it, its licence, a web page and the oldest mdeck it works with.
20
+ const PACKAGE_KEYS = ['version', 'author', 'license', 'homepage', 'mdeck']
18
21
  const KEYS = {
19
22
  layout: [...SHARED_KEYS, 'frame', 'files', 'regions', 'properties'],
20
- theme: [...SHARED_KEYS, 'palette', 'palettes', 'appearance', 'fonts', 'files', 'tokens', 'params'],
21
- palette: [...SHARED_KEYS, 'theme', 'light', 'dark'],
23
+ theme: [...SHARED_KEYS, ...PACKAGE_KEYS, 'palette', 'palettes', 'appearance', 'fonts', 'files', 'tokens', 'params'],
24
+ palette: [...SHARED_KEYS, ...PACKAGE_KEYS, 'theme', 'light', 'dark'],
22
25
  }
26
+ export const VERSION_RE = /^\d+\.\d+\.\d+$/
27
+ export const RANGE_RE = /^>=\s*(\d+\.\d+\.\d+)$/
23
28
  const SCHEMA_KEYS = ['type', 'title', 'description', 'enum', 'required', 'default', 'minimum', 'maximum', 'minItems', 'maxItems', 'items']
24
29
  const RESERVED_NAMES = ['constructor', 'prototype', '__proto__']
25
30
 
@@ -72,7 +77,19 @@ export function validateManifest(raw, { file, dir, folderName, fileExists = exis
72
77
  const manifest = kind === 'layout' ? validateLayout(raw, shared, fail)
73
78
  : kind === 'theme' ? validateTheme(raw, shared, fail)
74
79
  : validatePalette(raw, shared, fail)
75
- return { kind, id: raw.id, title: shared.title, description: shared.description, dir, file, manifest, files }
80
+ const pkg = kind === 'layout' ? null : validatePackage(raw, fail)
81
+ return { kind, id: raw.id, title: shared.title, description: shared.description, dir, file, manifest, files, ...(pkg ? { package: pkg } : {}) }
82
+ }
83
+
84
+ // The package settings, all optional for a theme or palette of one's own;
85
+ // the theme repository requires version, author and license.
86
+ function validatePackage(raw, fail) {
87
+ const pkg = {}
88
+ if (raw.version != null) { if (typeof raw.version !== 'string' || !VERSION_RE.test(raw.version)) fail('version must look like 1.0.0', 'version'); pkg.version = raw.version }
89
+ for (const key of ['author', 'license']) if (raw[key] != null) { if (typeof raw[key] !== 'string' || !raw[key].trim()) fail(`${key} must be text`, key); pkg[key] = raw[key].trim() }
90
+ if (raw.homepage != null) { if (typeof raw.homepage !== 'string' || !/^https:\/\//.test(raw.homepage)) fail('homepage must be an https:// address', 'homepage'); pkg.homepage = raw.homepage }
91
+ if (raw.mdeck != null) { if (typeof raw.mdeck !== 'string' || !RANGE_RE.test(raw.mdeck)) fail('mdeck must look like ">=3.1.0"', 'mdeck'); pkg.mdeck = raw.mdeck }
92
+ return pkg
76
93
  }
77
94
 
78
95
  const FILE_KEYS = { layout: ['layout', 'styles', 'starter'], theme: ['styles'], palette: [] }
@@ -1,13 +1,20 @@
1
1
  // Themes and palettes that came with mdeck before 3.0 and now live in the
2
- // theme repository, by the pack that has them. A deck that names one is told
3
- // how to install it instead of being told the name is unknown.
2
+ // theme repository. A deck that names one is told how to install it instead
3
+ // of being told the name is unknown.
4
4
  export const MOVED = {
5
- themes: { duet: 'duet', editorial: 'editorial', fhnw: 'fhnw', terminal: 'terminal', sketch: 'sketch' },
6
- palettes: { cobalt: 'duet', terra: 'editorial', brand: 'fhnw', phosphor: 'terminal', pastel: 'sketch', ember: 'earth', forest: 'earth', graphite: 'graphite' },
5
+ themes: ['duet', 'editorial', 'fhnw', 'terminal', 'sketch'],
6
+ palettes: ['cobalt', 'terra', 'brand', 'phosphor', 'pastel', 'forest'],
7
7
  }
8
8
 
9
- // "The theme "duet" is no longer built in: mdeck themes install duet", or null.
9
+ // Palettes that were removed because another one looks almost the same, by
10
+ // the one to use instead.
11
+ export const REPLACED = { palettes: { graphite: 'nordic', ember: 'paper' } }
12
+
13
+ // "The theme "duet" is no longer built into mdeck; install it with: mdeck
14
+ // themes install duet", what to use instead of a removed one, or null.
15
+ // Installing goes to every deck by default.
10
16
  export function movedHint(kind, id) {
11
- const pack = MOVED[`${kind}s`]?.[id]
12
- return pack ? `The ${kind} "${id}" is no longer built into mdeck; install it beside the deck with: mdeck themes install ${pack}` : null
17
+ const instead = REPLACED[`${kind}s`]?.[id]
18
+ if (instead) return `The ${kind} "${id}" was removed from mdeck; use ${kind}: ${instead}, which looks almost the same`
19
+ return MOVED[`${kind}s`]?.includes(id) ? `The ${kind} "${id}" is no longer built into mdeck; install it with: mdeck ${kind}s install ${id}` : null
13
20
  }