mdeck 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -1
- package/README.md +9 -9
- package/assets/base.css +4 -1
- package/assets/packs/academic/pack.toml +8 -0
- package/assets/packs/aurora/pack.toml +8 -0
- package/assets/packs/lagoon/pack.toml +8 -0
- package/assets/packs/minimal/minimal/extension.toml +45 -0
- package/assets/packs/minimal/minimal/styles.css +342 -0
- package/assets/packs/minimal/pack.toml +8 -0
- package/assets/packs/minimal/paper/extension.toml +27 -0
- package/assets/packs/neue/pack.toml +8 -0
- package/docs/reference/authoring.md +13 -18
- package/docs/reference/claude-skill.md +1 -1
- package/docs/reference/extensions.md +29 -20
- package/docs/reference/palettes.md +8 -12
- package/docs/reference/themes.md +8 -7
- package/docs/site/content/appearance.md +33 -49
- package/docs/site/content/commands.md +2 -2
- package/docs/site/content/theme-authoring.md +1 -1
- package/docs/site/content/theme-repository.md +19 -15
- package/docs/site/examples/first-talk.md +1 -1
- package/docs/site/images/themes/academic-dark.webp +0 -0
- package/docs/site/images/themes/academic-light.webp +0 -0
- package/docs/site/images/themes/academic.webp +0 -0
- package/docs/site/images/themes/aurora-dark.webp +0 -0
- package/docs/site/images/themes/aurora-light.webp +0 -0
- package/docs/site/images/themes/aurora.webp +0 -0
- package/docs/site/images/themes/minimal-dark.webp +0 -0
- package/docs/site/images/themes/minimal-light.webp +0 -0
- package/docs/site/images/themes/minimal.webp +0 -0
- package/docs/site/images/themes/neue-dark.webp +0 -0
- package/docs/site/images/themes/neue-light.webp +0 -0
- package/docs/site/images/themes/neue.webp +0 -0
- package/docs/site/pages.js +1 -1
- package/examples/custom-layouts/slides.md +1 -1
- package/examples/poll/slides.md +1 -1
- package/examples/python/slides.md +2 -2
- package/examples/showcase/slides.md +3 -3
- package/package.json +1 -1
- package/skills/write-slides/SKILL.md +2 -2
- package/src/build/check.js +0 -7
- package/src/build/editorPlugin.js +19 -11
- package/src/build/themeCheck.js +15 -5
- package/src/cli/args.js +1 -1
- package/src/cli/index.js +3 -3
- package/src/cli/themes.js +93 -64
- package/src/core/validateDeck.js +4 -2
- package/src/editor/api.js +9 -1
- package/src/editor/components/App.jsx +4 -2
- package/src/editor/components/DesignApp.jsx +12 -3
- package/src/editor/components/ExtensionEditor.jsx +15 -4
- package/src/editor/components/ExtensionsPanel.jsx +19 -21
- package/src/editor/designLink.js +18 -1
- package/src/editor/sampleDeck.js +3 -1
- package/src/extensions/appearance.js +2 -1
- package/src/extensions/discover.js +56 -16
- package/src/extensions/installed.js +63 -0
- package/src/extensions/moved.js +13 -0
- package/src/extensions/packs.js +135 -46
- package/src/home/Home.jsx +12 -6
- package/src/layouts/layoutApi.jsx +10 -2
- package/src/paths.js +2 -0
- package/src/runtime/themeLoader.js +5 -2
- package/assets/extensions/palettes/brand/extension.toml +0 -29
- package/assets/extensions/palettes/cobalt/extension.toml +0 -27
- package/assets/extensions/palettes/ember/extension.toml +0 -27
- package/assets/extensions/palettes/forest/extension.toml +0 -27
- package/assets/extensions/palettes/graphite/extension.toml +0 -27
- package/assets/extensions/palettes/pastel/extension.toml +0 -30
- package/assets/extensions/palettes/phosphor/extension.toml +0 -27
- package/assets/extensions/palettes/terra/extension.toml +0 -27
- package/assets/extensions/themes/duet/extension.toml +0 -46
- package/assets/extensions/themes/duet/styles.css +0 -573
- package/assets/extensions/themes/editorial/extension.toml +0 -46
- package/assets/extensions/themes/editorial/styles.css +0 -574
- package/assets/extensions/themes/fhnw/extension.toml +0 -61
- package/assets/extensions/themes/fhnw/styles.css +0 -604
- package/assets/extensions/themes/sketch/extension.toml +0 -48
- package/assets/extensions/themes/sketch/styles.css +0 -633
- package/assets/extensions/themes/terminal/extension.toml +0 -38
- package/assets/extensions/themes/terminal/styles.css +0 -550
- package/docs/site/images/themes/duet-dark.webp +0 -0
- package/docs/site/images/themes/duet-light.webp +0 -0
- package/docs/site/images/themes/duet.webp +0 -0
- package/docs/site/images/themes/editorial-dark.webp +0 -0
- package/docs/site/images/themes/editorial-light.webp +0 -0
- package/docs/site/images/themes/editorial.webp +0 -0
- package/docs/site/images/themes/fhnw.webp +0 -0
- package/docs/site/images/themes/sketch-dark.webp +0 -0
- package/docs/site/images/themes/sketch-light.webp +0 -0
- package/docs/site/images/themes/sketch.webp +0 -0
- package/docs/site/images/themes/terminal-dark.webp +0 -0
- package/docs/site/images/themes/terminal-light.webp +0 -0
- package/docs/site/images/themes/terminal.webp +0 -0
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +0 -19
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +0 -6
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +0 -10
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +0 -13
- /package/assets/{extensions/themes → packs/academic}/academic/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/academic}/academic/styles.css +0 -0
- /package/assets/{extensions/palettes → packs/academic}/nordic/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/aurora}/aurora/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/aurora}/aurora/styles.css +0 -0
- /package/assets/{extensions/palettes → packs/aurora}/neon/extension.toml +0 -0
- /package/assets/{extensions/palettes → packs/lagoon}/lagoon/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/neue}/neue/extension.toml +0 -0
- /package/assets/{extensions/themes → packs/neue}/neue/styles.css +0 -0
- /package/assets/{extensions/palettes → packs/neue}/swiss/extension.toml +0 -0
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// one is available to inverted slides as --inverse-bg, --inverse-ink, …
|
|
7
7
|
// Theme tokens (type, spacing, fonts) and theme parameters come on top.
|
|
8
8
|
import { tokensToCss, COLOR_ROLES, APPEARANCES, palettesFor } from './tokens.js'
|
|
9
|
+
import { movedHint } from './moved.js'
|
|
9
10
|
|
|
10
11
|
// Code highlighting and logos on dark slides, unless the palette says otherwise.
|
|
11
12
|
export const DARK_TOKENS = {
|
|
@@ -34,7 +35,7 @@ export function resolvePalette({ theme, palettes = {}, palette: requested, appea
|
|
|
34
35
|
const warnings = []
|
|
35
36
|
const offered = palettesFor(theme, palettes)
|
|
36
37
|
let palette = requested ? palettes[requested] : null
|
|
37
|
-
if (requested && !palette) warnings.push(`Unknown palette "${requested}". Available: ${offered.map(p => p.id).join(', ')}`)
|
|
38
|
+
if (requested && !palette) warnings.push(movedHint('palette', requested) ?? `Unknown palette "${requested}". Available: ${offered.map(p => p.id).join(', ')}`)
|
|
38
39
|
else if (palette && !offered.includes(palette)) { warnings.push(`The theme "${theme.id}" does not offer the palette "${requested}". Available: ${offered.map(p => p.id).join(', ')}`); palette = null }
|
|
39
40
|
palette ??= palettes[theme.palette] ?? offered[0] ?? null
|
|
40
41
|
let appearance = requestedAppearance || theme.appearance || 'light'
|
|
@@ -1,26 +1,55 @@
|
|
|
1
1
|
// One discovery layer for layouts, themes and palettes. The same registry
|
|
2
2
|
// backs `mdeck check`, CLI listings, the dev server and builds.
|
|
3
3
|
import { palettesFor } from './tokens.js'
|
|
4
|
-
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
4
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
|
|
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 } from '../paths.js'
|
|
8
|
+
import { builtinExtensionsRoot, bundledPacksRoot } from '../paths.js'
|
|
9
9
|
|
|
10
10
|
const SKIP = name => name.startsWith('.') || name === 'node_modules'
|
|
11
11
|
|
|
12
|
+
// mdeck's own folder for this user: ~/.mdeck, or $MDECK_HOME.
|
|
13
|
+
export function mdeckHome() {
|
|
14
|
+
return process.env.MDECK_HOME || resolve(homedir(), '.mdeck')
|
|
15
|
+
}
|
|
16
|
+
|
|
12
17
|
// Extensions installed for one user (`mdeck themes install --global`), for
|
|
13
|
-
// every deck: ~/.mdeck/extensions
|
|
18
|
+
// every deck: ~/.mdeck/extensions.
|
|
14
19
|
export function userExtensionsDir() {
|
|
15
|
-
return resolve(
|
|
20
|
+
return resolve(mdeckHome(), 'extensions')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The bundled packs this user removed (`mdeck themes remove <pack> --global`):
|
|
24
|
+
// they stay in mdeck's folder but are not offered.
|
|
25
|
+
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 [] }
|
|
28
|
+
}
|
|
29
|
+
export function setRemovedPacks(packs) {
|
|
30
|
+
mkdirSync(mdeckHome(), { recursive: true })
|
|
31
|
+
writeFileSync(removedFile(), JSON.stringify({ packs: [...new Set(packs)].sort() }, null, 2) + '\n')
|
|
32
|
+
}
|
|
33
|
+
|
|
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)]))
|
|
16
40
|
}
|
|
17
41
|
|
|
18
|
-
// Built-in, then the user's, then the deck's
|
|
19
|
-
// a
|
|
20
|
-
|
|
42
|
+
// Built-in layouts and the bundled packs, then the user's, then the deck's
|
|
43
|
+
// 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() } = {}) {
|
|
21
49
|
const deckDir = dirname(resolve(slidesPath))
|
|
22
50
|
return [
|
|
23
51
|
{ dir: builtinRoot, source: 'built-in' },
|
|
52
|
+
...Object.entries(bundledPacks({ root: packsRoot, hidden: userRoot ? removedPacks() : [] })).map(([pack, dir]) => ({ dir, source: 'built-in', pack })),
|
|
24
53
|
...(userRoot ? [{ dir: userRoot, source: 'user' }] : []),
|
|
25
54
|
{ dir: resolve(deckDir, 'extensions'), source: 'local' },
|
|
26
55
|
]
|
|
@@ -51,18 +80,26 @@ function readLayout(record) {
|
|
|
51
80
|
function loadManifest({ dir, file, folderName }) {
|
|
52
81
|
const raw = parseManifestText(readFileSync(file, 'utf8'), file)
|
|
53
82
|
const record = validateManifest(raw, { file, dir, folderName })
|
|
54
|
-
|
|
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
|
|
55
88
|
}
|
|
56
89
|
|
|
57
90
|
export function discoverExtensions(roots) {
|
|
58
91
|
const registry = { layouts: {}, themes: {}, palettes: {}, records: [], warnings: [] }
|
|
59
92
|
const byKey = new Map()
|
|
60
|
-
const
|
|
93
|
+
const RANK = { 'built-in': 0, user: 1, local: 2 }
|
|
94
|
+
const add = (record, source, pack) => {
|
|
61
95
|
const key = `${record.kind}:${record.id}`
|
|
62
96
|
const existing = byKey.get(key)
|
|
63
|
-
const entry = { ...record, source }
|
|
64
|
-
|
|
65
|
-
|
|
97
|
+
const entry = { ...record, source, ...(pack ? { pack } : {}) }
|
|
98
|
+
// Themes and palettes may be replaced from a later place; layouts and two
|
|
99
|
+
// with one id in the same place may not.
|
|
100
|
+
const replaces = existing && record.kind !== 'layout' && RANK[source] > RANK[existing.source]
|
|
101
|
+
if (replaces) {
|
|
102
|
+
if (existing.source === 'user') registry.warnings.push(`The ${record.kind} "${record.id}" in this deck's extensions replaces the one installed in ${dirname(existing.dir)}`)
|
|
66
103
|
registry.records.splice(registry.records.indexOf(existing), 1)
|
|
67
104
|
} else if (existing) throw new ManifestError(`Duplicate ${record.kind} "${record.id}" is also defined in ${existing.file}. Extension IDs must be unique within their kind.`, { file: record.file })
|
|
68
105
|
byKey.set(key, entry)
|
|
@@ -71,8 +108,10 @@ export function discoverExtensions(roots) {
|
|
|
71
108
|
}
|
|
72
109
|
for (const root of roots) {
|
|
73
110
|
if (!existsSync(root.dir) || !statSync(root.dir).isDirectory()) continue
|
|
74
|
-
for (const found of walk(root.dir))
|
|
111
|
+
for (const found of walk(root.dir)) { const record = loadManifest(found); add(record, root.source, root.pack ?? record.pack) }
|
|
75
112
|
}
|
|
113
|
+
// Themes and palettes in name order, wherever they come from.
|
|
114
|
+
for (const kind of ['themes', 'palettes']) registry[kind] = Object.fromEntries(Object.entries(registry[kind]).sort(([a], [b]) => a.localeCompare(b, 'en')))
|
|
76
115
|
checkPaletteReferences(registry)
|
|
77
116
|
return registry
|
|
78
117
|
}
|
|
@@ -96,9 +135,10 @@ function checkPaletteReferences(registry) {
|
|
|
96
135
|
}
|
|
97
136
|
}
|
|
98
137
|
|
|
99
|
-
// The extensions that come with mdeck, alone
|
|
138
|
+
// The extensions that come with mdeck, alone: layouts and every bundled pack,
|
|
139
|
+
// removed or not.
|
|
100
140
|
export function builtInRegistry() {
|
|
101
|
-
return discoverExtensions([{ dir: builtinExtensionsRoot, source: 'built-in' }])
|
|
141
|
+
return discoverExtensions([{ dir: builtinExtensionsRoot, source: 'built-in' }, ...Object.entries(bundledPacks({ all: true })).map(([pack, dir]) => ({ dir, source: 'built-in', pack }))])
|
|
102
142
|
}
|
|
103
143
|
|
|
104
144
|
export function loadRegistry(slidesPath, options) {
|
|
@@ -118,7 +158,7 @@ export function serializeRegistry(registry, { relativeTo } = {}) {
|
|
|
118
158
|
const out = { schema: 2, warnings: registry.warnings }
|
|
119
159
|
for (const kind of KINDS) {
|
|
120
160
|
out[`${kind}s`] = Object.values(registry[`${kind}s`]).map(record => ({
|
|
121
|
-
...record.manifest, kind, source: record.source,
|
|
161
|
+
...record.manifest, kind, source: record.source, ...(record.pack ? { pack: record.pack } : {}),
|
|
122
162
|
file: relativeTo ? relative(relativeTo, record.file) : record.file,
|
|
123
163
|
}))
|
|
124
164
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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'
|
|
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'
|
|
8
|
+
|
|
9
|
+
// Where a command works: for every deck (the user's folder), or beside a
|
|
10
|
+
// deck when one is named or --local is given.
|
|
11
|
+
export function target(where, local) {
|
|
12
|
+
if (!where && !local) return { user: true, root: userExtensionsDir(), deck: resolve('slides.md'), label: 'for every deck' }
|
|
13
|
+
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`)
|
|
15
|
+
const folder = existsSync(given) && statSync(given).isDirectory() ? given : dirname(given)
|
|
16
|
+
return { user: false, root: resolve(folder, 'extensions'), deck: existsSync(given) && !statSync(given).isDirectory() ? given : resolve(folder, 'slides.md'), label: `in ${resolve(folder, 'extensions')}` }
|
|
17
|
+
}
|
|
18
|
+
|
|
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
|
+
}
|
|
36
|
+
|
|
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) }))
|
|
45
|
+
}
|
|
46
|
+
|
|
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`)
|
|
59
|
+
}
|
|
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 }
|
|
63
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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.
|
|
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' },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// "The theme "duet" is no longer built in: mdeck themes install duet", or null.
|
|
10
|
+
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
|
|
13
|
+
}
|
package/src/extensions/packs.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
// Packs:
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// Packs: every theme and palette comes in one. A pack is a folder with
|
|
2
|
+
// pack.toml and one folder per theme or palette; it travels as one JSON file
|
|
3
|
+
// of text files, so installing it is checking it and writing those files.
|
|
4
|
+
// Only extension.toml and styles.css are allowed: a pack never carries code.
|
|
5
|
+
// A pack may require others (`requires = ["lagoon"]`) for palettes its
|
|
6
|
+
// themes name. Packs come from two places: the starter set bundled with
|
|
7
|
+
// mdeck (assets/packs), and the theme repository
|
|
8
|
+
// (https://gh.tschieber.de/mdeck-themes/).
|
|
6
9
|
import { createHash } from 'node:crypto'
|
|
7
10
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
8
11
|
import { resolve, relative } from 'node:path'
|
|
@@ -47,6 +50,7 @@ function checkMeta(meta, where) {
|
|
|
47
50
|
if (typeof meta.id !== 'string' || !ID_RE.test(meta.id)) fail(`${where}: id must use lowercase letters, digits and hyphens, starting with a letter`)
|
|
48
51
|
if (typeof meta.version !== 'string' || !VERSION_RE.test(meta.version)) fail(`${where}: version must look like 1.0.0`)
|
|
49
52
|
if (meta.mdeck != null && (typeof meta.mdeck !== 'string' || !RANGE_RE.test(meta.mdeck))) fail(`${where}: mdeck must look like ">=2.3.0"`)
|
|
53
|
+
if (meta.requires != null && (!Array.isArray(meta.requires) || meta.requires.some(id => typeof id !== 'string' || !ID_RE.test(id) || id === meta.id))) fail(`${where}: requires must be a list of other packs' ids`)
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
// pack.toml and the files of each theme or palette in a pack folder.
|
|
@@ -75,7 +79,7 @@ export function readPackDir(dir) {
|
|
|
75
79
|
// The published form of a pack folder: one JSON text and its checksum.
|
|
76
80
|
export function bundlePack(dir) {
|
|
77
81
|
const { meta, files } = readPackDir(dir)
|
|
78
|
-
const bundle = { schema: 1, id: meta.id, version: meta.version, ...(meta.mdeck ? { mdeck: meta.mdeck } : {}), files }
|
|
82
|
+
const bundle = { schema: 1, id: meta.id, version: meta.version, ...(meta.mdeck ? { mdeck: meta.mdeck } : {}), ...(meta.requires?.length ? { requires: meta.requires } : {}), files }
|
|
79
83
|
const text = JSON.stringify(bundle, null, 1) + '\n'
|
|
80
84
|
return { meta, bundle, text, sha256: sha256(text) }
|
|
81
85
|
}
|
|
@@ -87,10 +91,11 @@ function checkCss(text, where) {
|
|
|
87
91
|
}
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
// Checks a pack before it is published or installed. `
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
+
// Checks a pack before it is published or installed. `provided` holds the
|
|
95
|
+
// themes and palettes of the packs it requires ({ themes, palettes } by id),
|
|
96
|
+
// for those its themes name; `owners` maps ids to the packs that have them,
|
|
97
|
+
// so no pack takes another's id. Returns the ids and manifests it holds.
|
|
98
|
+
export function checkPack(bundle, { provided = { themes: {}, palettes: {} }, owners = {} } = {}) {
|
|
94
99
|
if (!bundle || typeof bundle !== 'object' || typeof bundle.files !== 'object' || !bundle.files) fail('Not a pack')
|
|
95
100
|
checkMeta(bundle, `Pack ${bundle.id ?? '?'}`)
|
|
96
101
|
const byId = {}
|
|
@@ -119,18 +124,19 @@ export function checkPack(bundle, { known = { themes: {}, palettes: {} } } = {})
|
|
|
119
124
|
for (const url of record.manifest.fonts) if (!FONT_HOSTS.some(host => url.startsWith(host))) fail(`${bundle.id}: ${id} loads fonts from ${url}; allowed are ${FONT_HOSTS.join(', ')}`)
|
|
120
125
|
checkCss(files['styles.css'] ?? '', `${bundle.id}: ${id}/styles.css`)
|
|
121
126
|
}
|
|
122
|
-
if (
|
|
127
|
+
if (owners[id] && owners[id] !== bundle.id) fail(`${bundle.id}: ${id} is already the id of a theme or palette in the pack ${owners[id]}`)
|
|
123
128
|
records.push(record)
|
|
124
129
|
}
|
|
125
|
-
const
|
|
126
|
-
const
|
|
127
|
-
const
|
|
128
|
-
const hasTheme = id => themes.includes(id) || Boolean(known.themes[id])
|
|
130
|
+
const mine = kind => Object.fromEntries(records.filter(r => r.kind === kind).map(r => [r.id, r.manifest]))
|
|
131
|
+
const themes = mine('theme'), palettes = mine('palette')
|
|
132
|
+
const requires = bundle.requires?.length ? ` or in ${bundle.requires.join(', ')}` : ''
|
|
129
133
|
for (const record of records) {
|
|
130
|
-
if (record.kind === 'theme') for (const id of [record.manifest.palette, ...(record.manifest.palettes ?? [])])
|
|
131
|
-
|
|
134
|
+
if (record.kind === 'theme') for (const id of [record.manifest.palette, ...(record.manifest.palettes ?? [])]) {
|
|
135
|
+
if (!palettes[id] && !provided.palettes[id]) fail(`${bundle.id}: the theme ${record.id} names the palette "${id}", which is not in the pack${requires}; add the pack that has it to requires`)
|
|
136
|
+
}
|
|
137
|
+
if (record.kind === 'palette' && record.manifest.theme && !themes[record.manifest.theme] && !provided.themes[record.manifest.theme]) fail(`${bundle.id}: the palette ${record.id} belongs to the theme "${record.manifest.theme}", which is not in the pack${requires}`)
|
|
132
138
|
}
|
|
133
|
-
return { themes, palettes }
|
|
139
|
+
return { themes: Object.keys(themes), palettes: Object.keys(palettes), manifests: { themes, palettes } }
|
|
134
140
|
}
|
|
135
141
|
|
|
136
142
|
function readMarker(dir) {
|
|
@@ -152,7 +158,7 @@ export function installedPacks(root) {
|
|
|
152
158
|
for (const entry of readdirSync(root).sort()) {
|
|
153
159
|
const marker = readMarker(resolve(root, entry))
|
|
154
160
|
if (!marker?.pack) continue
|
|
155
|
-
const pack = packs[marker.pack] ??= { version: marker.version, url: marker.url, folders: [] }
|
|
161
|
+
const pack = packs[marker.pack] ??= { version: marker.version, url: marker.url, requires: marker.requires ?? [], folders: [] }
|
|
156
162
|
pack.folders.push(entry)
|
|
157
163
|
}
|
|
158
164
|
return packs
|
|
@@ -166,8 +172,8 @@ const refuseChanged = (root, folder, marker) => {
|
|
|
166
172
|
// Writes a checked pack into an extensions folder (a deck's, or the user's).
|
|
167
173
|
// A folder of another pack or of the deck itself is never touched; a folder
|
|
168
174
|
// of this pack is replaced unless it was changed by hand.
|
|
169
|
-
export function installPack(bundle, { root,
|
|
170
|
-
const { themes, palettes } = checkPack(bundle, {
|
|
175
|
+
export function installPack(bundle, { root, provided, owners, url = null, digest = null, force = false } = {}) {
|
|
176
|
+
const { themes, palettes } = checkPack(bundle, { provided, owners })
|
|
171
177
|
const ids = [...themes, ...palettes]
|
|
172
178
|
const before = installedPacks(root)[bundle.id]
|
|
173
179
|
for (const id of ids) {
|
|
@@ -193,7 +199,7 @@ export function installPack(bundle, { root, known, url = null, digest = null, fo
|
|
|
193
199
|
hashes[name] = sha256(content)
|
|
194
200
|
}
|
|
195
201
|
for (const name of readdirSync(dir)) if (name !== MARKER && !Object.hasOwn(hashes, name)) rmSync(resolve(dir, name), { recursive: true, force: true })
|
|
196
|
-
writeFileSync(resolve(dir, MARKER), JSON.stringify({ pack: bundle.id, version: bundle.version, url, sha256: digest, files: hashes }, null, 2) + '\n')
|
|
202
|
+
writeFileSync(resolve(dir, MARKER), JSON.stringify({ pack: bundle.id, version: bundle.version, ...(bundle.requires?.length ? { requires: bundle.requires } : {}), url, sha256: digest, files: hashes }, null, 2) + '\n')
|
|
197
203
|
}
|
|
198
204
|
return { themes, palettes, replaced: before?.version ?? null }
|
|
199
205
|
}
|
|
@@ -230,38 +236,121 @@ export async function fetchPack(index, entry) {
|
|
|
230
236
|
return { bundle, url, digest: entry.sha256 }
|
|
231
237
|
}
|
|
232
238
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
239
|
+
// ─── The catalogue: bundled packs and the repository ──────────────────────
|
|
240
|
+
|
|
241
|
+
// A bundled pack as the catalogue lists it.
|
|
242
|
+
function bundledEntry(id, dir) {
|
|
243
|
+
const { meta, bundle, sha256: digest } = bundlePack(dir)
|
|
244
|
+
const { themes, palettes } = checkPack(bundle)
|
|
245
|
+
return { id, title: meta.title, description: meta.description ?? '', version: meta.version, author: meta.author, license: meta.license,
|
|
246
|
+
...(meta.mdeck ? { mdeck: meta.mdeck } : {}), requires: meta.requires ?? [], themes, palettes, sha256: digest, bundled: true, dir }
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Every pack mdeck can install: the bundled ones, and the repository's when
|
|
250
|
+
// it answers. Of two versions of one pack the newer is offered; the bundled
|
|
251
|
+
// one on a tie, since it needs no download. `offline` says why the
|
|
252
|
+
// repository is missing.
|
|
253
|
+
export async function loadCatalogue({ bundled = {}, url = indexUrl(), online = true } = {}) {
|
|
254
|
+
const packs = new Map(Object.entries(bundled).map(([id, dir]) => [id, bundledEntry(id, dir)]))
|
|
255
|
+
let offline = null, index = { url, packs: [] }
|
|
256
|
+
if (online) { try { index = await fetchIndex(url) } catch (error) { offline = error.message } }
|
|
257
|
+
for (const entry of index.packs) {
|
|
258
|
+
const have = packs.get(entry.id)
|
|
259
|
+
if (!have || compareVersions(entry.version, have.version) > 0) packs.set(entry.id, { ...entry, requires: entry.requires ?? [], bundled: false, bundledVersion: have?.version })
|
|
260
|
+
else if (have.bundled) have.repository = entry
|
|
261
|
+
}
|
|
262
|
+
return { url, offline, packs: [...packs.values()].sort((a, b) => a.id.localeCompare(b.id, 'en')) }
|
|
238
263
|
}
|
|
239
264
|
|
|
240
|
-
export function findPack(
|
|
241
|
-
return
|
|
265
|
+
export function findPack(catalogue, id) {
|
|
266
|
+
return catalogue.packs.find(pack => pack.id === id) ?? fail(`There is no pack "${id}"${catalogue.offline ? ` among those that come with mdeck, and the theme repository could not be reached (${catalogue.offline})` : `; mdeck themes search lists them`}`)
|
|
242
267
|
}
|
|
243
268
|
|
|
269
|
+
// The ids every pack in the catalogue has, so packs do not take each other's.
|
|
270
|
+
export const ownersIn = catalogue => Object.fromEntries(catalogue.packs.flatMap(pack => [...pack.themes, ...pack.palettes].map(id => [id, pack.id])))
|
|
271
|
+
|
|
272
|
+
// A pack's files: read from mdeck's folder, or downloaded and checked
|
|
273
|
+
// against the repository's checksum.
|
|
274
|
+
export async function obtainPack(catalogue, entry) {
|
|
275
|
+
if (entry.bundled) { const { bundle, sha256: digest } = bundlePack(entry.dir); return { bundle, url: null, digest } }
|
|
276
|
+
return fetchPack({ url: catalogue.url }, entry)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// A pack and the packs it requires, in the order to install them; the pack
|
|
280
|
+
// itself last.
|
|
281
|
+
export function withRequirements(catalogue, id, seen = []) {
|
|
282
|
+
if (seen.includes(id)) fail(`The packs ${[...seen, id].join(' → ')} require each other in a circle`)
|
|
283
|
+
const entry = findPack(catalogue, id)
|
|
284
|
+
const order = []
|
|
285
|
+
for (const required of entry.requires ?? []) for (const pack of withRequirements(catalogue, required, [...seen, id])) if (!order.includes(pack)) order.push(pack)
|
|
286
|
+
return [...order, entry]
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Installs a pack and what it requires into an extensions folder. A required
|
|
290
|
+
// pack that `has(id)` says is there already is left as it is. `bundledHere`
|
|
291
|
+
// handles a bundled pack without writing it (for the user's folder, where the
|
|
292
|
+
// bundled one is offered anyway); it returns true when it did.
|
|
293
|
+
export async function installWithRequirements(catalogue, id, { root, mdeckVersion, force = false, has = () => false, bundledHere = null }) {
|
|
294
|
+
const owners = ownersIn(catalogue)
|
|
295
|
+
const provided = { themes: {}, palettes: {} }
|
|
296
|
+
const done = []
|
|
297
|
+
for (const entry of withRequirements(catalogue, id)) {
|
|
298
|
+
if (!satisfies(entry.mdeck, mdeckVersion)) fail(`${entry.id} ${entry.version} needs mdeck ${entry.mdeck}; this is ${mdeckVersion}. Update mdeck first: npm install -g mdeck`)
|
|
299
|
+
const { bundle, url, digest } = await obtainPack(catalogue, entry)
|
|
300
|
+
const { manifests } = checkPack(bundle, { provided, owners })
|
|
301
|
+
const target = entry.id === id
|
|
302
|
+
if (!target && has(entry.id)) done.push({ id: entry.id, version: entry.version, kept: true })
|
|
303
|
+
else if (entry.bundled && bundledHere?.(entry)) done.push({ id: entry.id, version: entry.version, bundled: true, themes: Object.keys(manifests.themes), palettes: Object.keys(manifests.palettes) })
|
|
304
|
+
else done.push({ id: entry.id, version: entry.version, ...installPack(bundle, { root, provided, owners, url, digest, force }) })
|
|
305
|
+
Object.assign(provided.themes, manifests.themes)
|
|
306
|
+
Object.assign(provided.palettes, manifests.palettes)
|
|
307
|
+
}
|
|
308
|
+
return done
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── The repository itself ────────────────────────────────────────────────
|
|
312
|
+
|
|
244
313
|
// The theme repository as served: index.json and packs/<id>-<version>.json,
|
|
245
|
-
// from a folder of pack folders
|
|
246
|
-
//
|
|
247
|
-
// share an id. Previews are added by the caller.
|
|
248
|
-
export function buildRepository(packsDir, outDir, {
|
|
249
|
-
const
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
314
|
+
// from a folder of pack folders and mdeck's bundled packs (marked bundled).
|
|
315
|
+
// Each pack is checked with only the packs it requires, as it is installed;
|
|
316
|
+
// no two packs may share an id. Previews are added by the caller.
|
|
317
|
+
export function buildRepository(packsDir, outDir, { bundled = {} } = {}) {
|
|
318
|
+
const dirs = { ...Object.fromEntries(readdirSync(packsDir).filter(name => !name.startsWith('.') && statSync(resolve(packsDir, name)).isDirectory()).map(name => [name, resolve(packsDir, name)])) }
|
|
319
|
+
for (const [id, dir] of Object.entries(bundled)) {
|
|
320
|
+
if (dirs[id]) fail(`${id} comes with mdeck; change it there instead of in ${packsDir}`)
|
|
321
|
+
dirs[id] = dir
|
|
322
|
+
}
|
|
323
|
+
const read = Object.fromEntries(Object.entries(dirs).map(([id, dir]) => [id, bundlePack(dir)]))
|
|
324
|
+
const owners = {}
|
|
325
|
+
for (const [id, { bundle }] of Object.entries(read)) {
|
|
326
|
+
for (const ext of Object.keys(bundle.files).map(path => path.split('/')[0])) {
|
|
327
|
+
if (owners[ext] && owners[ext] !== id) fail(`${ext} is in both ${owners[ext]} and ${id}`)
|
|
328
|
+
owners[ext] = id
|
|
259
329
|
}
|
|
330
|
+
}
|
|
331
|
+
const checked = {}
|
|
332
|
+
const check = (id, seen = []) => {
|
|
333
|
+
if (checked[id]) return checked[id]
|
|
334
|
+
if (!read[id]) fail(`${seen.at(-1)} requires ${id}, which is not in the repository`)
|
|
335
|
+
if (seen.includes(id)) fail(`The packs ${[...seen, id].join(' → ')} require each other in a circle`)
|
|
336
|
+
const provided = { themes: {}, palettes: {} }
|
|
337
|
+
for (const required of read[id].bundle.requires ?? []) {
|
|
338
|
+
const result = check(required, [...seen, id])
|
|
339
|
+
Object.assign(provided.themes, result.provides.themes); Object.assign(provided.palettes, result.provides.palettes)
|
|
340
|
+
}
|
|
341
|
+
const result = checkPack(read[id].bundle, { provided, owners })
|
|
342
|
+
return checked[id] = { ...result, provides: { themes: { ...provided.themes, ...result.manifests.themes }, palettes: { ...provided.palettes, ...result.manifests.palettes } } }
|
|
343
|
+
}
|
|
344
|
+
mkdirSync(resolve(outDir, 'packs'), { recursive: true })
|
|
345
|
+
const entries = Object.keys(read).sort((a, b) => a.localeCompare(b, 'en')).map(id => {
|
|
346
|
+
const { meta, text, sha256: digest } = read[id]
|
|
347
|
+
const { themes, palettes } = check(id)
|
|
260
348
|
const file = `packs/${meta.id}-${meta.version}.json`
|
|
261
349
|
writeFileSync(resolve(outDir, file), text)
|
|
262
|
-
|
|
263
|
-
...(meta.homepage ? { homepage: meta.homepage } : {}), ...(meta.mdeck ? { mdeck: meta.mdeck } : {}),
|
|
264
|
-
|
|
350
|
+
return { id: meta.id, title: meta.title, description: meta.description ?? '', version: meta.version, author: meta.author, license: meta.license,
|
|
351
|
+
...(meta.homepage ? { homepage: meta.homepage } : {}), ...(meta.mdeck ? { mdeck: meta.mdeck } : {}), ...(meta.requires?.length ? { requires: meta.requires } : {}),
|
|
352
|
+
...(bundled[id] ? { bundled: true } : {}), themes, palettes, url: file, sha256: digest }
|
|
353
|
+
})
|
|
265
354
|
const index = { schema: 1, packs: entries }
|
|
266
355
|
writeFileSync(resolve(outDir, 'index.json'), JSON.stringify(index, null, 2) + '\n')
|
|
267
356
|
return index
|
package/src/home/Home.jsx
CHANGED
|
@@ -66,24 +66,29 @@ async function openService(action, page = '') {
|
|
|
66
66
|
const tab = window.open('about:blank', '_blank')
|
|
67
67
|
try {
|
|
68
68
|
const { url } = await api('/action', { action })
|
|
69
|
-
const target = new URL(page, url)
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
const target = new URL(page, url)
|
|
70
|
+
// The editors link back here.
|
|
71
|
+
if (action === 'editor') target.searchParams.set('home', location.href.split('#')[0])
|
|
72
|
+
if (tab) tab.location = target.href
|
|
73
|
+
else window.open(target.href, '_blank')
|
|
72
74
|
} catch (error) {
|
|
73
75
|
tab?.close()
|
|
74
76
|
throw error
|
|
75
77
|
}
|
|
76
78
|
}
|
|
77
79
|
|
|
78
|
-
|
|
80
|
+
// `page` opens another page of the same service, such as the editor's
|
|
81
|
+
// design page.
|
|
82
|
+
function ServiceTile({ icon, title, text, action, page = '', url, onError, onStarted }) {
|
|
79
83
|
const [starting, setStarting] = useState(false)
|
|
80
84
|
const launch = async () => {
|
|
81
85
|
setStarting(true)
|
|
82
|
-
try { await openService(action); onStarted() } catch (error) { onError(error.message) } finally { setStarting(false) }
|
|
86
|
+
try { await openService(action, page); onStarted() } catch (error) { onError(error.message) } finally { setStarting(false) }
|
|
83
87
|
}
|
|
88
|
+
const shown = url && new URL(page, url).href
|
|
84
89
|
return <button class="home-tile" onClick={launch} disabled={starting}>
|
|
85
90
|
<span class="home-tile-icon"><HomeIcon name={icon} size={22} /></span>
|
|
86
|
-
<span class="home-tile-body"><strong>{title}</strong><span>{starting ? 'Starting…' :
|
|
91
|
+
<span class="home-tile-body"><strong>{title}</strong><span>{starting ? 'Starting…' : shown ? shown.replace(/^https?:\/\//, '') : text}</span></span>
|
|
87
92
|
<HomeIcon name="external" size={16} />
|
|
88
93
|
</button>
|
|
89
94
|
}
|
|
@@ -335,6 +340,7 @@ export function Home() {
|
|
|
335
340
|
<h2>Write and learn</h2>
|
|
336
341
|
<div class="home-tiles">
|
|
337
342
|
<ServiceTile icon="editor" title="Visual editor" text="Forms and a live preview; saves into the file and keeps a backup." action="editor" url={info.services.editor} onError={setError} onStarted={load} />
|
|
343
|
+
<ServiceTile icon="look" title="Theme editor" text="Try and fine-tune themes and palettes on a sample deck; install more." action="editor" page="design.html" url={info.services.editor} onError={setError} onStarted={load} />
|
|
338
344
|
<ServiceTile icon="guides" title="Guides" text="How to write slides, layouts, presenting, sharing and more." action="docs" url={info.services.docs} onError={setError} onStarted={load} />
|
|
339
345
|
</div>
|
|
340
346
|
</section>
|
|
@@ -62,8 +62,16 @@ export function extractContent(markdown, { headingLevels = [], paragraph = false
|
|
|
62
62
|
|
|
63
63
|
// An SVG picture that uses the theme's colour variables is drawn inline, so it
|
|
64
64
|
// follows the theme, palette and light or dark. The build lists them by the
|
|
65
|
-
// path the deck uses.
|
|
66
|
-
export const themedSvg = src => themedSvgs[src] ?? themedSvgs[String(src ?? '').split(/[?#]/)[0]]
|
|
65
|
+
// path the deck uses; an SVG data URL carries its drawing itself.
|
|
66
|
+
export const themedSvg = src => themedSvgs[src] ?? themedSvgs[String(src ?? '').split(/[?#]/)[0]] ?? themedDataSvg(src)
|
|
67
|
+
|
|
68
|
+
function themedDataSvg(src) {
|
|
69
|
+
const match = /^data:image\/svg\+xml(;base64)?,(.*)$/s.exec(String(src ?? ''))
|
|
70
|
+
if (!match) return null
|
|
71
|
+
let markup
|
|
72
|
+
try { markup = match[1] ? atob(match[2]) : decodeURIComponent(match[2]) } catch { return null }
|
|
73
|
+
return /var\(--/.test(markup) && /^\s*<svg\b/.test(markup) ? markup : null
|
|
74
|
+
}
|
|
67
75
|
|
|
68
76
|
const attribute = (tag, name) => tag.match(new RegExp(`\\s${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, 'i'))?.slice(1).find(v => v != null)
|
|
69
77
|
|
package/src/paths.js
CHANGED
|
@@ -7,3 +7,5 @@ export const runtimeRoot = resolve(frameworkRoot, 'src/runtime')
|
|
|
7
7
|
export const assetsRoot = resolve(frameworkRoot, 'assets')
|
|
8
8
|
export const docsRoot = resolve(frameworkRoot, 'docs/site')
|
|
9
9
|
export const builtinExtensionsRoot = resolve(assetsRoot, 'extensions')
|
|
10
|
+
// The packs of themes and palettes that come with mdeck (the starter set).
|
|
11
|
+
export const bundledPacksRoot = resolve(assetsRoot, 'packs')
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import baseCSS from '../../assets/base.css?inline'
|
|
2
2
|
import { themes, palettes } from 'virtual:mdeck-extensions'
|
|
3
3
|
import { buildAppearance } from '../extensions/appearance.js'
|
|
4
|
+
import { movedHint } from '../extensions/moved.js'
|
|
4
5
|
|
|
5
6
|
const SELF_CONTAINED = typeof __MDECK_SELF_CONTAINED__ !== 'undefined'
|
|
6
7
|
&& __MDECK_SELF_CONTAINED__
|
|
@@ -48,8 +49,10 @@ function syncThemeFonts(urls) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export async function loadTheme({ theme = 'neue', palette, appearance, params = {}, meta = {} } = {}) {
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
// A deck without a theme gets neue, or the first installed theme when neue
|
|
53
|
+
// was removed.
|
|
54
|
+
const entry = themeEntry(theme) ?? (theme === 'neue' && THEME_NAMES.length ? themeEntry(THEME_NAMES[0]) : null)
|
|
55
|
+
if (!entry) throw new Error(movedHint('theme', theme) ?? `Unknown theme: "${theme}". Available: ${THEME_NAMES.join(', ') || 'none; install one with mdeck themes install'}`)
|
|
53
56
|
|
|
54
57
|
const styles = await entry.load()
|
|
55
58
|
const look = buildAppearance({
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
schema = 1
|
|
2
|
-
kind = "palette"
|
|
3
|
-
id = "brand"
|
|
4
|
-
title = "FHNW"
|
|
5
|
-
description = "The FHNW corporate colours: black, white and FHNW yellow."
|
|
6
|
-
# Only for the fhnw theme: its corporate colours.
|
|
7
|
-
theme = "fhnw"
|
|
8
|
-
|
|
9
|
-
[light]
|
|
10
|
-
"--bg" = "#ffffff"
|
|
11
|
-
"--surface" = "#f1f1ee"
|
|
12
|
-
"--ink" = "#000000"
|
|
13
|
-
"--ink-soft" = "#262626"
|
|
14
|
-
"--muted" = "#6e6e6e"
|
|
15
|
-
"--rule" = "#deded9"
|
|
16
|
-
"--accent" = "#fde70e"
|
|
17
|
-
"--accent-2" = "#000000"
|
|
18
|
-
"--on-accent" = "#000000"
|
|
19
|
-
|
|
20
|
-
[dark]
|
|
21
|
-
"--bg" = "#000000"
|
|
22
|
-
"--surface" = "#1c1c1c"
|
|
23
|
-
"--ink" = "#ffffff"
|
|
24
|
-
"--ink-soft" = "#dcdcdc"
|
|
25
|
-
"--muted" = "#9a9a9a"
|
|
26
|
-
"--rule" = "#333333"
|
|
27
|
-
"--accent" = "#fde70e"
|
|
28
|
-
"--accent-2" = "#ffffff"
|
|
29
|
-
"--on-accent" = "#000000"
|