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.
Files changed (45) hide show
  1. package/CHANGELOG.md +11 -1
  2. package/assets/{packs/lagoon → extensions/palettes}/lagoon/extension.toml +3 -0
  3. package/assets/{packs/aurora → extensions/palettes}/neon/extension.toml +3 -0
  4. package/assets/{packs/academic → extensions/palettes}/nordic/extension.toml +3 -0
  5. package/assets/{packs/minimal → extensions/palettes}/paper/extension.toml +3 -0
  6. package/assets/{packs/neue → extensions/palettes}/swiss/extension.toml +3 -0
  7. package/assets/{packs/academic → extensions/themes}/academic/extension.toml +3 -0
  8. package/assets/{packs/aurora → extensions/themes}/aurora/extension.toml +3 -0
  9. package/assets/{packs/minimal → extensions/themes}/minimal/extension.toml +3 -0
  10. package/assets/{packs/neue → extensions/themes}/neue/extension.toml +3 -0
  11. package/docs/reference/authoring.md +3 -3
  12. package/docs/reference/extensions.md +40 -35
  13. package/docs/reference/palettes.md +1 -1
  14. package/docs/reference/themes.md +3 -3
  15. package/docs/site/content/appearance.md +11 -9
  16. package/docs/site/content/commands.md +2 -1
  17. package/docs/site/content/theme-authoring.md +1 -1
  18. package/docs/site/content/theme-repository.md +36 -28
  19. package/package.json +2 -2
  20. package/skills/write-slides/SKILL.md +1 -1
  21. package/src/build/editorPlugin.js +22 -17
  22. package/src/build/themeCheck.js +2 -1
  23. package/src/cli/args.js +2 -1
  24. package/src/cli/index.js +7 -6
  25. package/src/cli/themes.js +92 -91
  26. package/src/editor/api.js +11 -10
  27. package/src/editor/components/DesignApp.jsx +9 -11
  28. package/src/editor/components/ExtensionEditor.jsx +10 -11
  29. package/src/editor/components/ExtensionsPanel.jsx +24 -28
  30. package/src/extensions/discover.js +28 -35
  31. package/src/extensions/installed.js +73 -48
  32. package/src/extensions/manifest.js +20 -3
  33. package/src/extensions/moved.js +14 -7
  34. package/src/extensions/packages.js +318 -0
  35. package/src/paths.js +0 -2
  36. package/assets/packs/academic/pack.toml +0 -8
  37. package/assets/packs/aurora/pack.toml +0 -8
  38. package/assets/packs/lagoon/pack.toml +0 -8
  39. package/assets/packs/minimal/pack.toml +0 -8
  40. package/assets/packs/neue/pack.toml +0 -8
  41. package/src/extensions/packs.js +0 -357
  42. /package/assets/{packs/academic → extensions/themes}/academic/styles.css +0 -0
  43. /package/assets/{packs/aurora → extensions/themes}/aurora/styles.css +0 -0
  44. /package/assets/{packs/minimal → extensions/themes}/minimal/styles.css +0 -0
  45. /package/assets/{packs/neue → extensions/themes}/neue/styles.css +0 -0
@@ -0,0 +1,318 @@
1
+ // Packages: every theme and every palette can be installed on its own, from
2
+ // mdeck itself (the built-in ones) or from the theme repository
3
+ // (https://gh.tschieber.de/mdeck-themes/). A package is one extension folder;
4
+ // it travels as one JSON file of its text files, so installing it is checking
5
+ // it and writing those files. Only extension.toml and, for a theme,
6
+ // styles.css are allowed: a package never carries code.
7
+ // What a package needs follows from its manifest: a theme needs its default
8
+ // palette, and a palette that belongs to one theme needs that theme. Those
9
+ // are installed along with it.
10
+ import { createHash } from 'node:crypto'
11
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs'
12
+ import { resolve, relative, basename } from 'node:path'
13
+ import { ID_RE, ManifestError, RANGE_RE, parseManifestText, validateManifest } from './manifest.js'
14
+ import { builtinExtensionsRoot } from '../paths.js'
15
+
16
+ export const CATALOGUE_URL = 'https://gh.tschieber.de/mdeck-themes/catalogue.json'
17
+ export const MARKER = '.mdeck-package.json'
18
+ const PACKAGE_KINDS = ['theme', 'palette']
19
+ const FILES = { theme: ['extension.toml', 'styles.css'], palette: ['extension.toml'] }
20
+ // Where theme fonts may come from: stylesheet services that only serve fonts.
21
+ export const FONT_HOSTS = ['https://fonts.googleapis.com/', 'https://fonts.bunny.net/']
22
+
23
+ export class PackageError extends Error {}
24
+ const fail = message => { throw new PackageError(message) }
25
+
26
+ export const sha256 = text => createHash('sha256').update(text, 'utf8').digest('hex')
27
+ export const keyOf = (kind, id) => `${kind}:${id}`
28
+
29
+ export function catalogueUrl() {
30
+ return process.env.MDECK_THEMES_URL || CATALOGUE_URL
31
+ }
32
+
33
+ const parts = version => version.split('.').map(Number)
34
+ export function compareVersions(a, b) {
35
+ const [x, y] = [parts(a), parts(b)]
36
+ for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] - y[i]
37
+ return 0
38
+ }
39
+
40
+ // Ranges are only of the form ">=1.2.3"; no range means any version.
41
+ export function satisfies(range, version) {
42
+ if (!range) return true
43
+ const match = RANGE_RE.exec(range)
44
+ return Boolean(match) && compareVersions(version, match[1]) >= 0
45
+ }
46
+
47
+ function checkCss(text, where) {
48
+ if (/@import/i.test(text)) fail(`${where}: @import is not allowed; list font stylesheets under fonts in extension.toml`)
49
+ for (const match of text.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi)) {
50
+ if (!match[2].trim().startsWith('data:')) fail(`${where}: url(${match[2]}) is not allowed; only data: URLs`)
51
+ }
52
+ }
53
+
54
+ // Checks one package's files: what it may hold, its manifest, its fonts and
55
+ // stylesheet. `published` also requires version, author and license, as the
56
+ // theme repository does. Returns the validated record.
57
+ export function checkPackage({ kind, id, files }, { published = false } = {}) {
58
+ if (!PACKAGE_KINDS.includes(kind)) fail(`${kind} is not a kind of package; only themes and palettes are`)
59
+ if (typeof id !== 'string' || !ID_RE.test(id)) fail(`"${id}" is not an id: lowercase letters, digits and hyphens, starting with a letter`)
60
+ if (!files || typeof files !== 'object') fail(`The ${kind} ${id} has no files`)
61
+ for (const [name, text] of Object.entries(files)) {
62
+ if (!FILES[kind].includes(name)) fail(`The ${kind} ${id}: "${name}" is not allowed; a ${kind} holds only ${FILES[kind].join(' and ')}`)
63
+ if (typeof text !== 'string') fail(`The ${kind} ${id}: ${name} is not text`)
64
+ }
65
+ if (!files['extension.toml']) fail(`The ${kind} ${id}: extension.toml is missing`)
66
+ const dir = `/package/${id}`
67
+ const file = `${dir}/extension.toml`
68
+ let record
69
+ try {
70
+ record = validateManifest(parseManifestText(files['extension.toml'], file), { file, dir, folderName: id, fileExists: path => Object.hasOwn(files, relative(dir, path)) })
71
+ } catch (error) {
72
+ if (error instanceof ManifestError) fail(`The ${kind} ${id}: ${error.message.replace(`${file}: `, '')}`)
73
+ throw error
74
+ }
75
+ if (record.kind !== kind) fail(`${id}/extension.toml says kind = "${record.kind}", but it is in the ${kind}s`)
76
+ if (published) for (const key of ['version', 'author', 'license']) if (!record.package?.[key]) fail(`The ${kind} ${id}: ${key} is required in the theme repository`)
77
+ if (kind === 'theme') {
78
+ for (const url of record.manifest.fonts) if (!FONT_HOSTS.some(host => url.startsWith(host))) fail(`The theme ${id} loads fonts from ${url}; allowed are ${FONT_HOSTS.join(', ')}`)
79
+ checkCss(files['styles.css'] ?? '', `The theme ${id}: styles.css`)
80
+ }
81
+ return record
82
+ }
83
+
84
+ // What a package needs installed with it: a theme its default palette, a
85
+ // palette of one theme that theme.
86
+ export function dependencies(kind, manifest) {
87
+ if (kind === 'theme') return [{ kind: 'palette', id: manifest.palette }]
88
+ return manifest.theme ? [{ kind: 'theme', id: manifest.theme }] : []
89
+ }
90
+
91
+ // A package folder's files and record; `kind` comes from the manifest.
92
+ export function readPackageDir(dir, { published = false } = {}) {
93
+ const id = basename(dir)
94
+ const files = {}
95
+ for (const name of readdirSync(dir).sort()) {
96
+ if (name.startsWith('.') || name === 'README.md') continue
97
+ if (!statSync(resolve(dir, name)).isFile()) fail(`${resolve(dir, name)}: a package holds only files`)
98
+ files[name] = readFileSync(resolve(dir, name), 'utf8')
99
+ }
100
+ const kind = /^kind\s*=\s*"([a-z]+)"/m.exec(files['extension.toml'] ?? '')?.[1]
101
+ const record = checkPackage({ kind, id, files }, { published })
102
+ return { kind, id, files, record }
103
+ }
104
+
105
+ // The published form of a package folder: one JSON text and its checksum.
106
+ export function bundlePackage(dir, options) {
107
+ const { kind, id, files, record } = readPackageDir(dir, options)
108
+ const bundle = { schema: 2, kind, id, version: record.package?.version ?? '0.0.0', ...(record.package?.mdeck ? { mdeck: record.package.mdeck } : {}), files }
109
+ const text = JSON.stringify(bundle, null, 1) + '\n'
110
+ return { bundle, text, sha256: sha256(text), record }
111
+ }
112
+
113
+ // ─── Installed packages ───────────────────────────────────────────────────
114
+
115
+ const readJson = file => { try { return JSON.parse(readFileSync(file, 'utf8')) } catch { return null } }
116
+ const packageDir = (root, kind, id) => resolve(root, `${kind}s`, id)
117
+
118
+ // Files changed since the package was installed, by name.
119
+ function changedFiles(dir, marker) {
120
+ return Object.entries(marker.files ?? {}).filter(([name, hash]) => {
121
+ const file = resolve(dir, name)
122
+ return !existsSync(file) || sha256(readFileSync(file, 'utf8')) !== hash
123
+ }).map(([name]) => name)
124
+ }
125
+
126
+ const refuseChanged = (dir, marker) => {
127
+ const changed = changedFiles(dir, marker)
128
+ if (changed.length) fail(`${dir} was changed since it was installed (${changed.join(', ')}); add --force to replace or remove it anyway`)
129
+ }
130
+
131
+ // The packages installed in an extensions folder (in its themes/ and
132
+ // palettes/), by "kind:id".
133
+ export function installedPackages(root) {
134
+ const found = {}
135
+ for (const kind of PACKAGE_KINDS) {
136
+ const dir = resolve(root, `${kind}s`)
137
+ if (!existsSync(dir)) continue
138
+ for (const id of readdirSync(dir).sort()) {
139
+ const marker = readJson(resolve(dir, id, MARKER))
140
+ if (marker?.kind === kind) found[keyOf(kind, id)] = { kind, id, version: marker.version ?? null, url: marker.url ?? null, dir: resolve(dir, id), marker }
141
+ }
142
+ }
143
+ return found
144
+ }
145
+
146
+ // Writes a checked package into an extensions folder (a deck's, or the
147
+ // user's), as <kind>s/<id>. A folder of the deck's own is never touched; one
148
+ // installed before is replaced unless it was changed by hand.
149
+ export function installPackage(bundle, { root, url = null, digest = null, force = false } = {}) {
150
+ const { kind, id } = bundle
151
+ const record = checkPackage(bundle)
152
+ const dir = packageDir(root, kind, id)
153
+ const before = installedPackages(root)[keyOf(kind, id)]
154
+ if (existsSync(dir) && !readJson(resolve(dir, MARKER))) fail(`${dir} already exists and was not installed by mdeck; rename or remove it first`)
155
+ if (before && !force) refuseChanged(before.dir, before.marker)
156
+ // Files are overwritten in place, not deleted and written again, so a
157
+ // running editor sees a change instead of a file that disappears.
158
+ mkdirSync(dir, { recursive: true })
159
+ const hashes = {}
160
+ for (const [name, content] of Object.entries(bundle.files)) {
161
+ writeFileSync(resolve(dir, name), content, 'utf8')
162
+ hashes[name] = sha256(content)
163
+ }
164
+ for (const name of readdirSync(dir)) if (name !== MARKER && !Object.hasOwn(hashes, name)) rmSync(resolve(dir, name), { recursive: true, force: true })
165
+ writeFileSync(resolve(dir, MARKER), JSON.stringify({ kind, id, version: bundle.version, url, sha256: digest, files: hashes }, null, 2) + '\n')
166
+ return { record, dir, replaced: before?.version ?? null }
167
+ }
168
+
169
+ export function removePackage(kind, id, { root, force = false } = {}) {
170
+ const installed = installedPackages(root)[keyOf(kind, id)]
171
+ if (!installed) fail(`The ${kind} ${id} is not installed in ${root}`)
172
+ if (!force) refuseChanged(installed.dir, installed.marker)
173
+ rmSync(installed.dir, { recursive: true, force: true })
174
+ return installed
175
+ }
176
+
177
+ // ─── The catalogue: built-in packages and the repository ──────────────────
178
+
179
+ async function get(url, as) {
180
+ let response
181
+ try { response = await fetch(url) } catch (error) { fail(`Could not reach ${url}: ${error.cause?.message ?? error.message}`) }
182
+ if (!response.ok) fail(`${url} answered ${response.status}`)
183
+ return as === 'json' ? response.json() : response.text()
184
+ }
185
+
186
+ export async function fetchCatalogue(url = catalogueUrl()) {
187
+ const data = await get(url, 'json')
188
+ if (data?.schema !== 2 || !Array.isArray(data.themes) || !Array.isArray(data.palettes)) fail(`${url} is not a theme repository catalogue`)
189
+ return data
190
+ }
191
+
192
+ // One entry of the catalogue, from a package's record.
193
+ function entryOf(kind, record) {
194
+ return {
195
+ kind, id: record.id, title: record.title, description: record.description ?? '',
196
+ version: record.package?.version ?? '0.0.0', ...(record.package?.author ? { author: record.package.author } : {}), ...(record.package?.license ? { license: record.package.license } : {}),
197
+ ...(record.package?.homepage ? { homepage: record.package.homepage } : {}), ...(record.package?.mdeck ? { mdeck: record.package.mdeck } : {}),
198
+ ...(kind === 'theme' ? { palette: record.manifest.palette, ...(record.manifest.palettes?.length ? { palettes: record.manifest.palettes } : {}), appearance: record.manifest.appearance ?? 'light' } : {}),
199
+ ...(kind === 'palette' && record.manifest.theme ? { theme: record.manifest.theme } : {}),
200
+ }
201
+ }
202
+
203
+ // The themes and palettes that come with mdeck, as catalogue entries.
204
+ export function builtInEntries(root = builtinExtensionsRoot) {
205
+ const entries = []
206
+ for (const kind of PACKAGE_KINDS) {
207
+ const dir = resolve(root, `${kind}s`)
208
+ if (!existsSync(dir)) continue
209
+ for (const id of readdirSync(dir).sort()) {
210
+ if (id.startsWith('.')) continue
211
+ const { record } = readPackageDir(resolve(dir, id))
212
+ entries.push({ ...entryOf(kind, record), builtIn: true, dir: resolve(dir, id) })
213
+ }
214
+ }
215
+ return entries
216
+ }
217
+
218
+ // Every theme and palette mdeck can install: the built-in ones, and the
219
+ // repository's when it answers. Of two versions the newer is offered; the
220
+ // built-in one on a tie, since it needs no download. `offline` says why the
221
+ // repository is missing.
222
+ export async function loadCatalogue({ url = catalogueUrl(), online = true } = {}) {
223
+ const entries = new Map(builtInEntries().map(entry => [keyOf(entry.kind, entry.id), entry]))
224
+ let offline = null
225
+ let remote = { themes: [], palettes: [] }
226
+ if (online) { try { remote = await fetchCatalogue(url) } catch (error) { offline = error.message } }
227
+ for (const entry of [...remote.themes.map(e => ({ ...e, kind: 'theme' })), ...remote.palettes.map(e => ({ ...e, kind: 'palette' }))]) {
228
+ const key = keyOf(entry.kind, entry.id)
229
+ const have = entries.get(key)
230
+ if (entry.builtIn) { if (have) have.preview = entry.preview; continue }
231
+ if (!have || compareVersions(entry.version, have.version) > 0) entries.set(key, { ...entry, builtIn: false, builtInVersion: have?.version })
232
+ }
233
+ const sorted = [...entries.values()].sort((a, b) => a.kind.localeCompare(b.kind) || a.id.localeCompare(b.id, 'en'))
234
+ return { url, offline, entries: sorted, get: (kind, id) => entries.get(keyOf(kind, id)) }
235
+ }
236
+
237
+ export function findEntry(catalogue, kind, id) {
238
+ return catalogue.get(kind, id) ?? fail(`There is no ${kind} "${id}"${catalogue.offline ? ` among those that come with mdeck, and the theme repository could not be reached (${catalogue.offline})` : `; mdeck ${kind}s search lists them`}`)
239
+ }
240
+
241
+ // A package's files: read from mdeck's folder, or downloaded and checked
242
+ // against the repository's checksum.
243
+ export async function obtainPackage(catalogue, entry) {
244
+ if (entry.builtIn) { const { bundle, sha256: digest } = bundlePackage(entry.dir); return { bundle, url: null, digest } }
245
+ const url = new URL(entry.url, catalogue.url).href
246
+ const text = await get(url, 'text')
247
+ if (sha256(text) !== entry.sha256) fail(`${url} does not match its checksum in the catalogue; not installed`)
248
+ let bundle
249
+ try { bundle = JSON.parse(text) } catch { fail(`${url} is not a package`) }
250
+ if (bundle.kind !== entry.kind || bundle.id !== entry.id || bundle.version !== entry.version) fail(`${url} holds the ${bundle.kind} ${bundle.id} ${bundle.version}, not the ${entry.kind} ${entry.id} ${entry.version}`)
251
+ return { bundle, url, digest: entry.sha256 }
252
+ }
253
+
254
+ // A package and what it needs, in the order to install them: what is needed
255
+ // first, the package itself last. A theme and its own palette need each
256
+ // other; each is installed once.
257
+ export function withDependencies(catalogue, kind, id, seen = new Set()) {
258
+ const key = keyOf(kind, id)
259
+ if (seen.has(key)) return []
260
+ seen.add(key)
261
+ const entry = findEntry(catalogue, kind, id)
262
+ const needs = kind === 'theme' ? [{ kind: 'palette', id: entry.palette }] : entry.theme ? [{ kind: 'theme', id: entry.theme }] : []
263
+ const order = []
264
+ for (const need of needs) for (const item of withDependencies(catalogue, need.kind, need.id, seen)) if (!order.includes(item)) order.push(item)
265
+ return [...order, entry]
266
+ }
267
+
268
+ // ─── The repository itself ────────────────────────────────────────────────
269
+
270
+ // The theme repository as served: catalogue.json, and <kind>s/<id>-<version>.json
271
+ // for each package, from a folder with themes/ and palettes/. mdeck's
272
+ // built-in packages are listed too (marked builtIn), so the repository's
273
+ // themes may use built-in palettes. Every package is checked, no id appears
274
+ // twice in a kind, and what each needs is in the catalogue. Previews are added
275
+ // by the caller.
276
+ export function buildRepository(repoDir, outDir) {
277
+ const builtIn = builtInEntries()
278
+ const records = {}
279
+ const add = (kind, id, value) => {
280
+ if (records[keyOf(kind, id)]) fail(`There are two ${kind}s "${id}"${value.builtIn || records[keyOf(kind, id)].builtIn ? '; one comes with mdeck, change it there' : ''}`)
281
+ records[keyOf(kind, id)] = value
282
+ }
283
+ for (const entry of builtIn) add(entry.kind, entry.id, { entry, builtIn: true, ...bundlePackage(entry.dir) })
284
+ for (const kind of PACKAGE_KINDS) {
285
+ const dir = resolve(repoDir, `${kind}s`)
286
+ if (!existsSync(dir)) continue
287
+ for (const id of readdirSync(dir).sort()) {
288
+ if (id.startsWith('.') || !statSync(resolve(dir, id)).isDirectory()) continue
289
+ const read = bundlePackage(resolve(dir, id), { published: true })
290
+ if (read.bundle.kind !== kind) fail(`${kind}s/${id} holds a ${read.bundle.kind}`)
291
+ add(kind, id, { entry: entryOf(kind, read.record), builtIn: false, dir: resolve(dir, id), ...read })
292
+ }
293
+ }
294
+ // What each needs must be there, and a theme may offer only palettes that
295
+ // are open to it.
296
+ for (const { record, bundle } of Object.values(records)) {
297
+ for (const need of dependencies(bundle.kind, record.manifest)) {
298
+ if (!records[keyOf(need.kind, need.id)]) fail(`The ${bundle.kind} ${bundle.id} needs the ${need.kind} "${need.id}", which is neither in the repository nor built into mdeck`)
299
+ }
300
+ if (bundle.kind === 'theme') for (const id of [record.manifest.palette, ...(record.manifest.palettes ?? [])]) {
301
+ const palette = records[keyOf('palette', id)]
302
+ if (!palette) fail(`The theme ${bundle.id} offers the palette "${id}", which is neither in the repository nor built into mdeck`)
303
+ const owner = palette.record.manifest.theme
304
+ if (owner && owner !== bundle.id) fail(`The theme ${bundle.id} offers the palette ${id}, which belongs to the theme ${owner}`)
305
+ }
306
+ }
307
+ const catalogue = { schema: 2, themes: [], palettes: [] }
308
+ for (const key of Object.keys(records).sort()) {
309
+ const { entry, text, sha256: digest, bundle, builtIn: isBuiltIn } = records[key]
310
+ const file = `${bundle.kind}s/${bundle.id}-${bundle.version}.json`
311
+ mkdirSync(resolve(outDir, `${bundle.kind}s`), { recursive: true })
312
+ writeFileSync(resolve(outDir, file), text)
313
+ const { dir, ...listed } = entry
314
+ catalogue[`${bundle.kind}s`].push({ ...listed, ...(isBuiltIn ? { builtIn: true } : {}), url: file, sha256: digest })
315
+ }
316
+ writeFileSync(resolve(outDir, 'catalogue.json'), JSON.stringify(catalogue, null, 2) + '\n')
317
+ return { catalogue, dirs: Object.fromEntries(Object.entries(records).map(([key, value]) => [key, value.dir ?? value.entry.dir])) }
318
+ }
package/src/paths.js CHANGED
@@ -7,5 +7,3 @@ 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,8 +0,0 @@
1
- schema = 1
2
- id = "academic"
3
- title = "Academic"
4
- description = "A lecture: Source Sans and Source Serif, compact slides with the heading in a fixed place, theorem and definition blocks, booktabs tables and a footnote rule."
5
- version = "1.0.0"
6
- author = "Tilman Schieber"
7
- license = "MIT"
8
- mdeck = ">=3.0.0"
@@ -1,8 +0,0 @@
1
- schema = 1
2
- id = "aurora"
3
- title = "Aurora"
4
- description = "Northern lights: curtains of light in the accent colours, night-sky chapter and statement slides, frosted glass."
5
- version = "1.0.0"
6
- author = "Tilman Schieber"
7
- license = "MIT"
8
- mdeck = ">=3.0.0"
@@ -1,8 +0,0 @@
1
- schema = 1
2
- id = "lagoon"
3
- title = "Lagoon"
4
- description = "Teal on warm stone, with an orange second accent."
5
- version = "1.0.0"
6
- author = "Tilman Schieber"
7
- license = "MIT"
8
- mdeck = ">=3.0.0"
@@ -1,8 +0,0 @@
1
- schema = 1
2
- id = "minimal"
3
- title = "Minimal"
4
- description = "Nothing but the content: system fonts, calm type, plenty of space, no decoration. Works offline."
5
- version = "1.0.0"
6
- author = "Tilman Schieber"
7
- license = "MIT"
8
- mdeck = ">=3.0.0"
@@ -1,8 +0,0 @@
1
- schema = 1
2
- id = "neue"
3
- title = "Neue"
4
- description = "Swiss typography: large flush-left type, rules instead of boxes, signal red on white."
5
- version = "1.0.0"
6
- author = "Tilman Schieber"
7
- license = "MIT"
8
- mdeck = ">=3.0.0"