mdeck 2.3.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 +259 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/assets/base.css +196 -0
- package/assets/extensions/layouts/chapter/extension.toml +26 -0
- package/assets/extensions/layouts/chapter/layout.jsx +21 -0
- package/assets/extensions/layouts/chapter/starter.md +8 -0
- package/assets/extensions/layouts/focus/extension.toml +8 -0
- package/assets/extensions/layouts/focus/layout.jsx +10 -0
- package/assets/extensions/layouts/focus/starter.md +4 -0
- package/assets/extensions/layouts/full-bleed-image/extension.toml +31 -0
- package/assets/extensions/layouts/full-bleed-image/layout.jsx +19 -0
- package/assets/extensions/layouts/full-bleed-image/starter.md +6 -0
- package/assets/extensions/layouts/generic/extension.toml +8 -0
- package/assets/extensions/layouts/generic/layout.jsx +8 -0
- package/assets/extensions/layouts/generic/starter.md +4 -0
- package/assets/extensions/layouts/image-text/extension.toml +25 -0
- package/assets/extensions/layouts/image-text/layout.jsx +17 -0
- package/assets/extensions/layouts/image-text/starter.md +6 -0
- package/assets/extensions/layouts/split/extension.toml +25 -0
- package/assets/extensions/layouts/split/layout.jsx +19 -0
- package/assets/extensions/layouts/split/starter.md +14 -0
- package/assets/extensions/layouts/title/extension.toml +26 -0
- package/assets/extensions/layouts/title/layout.jsx +14 -0
- package/assets/extensions/layouts/title/starter.md +5 -0
- package/assets/extensions/palettes/brand/extension.toml +29 -0
- package/assets/extensions/palettes/cobalt/extension.toml +27 -0
- package/assets/extensions/palettes/ember/extension.toml +27 -0
- package/assets/extensions/palettes/forest/extension.toml +27 -0
- package/assets/extensions/palettes/graphite/extension.toml +27 -0
- package/assets/extensions/palettes/lagoon/extension.toml +27 -0
- package/assets/extensions/palettes/neon/extension.toml +27 -0
- package/assets/extensions/palettes/nordic/extension.toml +27 -0
- package/assets/extensions/palettes/pastel/extension.toml +30 -0
- package/assets/extensions/palettes/phosphor/extension.toml +27 -0
- package/assets/extensions/palettes/swiss/extension.toml +27 -0
- package/assets/extensions/palettes/terra/extension.toml +27 -0
- package/assets/extensions/themes/academic/extension.toml +53 -0
- package/assets/extensions/themes/academic/styles.css +631 -0
- package/assets/extensions/themes/aurora/extension.toml +37 -0
- package/assets/extensions/themes/aurora/styles.css +644 -0
- package/assets/extensions/themes/duet/extension.toml +46 -0
- package/assets/extensions/themes/duet/styles.css +573 -0
- package/assets/extensions/themes/editorial/extension.toml +46 -0
- package/assets/extensions/themes/editorial/styles.css +574 -0
- package/assets/extensions/themes/fhnw/extension.toml +61 -0
- package/assets/extensions/themes/fhnw/styles.css +604 -0
- package/assets/extensions/themes/neue/extension.toml +46 -0
- package/assets/extensions/themes/neue/styles.css +489 -0
- package/assets/extensions/themes/sketch/extension.toml +48 -0
- package/assets/extensions/themes/sketch/styles.css +633 -0
- package/assets/extensions/themes/terminal/extension.toml +38 -0
- package/assets/extensions/themes/terminal/styles.css +550 -0
- package/assets/logo/avatar.png +0 -0
- package/assets/logo/logo-dark.svg +10 -0
- package/assets/logo/logo-light.svg +10 -0
- package/assets/logo/mark.svg +15 -0
- package/bin/mdeck.js +2 -0
- package/docs/README.md +8 -0
- package/docs/reference/authoring.md +795 -0
- package/docs/reference/claude-skill.md +96 -0
- package/docs/reference/extensions.md +250 -0
- package/docs/reference/layouts.md +153 -0
- package/docs/reference/palettes.md +106 -0
- package/docs/reference/structured-slides.md +120 -0
- package/docs/reference/themes.md +133 -0
- package/docs/site/assets/favicon.svg +15 -0
- package/docs/site/assets/github-logo.svg +1 -0
- package/docs/site/assets/site.css +162 -0
- package/docs/site/assets/site.js +123 -0
- package/docs/site/build.js +148 -0
- package/docs/site/content/appearance.md +203 -0
- package/docs/site/content/audience.md +178 -0
- package/docs/site/content/commands.md +153 -0
- package/docs/site/content/components.md +134 -0
- package/docs/site/content/drawing.md +72 -0
- package/docs/site/content/editing.md +59 -0
- package/docs/site/content/getting-started.md +103 -0
- package/docs/site/content/layouts.md +126 -0
- package/docs/site/content/media.md +107 -0
- package/docs/site/content/more-content.md +130 -0
- package/docs/site/content/presenting.md +93 -0
- package/docs/site/content/reusable-layouts.md +74 -0
- package/docs/site/content/sharing.md +88 -0
- package/docs/site/content/theme-authoring.md +144 -0
- package/docs/site/content/theme-repository.md +76 -0
- package/docs/site/content/troubleshooting.md +92 -0
- package/docs/site/content/views.md +56 -0
- package/docs/site/content/welcome.md +71 -0
- package/docs/site/content/writing.md +121 -0
- package/docs/site/examples/first-talk.md +88 -0
- package/docs/site/examples/notebook.svg +58 -0
- 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/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/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/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/docs/site/package.json +9 -0
- package/docs/site/pages.js +27 -0
- package/docs/site/server.js +58 -0
- package/examples/custom-layouts/extensions/comparison/extension.toml +35 -0
- package/examples/custom-layouts/extensions/comparison/layout.jsx +15 -0
- package/examples/custom-layouts/extensions/comparison/starter.md +17 -0
- package/examples/custom-layouts/extensions/comparison/styles.css +4 -0
- package/examples/custom-layouts/slides.md +60 -0
- package/examples/poll/slides.md +127 -0
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +19 -0
- package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +6 -0
- package/examples/python/slides.drawings.json +6 -0
- package/examples/python/slides.md +493 -0
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +10 -0
- package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +13 -0
- package/examples/showcase/img/image.jpg +0 -0
- package/examples/showcase/img/mdeck.svg +66 -0
- package/examples/showcase/slides.drawings.json +11 -0
- package/examples/showcase/slides.md +427 -0
- package/package.json +75 -0
- package/skills/write-slides/SKILL.md +144 -0
- package/src/build/check.js +49 -0
- package/src/build/chrome.js +130 -0
- package/src/build/components.js +44 -0
- package/src/build/config.js +61 -0
- package/src/build/drawings.js +14 -0
- package/src/build/editorPlugin.js +272 -0
- package/src/build/editorServer.js +36 -0
- package/src/build/fonts.js +84 -0
- package/src/build/homePlugin.js +208 -0
- package/src/build/inkPlugin.js +192 -0
- package/src/build/ownWrites.js +18 -0
- package/src/build/pairing.js +81 -0
- package/src/build/pdf.js +34 -0
- package/src/build/relayAccess.js +85 -0
- package/src/build/slidesPlugin.js +371 -0
- package/src/build/themeCheck.js +151 -0
- package/src/build/tunnelClient.js +184 -0
- package/src/cli/args.js +76 -0
- package/src/cli/index.js +758 -0
- package/src/cli/migrate.js +123 -0
- package/src/cli/skill.js +40 -0
- package/src/cli/themes.js +140 -0
- package/src/components/CodeBlock.jsx +168 -0
- package/src/components/ConnectedViews.jsx +25 -0
- package/src/components/Icon.jsx +8 -0
- package/src/components/Poll.jsx +67 -0
- package/src/components/QrCode.jsx +29 -0
- package/src/components/QrCodeTag.jsx +27 -0
- package/src/components/Question.jsx +28 -0
- package/src/components/Scale.jsx +47 -0
- package/src/components/VideoPlayer.jsx +108 -0
- package/src/components/WordCloud.jsx +106 -0
- package/src/components/activity.jsx +35 -0
- package/src/components/callout.css +78 -0
- package/src/components/code-block.css +123 -0
- package/src/components/columns.css +11 -0
- package/src/components/poll.css +81 -0
- package/src/components/video-player.css +14 -0
- package/src/core/devPath.js +4 -0
- package/src/core/editDeck.js +230 -0
- package/src/core/icons.js +69 -0
- package/src/core/ink.js +221 -0
- package/src/core/labels.js +144 -0
- package/src/core/outline.js +33 -0
- package/src/core/parseSlides.js +152 -0
- package/src/core/source.js +137 -0
- package/src/core/urls.js +34 -0
- package/src/core/validateDeck.js +139 -0
- package/src/editor/api.js +67 -0
- package/src/editor/components/App.jsx +75 -0
- package/src/editor/components/ConflictBanner.jsx +10 -0
- package/src/editor/components/DeckSettings.jsx +108 -0
- package/src/editor/components/DesignApp.jsx +252 -0
- package/src/editor/components/Diagnostics.jsx +10 -0
- package/src/editor/components/ExtensionEditor.jsx +66 -0
- package/src/editor/components/ExtensionForms.jsx +134 -0
- package/src/editor/components/ExtensionsPanel.jsx +114 -0
- package/src/editor/components/Field.jsx +24 -0
- package/src/editor/components/Inspector.jsx +36 -0
- package/src/editor/components/LayoutPicker.jsx +23 -0
- package/src/editor/components/LayoutSelect.jsx +25 -0
- package/src/editor/components/LookThumbnail.jsx +8 -0
- package/src/editor/components/MetaFields.jsx +24 -0
- package/src/editor/components/Outline.jsx +28 -0
- package/src/editor/components/PalettePicker.jsx +32 -0
- package/src/editor/components/Preview.jsx +63 -0
- package/src/editor/components/PropsForm.jsx +46 -0
- package/src/editor/components/RawSlideEditor.jsx +9 -0
- package/src/editor/components/RegionEditor.jsx +15 -0
- package/src/editor/components/Resizer.jsx +18 -0
- package/src/editor/designLink.js +19 -0
- package/src/editor/editor.css +139 -0
- package/src/editor/extensions.js +86 -0
- package/src/editor/keyboard.js +21 -0
- package/src/editor/model.js +38 -0
- package/src/editor/sampleDeck.js +165 -0
- package/src/editor/saveQueue.js +47 -0
- package/src/editor/schema.js +36 -0
- package/src/editor/state.js +92 -0
- package/src/editor/useDeckDocument.js +67 -0
- package/src/extensions/appearance.js +79 -0
- package/src/extensions/contrast.js +57 -0
- package/src/extensions/discover.js +126 -0
- package/src/extensions/manifest.js +209 -0
- package/src/extensions/packs.js +268 -0
- package/src/extensions/tokens.js +25 -0
- package/src/home/Home.jsx +362 -0
- package/src/home/home.css +148 -0
- package/src/layouts/layoutApi.jsx +205 -0
- package/src/layouts/layoutProps.js +30 -0
- package/src/layouts/renderSlide.jsx +17 -0
- package/src/live/answer/answer.css +34 -0
- package/src/live/answer/answer.html +14 -0
- package/src/live/answer/answer.js +130 -0
- package/src/live/client.js +290 -0
- package/src/live/code.js +19 -0
- package/src/live/follow.js +17 -0
- package/src/live/pairing.js +34 -0
- package/src/live/presence.js +33 -0
- package/src/live/roomTag.js +38 -0
- package/src/live/rooms.js +249 -0
- package/src/live/server.js +82 -0
- package/src/live/tunnel.js +180 -0
- package/src/paths.js +9 -0
- package/src/runtime/ReaderView.jsx +193 -0
- package/src/runtime/SlideErrorBoundary.jsx +28 -0
- package/src/runtime/chrome.jsx +55 -0
- package/src/runtime/deck-stage.js +1533 -0
- package/src/runtime/design.html +12 -0
- package/src/runtime/design.jsx +5 -0
- package/src/runtime/editor.html +12 -0
- package/src/runtime/editor.jsx +5 -0
- package/src/runtime/editorBridge.js +58 -0
- package/src/runtime/fit.js +149 -0
- package/src/runtime/home.html +13 -0
- package/src/runtime/home.jsx +5 -0
- package/src/runtime/index.html +15 -0
- package/src/runtime/ink/InkLayer.jsx +17 -0
- package/src/runtime/ink/Toolbar.jsx +145 -0
- package/src/runtime/ink/attach.js +69 -0
- package/src/runtime/ink/bus.js +126 -0
- package/src/runtime/ink/controller.js +132 -0
- package/src/runtime/ink/room.js +101 -0
- package/src/runtime/ink/select.js +124 -0
- package/src/runtime/ink/server.js +75 -0
- package/src/runtime/ink/store.js +55 -0
- package/src/runtime/ink/toolbar.css +79 -0
- package/src/runtime/main.jsx +863 -0
- package/src/runtime/markedSetup.js +147 -0
- package/src/runtime/reader.css +59 -0
- package/src/runtime/registry.jsx +26 -0
- package/src/runtime/syncOrder.js +32 -0
- package/src/runtime/themeLoader.js +68 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// The design page, where themes, palettes and layouts are made and changed.
|
|
2
|
+
// Both editor pages open it in the same named tab, so it is never open twice.
|
|
3
|
+
export const DESIGN_URL = '/design.html'
|
|
4
|
+
export const DESIGN_TAB = 'mdeck-design'
|
|
5
|
+
|
|
6
|
+
// `copy` starts a copy of a built-in extension to change; `open` opens one of
|
|
7
|
+
// the deck's own. Both are `kind:id`.
|
|
8
|
+
export function designUrl({ copy = null, open = null } = {}) {
|
|
9
|
+
const query = new URLSearchParams()
|
|
10
|
+
if (copy) query.set('copy', copy)
|
|
11
|
+
if (open) query.set('open', open)
|
|
12
|
+
return query.size ? `${DESIGN_URL}?${query}` : DESIGN_URL
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Reads `kind:id` back; null when it is not one.
|
|
16
|
+
export function parseTarget(value) {
|
|
17
|
+
const match = /^(theme|palette|layout):([a-z][a-z0-9-]*)$/.exec(value ?? '')
|
|
18
|
+
return match ? { kind: match[1], id: match[2] } : null
|
|
19
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
:root { color-scheme: dark; }
|
|
2
|
+
html, body { margin: 0; height: 100%; background: #111; color: #e6e6e6; font: 13px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
3
|
+
#editor-root, #design-root { height: 100%; }
|
|
4
|
+
.muted { color: #8e8e8e; }
|
|
5
|
+
button, select, input, textarea { font: inherit; }
|
|
6
|
+
a { color: #8ab4f8; }
|
|
7
|
+
.editor { display: grid; grid-template-rows: 44px 1fr; height: 100vh; }
|
|
8
|
+
.editor-topbar { display: flex; align-items: center; gap: 12px; padding: 0 14px; border-bottom: 1px solid #222; background: #161616; }
|
|
9
|
+
.editor-topbar h1 { font-size: 14px; font-weight: 600; margin: 0; color: #eee; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
10
|
+
.editor-topbar .spacer { flex: 1; }
|
|
11
|
+
.editor-topbar .tabs button { padding: 10px 12px; }
|
|
12
|
+
fieldset:disabled input, fieldset:disabled select, fieldset:disabled textarea, fieldset:disabled .btn { opacity: 0.6; cursor: not-allowed; }
|
|
13
|
+
.editor-status { color: #a8a8a8; font-size: 12px; }
|
|
14
|
+
.editor-status.is-error, .editor-status.is-conflict { color: #f28b82; }
|
|
15
|
+
.editor-status.is-saving, .editor-status.is-unsaved { color: #e3c36d; }
|
|
16
|
+
.editor-main { display: grid; grid-template-columns: 240px minmax(0, 1fr) 400px; min-height: 0; }
|
|
17
|
+
.editor-panel { min-height: 0; overflow: hidden auto; border-right: 1px solid #222; position: relative; }
|
|
18
|
+
.resizer { position: sticky; top: 0; float: left; width: 6px; height: 100vh; margin-right: -6px; cursor: col-resize; z-index: 3; }
|
|
19
|
+
.resizer:hover { background: rgba(138, 180, 248, 0.25); }
|
|
20
|
+
.editor-panel:last-child { border-right: 0; }
|
|
21
|
+
.editor-panel-head { display: flex; align-items: center; gap: 6px; padding: 10px 12px; border-bottom: 1px solid #222; position: sticky; top: 0; background: #111; z-index: 1; }
|
|
22
|
+
.editor-panel-head span { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #a0a0a0; flex: 1; }
|
|
23
|
+
.btn { appearance: none; display: inline-flex; align-items: center; justify-content: center; gap: 4px; padding: 5px 10px; border-radius: 5px; border: 1px solid #3a3a3a; background: #222; color: #e0e0e0; cursor: pointer; line-height: 1.4; }
|
|
24
|
+
.btn:hover { color: #fff; border-color: #5a5a5a; }
|
|
25
|
+
.btn:disabled { opacity: 0.4; cursor: default; }
|
|
26
|
+
.btn.is-primary { background: #27436a; border-color: #3d6094; color: #e8f0ff; }
|
|
27
|
+
.btn.is-small { padding: 2px 7px; font-size: 12px; }
|
|
28
|
+
.btn.is-icon { padding: 3px 5px; }
|
|
29
|
+
.outline-list { list-style: none; margin: 0; padding: 6px; }
|
|
30
|
+
.outline-item { display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 8px; align-items: center; padding: 7px 8px; border-radius: 6px; cursor: pointer; color: #d6d6d6; }
|
|
31
|
+
.outline-item:hover { background: #1e1e1e; }
|
|
32
|
+
.outline-item.is-selected { background: #23384f; color: #ffffff; }
|
|
33
|
+
.outline-item .num { color: #8a8a8a; font-variant-numeric: tabular-nums; }
|
|
34
|
+
.outline-item > span { min-width: 0; }
|
|
35
|
+
.outline-item .title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
36
|
+
.outline-item .layout { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: #9a9a9a; }
|
|
37
|
+
.outline-item .dot { width: 8px; height: 8px; border-radius: 50%; background: #f28b82; }
|
|
38
|
+
.outline-item.has-thumb { grid-template-columns: minmax(0, 1fr) !important; grid-template-rows: auto auto; gap: 6px; }
|
|
39
|
+
.thumb { aspect-ratio: 16 / 9; overflow: hidden; border-radius: 4px; border: 1px solid #2a2a2a; background: #000; pointer-events: none; }
|
|
40
|
+
.thumb iframe { width: 100%; height: 100%; border: 0; display: block; }
|
|
41
|
+
.outline-tools { display: flex; gap: 4px; padding: 6px 12px; border-top: 1px solid #222; position: sticky; bottom: 0; background: #111; }
|
|
42
|
+
.preview { display: flex; flex-direction: column; min-height: 0; background: #0b0b0b; }
|
|
43
|
+
.preview-frame { flex: 1; display: flex; align-items: center; justify-content: center; padding: 16px; min-height: 0; }
|
|
44
|
+
.preview-frame iframe { border: 0; background: #000; box-shadow: 0 8px 40px rgba(0,0,0,0.6); width: 100%; max-height: 100%; }
|
|
45
|
+
.preview-tools { display: flex; align-items: center; gap: 6px; padding: 8px 14px; border-top: 1px solid #222; color: #b0b0b0; }
|
|
46
|
+
.preview-tools .spacer { flex: 1; }
|
|
47
|
+
.tabs { display: flex; gap: 2px; padding: 8px 12px 0; border-bottom: 1px solid #222; position: sticky; top: 0; background: #111; z-index: 1; }
|
|
48
|
+
.tabs button { appearance: none; background: none; border: 0; border-bottom: 2px solid transparent; color: #b4b4b4; padding: 6px 10px; cursor: pointer; }
|
|
49
|
+
.tabs button.is-active { color: #fff; border-bottom-color: #8ab4f8; }
|
|
50
|
+
.form { padding: 12px; display: flex; flex-direction: column; gap: 14px; }
|
|
51
|
+
.field { display: flex; flex-direction: column; gap: 4px; }
|
|
52
|
+
.field label { color: #b8b8b8; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; display: flex; gap: 6px; align-items: baseline; }
|
|
53
|
+
.field label .req { color: #e3c36d; text-transform: none; letter-spacing: 0; }
|
|
54
|
+
.field label .hint { color: #8e8e8e; text-transform: none; letter-spacing: 0; font-size: 12px; }
|
|
55
|
+
.field .row { display: flex; gap: 6px; align-items: center; }
|
|
56
|
+
.field .row > * { flex: 1; min-width: 0; }
|
|
57
|
+
.field .row > .btn { flex: 0 0 auto; }
|
|
58
|
+
.field .error { color: #f28b82; font-size: 12px; }
|
|
59
|
+
.field small { color: #8e8e8e; }
|
|
60
|
+
input[type="text"], input[type="number"], select, textarea { width: 100%; box-sizing: border-box; background: #1b1b1b; color: #f2f2f2; border: 1px solid #3a3a3a; border-radius: 5px; padding: 6px 8px; }
|
|
61
|
+
input[type="text"]:focus, input[type="number"]:focus, select:focus, textarea:focus { outline: none; border-color: #4a6a8a; }
|
|
62
|
+
input[type="color"] { width: 32px; height: 28px; padding: 2px; border: 1px solid #3a3a3a; border-radius: 5px; background: #1b1b1b; cursor: pointer; }
|
|
63
|
+
textarea { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 13.5px; line-height: 1.55; resize: vertical; min-height: 64px; }
|
|
64
|
+
textarea.is-tall { min-height: calc(100vh - 220px); }
|
|
65
|
+
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
|
66
|
+
.section-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: #c0c0c0; margin: 4px 0 -6px; }
|
|
67
|
+
details.more summary { cursor: pointer; color: #b4b4b4; }
|
|
68
|
+
details.more > .form { padding: 10px 0 0; }
|
|
69
|
+
.diagnostics { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
70
|
+
.diagnostics li { font-size: 12px; padding: 6px 8px; border-radius: 5px; background: #1a1414; color: #f28b82; }
|
|
71
|
+
.diagnostics li.is-warning { background: #1a1810; color: #e3c36d; }
|
|
72
|
+
.diagnostics li code { opacity: 0.7; margin-right: 6px; }
|
|
73
|
+
.banner { display: flex; gap: 10px; align-items: center; padding: 8px 14px; background: #3a2a1a; color: #f3d9b1; border-bottom: 1px solid #5a3a1a; }
|
|
74
|
+
.banner .spacer { flex: 1; }
|
|
75
|
+
.modal-scrim { position: fixed; inset: 0; background: rgba(0,0,0,0.75); display: flex; align-items: center; justify-content: center; z-index: 100; }
|
|
76
|
+
.modal { background: #161616; border: 1px solid #2a2a2a; border-radius: 10px; padding: 18px; width: min(90vw, 560px); max-height: 80vh; overflow: auto; }
|
|
77
|
+
.modal h2 { margin: 0 0 12px; font-size: 14px; color: #eee; }
|
|
78
|
+
.picker { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
|
79
|
+
.picker button { width: 100%; text-align: left; display: flex; flex-direction: column; gap: 2px; }
|
|
80
|
+
.picker button small { color: #b0b0b0; }
|
|
81
|
+
.swatches { display: inline-flex; gap: 2px; }
|
|
82
|
+
.swatches i { width: 10px; height: 10px; border-radius: 2px; border: 1px solid rgba(255,255,255,0.08); }
|
|
83
|
+
.empty { padding: 40px 20px; color: #a0a0a0; text-align: center; }
|
|
84
|
+
.loading { display: flex; align-items: center; justify-content: center; height: 100vh; color: #b0b0b0; }
|
|
85
|
+
/* The deck's palette: a list showing every palette's colours, and light or dark. */
|
|
86
|
+
.palette-picker { position: relative; }
|
|
87
|
+
.palette-current, .palette-list button { appearance: none; display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box; padding: 6px 8px; border: 1px solid #3a3a3a; border-radius: 5px; background: #1b1b1b; color: #f2f2f2; cursor: pointer; text-align: left; }
|
|
88
|
+
.palette-current:hover, .palette-current[aria-expanded="true"] { border-color: #4a6a8a; }
|
|
89
|
+
.palette-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
90
|
+
.palette-default { color: #8e8e8e; }
|
|
91
|
+
.palette-list { position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 10; max-height: 360px; overflow: auto; padding: 4px; border: 1px solid #3a3a3a; border-radius: 6px; background: #1b1b1b; box-shadow: 0 8px 28px rgba(0, 0, 0, 0.5); }
|
|
92
|
+
.palette-list button { border-color: transparent; background: none; color: #c8c8c8; }
|
|
93
|
+
.palette-list button:hover { background: #262626; color: #fff; }
|
|
94
|
+
.palette-list button.is-active { background: #27436a; color: #fff; }
|
|
95
|
+
.appearance-toggle { display: flex; gap: 6px; }
|
|
96
|
+
.appearance-toggle .btn { flex: 1; }
|
|
97
|
+
.appearance-toggle .btn.is-active { background: #27436a; border-color: #3d6094; color: #fff; }
|
|
98
|
+
.note { margin: 0; padding: 8px 10px; border-left: 3px solid #4a6a8a; background: #1a2230; color: #c8d4e4; font-size: 12px; border-radius: 0 4px 4px 0; }
|
|
99
|
+
.extensions-tabs { padding: 0 10px 0 4px; align-items: stretch; }
|
|
100
|
+
.extensions-tabs .tabs { flex: 1; padding: 0; border: 0; position: static; background: none; }
|
|
101
|
+
.extensions-tabs .tabs button { padding: 12px 8px 10px; }
|
|
102
|
+
.extensions-tabs > .btn { align-self: center; }
|
|
103
|
+
.preview-bar { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-bottom: 1px solid #222; color: #e0e0e0; }
|
|
104
|
+
.preview-bar .spacer { flex: 1; }
|
|
105
|
+
.preview-bar .muted { color: #8e8e8e; }
|
|
106
|
+
.preview-choice { display: inline-flex; align-items: center; gap: 6px; color: #a8a8a8; font-size: 12px; }
|
|
107
|
+
.preview-choice select { max-width: 160px; }
|
|
108
|
+
.appearance-toggle.is-compact { gap: 2px; }
|
|
109
|
+
.appearance-toggle.is-compact .btn { flex: 0 0 auto; }
|
|
110
|
+
.segmented { display: inline-flex; border: 1px solid #3a3a3a; border-radius: 5px; overflow: hidden; }
|
|
111
|
+
.segmented button { appearance: none; border: 0; background: #1b1b1b; color: #c8c8c8; padding: 3px 10px; font-size: 12px; cursor: pointer; }
|
|
112
|
+
.segmented button + button { border-left: 1px solid #3a3a3a; }
|
|
113
|
+
.segmented button.is-active { background: #27436a; color: #fff; }
|
|
114
|
+
/* The deck's theme, chosen from pictures of its first slide. */
|
|
115
|
+
.theme-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 8px; }
|
|
116
|
+
.theme-choice { appearance: none; display: flex; flex-direction: column; gap: 4px; padding: 4px; border: 1px solid #2a2a2a; border-radius: 6px; background: #161616; color: #d6d6d6; cursor: pointer; text-align: left; font-size: 12px; }
|
|
117
|
+
.theme-choice:hover { border-color: #4a6a8a; }
|
|
118
|
+
.theme-choice.is-active { border-color: #60a5fa; background: #1d2b3c; color: #fff; }
|
|
119
|
+
.theme-choice-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 2px; }
|
|
120
|
+
/* The design page: the deck's own extensions first, then built-in ones. */
|
|
121
|
+
.panel-section-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 14px 0; }
|
|
122
|
+
.panel-section-head .section-title { margin: 0; }
|
|
123
|
+
.online-pack .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
124
|
+
.online-pack { cursor: default; }
|
|
125
|
+
.online-meta { font-size: 11px; color: #9a9a9a; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block; }
|
|
126
|
+
.online-error { color: #f28b82; margin: 6px 0 0; }
|
|
127
|
+
.hint-text { margin: 0; color: #9a9a9a; font-size: 12px; }
|
|
128
|
+
.panel-hint { margin: 6px 14px 4px; color: #9a9a9a; font-size: 12px; }
|
|
129
|
+
.design-usage { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-bottom: 1px solid #222; background: #161b22; font-size: 12px; }
|
|
130
|
+
.design-usage > span { flex: 1; display: inline-flex; align-items: center; gap: 6px; }
|
|
131
|
+
.palette-checks { display: grid; gap: 2px; max-height: 220px; overflow: auto; padding: 4px; border: 1px solid #2a2a2a; border-radius: 5px; }
|
|
132
|
+
.field .palette-check { display: flex; align-items: center; gap: 8px; padding: 3px 4px; border-radius: 4px; cursor: pointer; text-transform: none; letter-spacing: normal; font-size: 13px; color: #d6d6d6; }
|
|
133
|
+
.field .palette-check:hover { background: #1e1e1e; }
|
|
134
|
+
.builtin-title { margin: 0; font-size: 16px; color: #f0f0f0; }
|
|
135
|
+
.builtin-title .muted { font-size: 12px; font-weight: 400; margin-left: 6px; }
|
|
136
|
+
.builtin-description { margin: 0; color: #c0c0c0; }
|
|
137
|
+
.builtin-source summary, .advanced summary { cursor: pointer; color: #a8a8a8; }
|
|
138
|
+
.builtin-source pre { margin: 8px 0 0; padding: 10px; max-height: 360px; overflow: auto; background: #161616; border: 1px solid #2a2a2a; border-radius: 5px; font-size: 11px; white-space: pre-wrap; }
|
|
139
|
+
.advanced[open] summary { margin-bottom: 8px; }
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Extension editing model: the files of an extension folder are the source of
|
|
2
|
+
// truth; forms read a model out of extension.toml and write it back as TOML.
|
|
3
|
+
import { parse, stringify } from 'smol-toml'
|
|
4
|
+
|
|
5
|
+
export const MANIFEST = 'extension.toml'
|
|
6
|
+
export const ID_RE = /^[a-z][a-z0-9-]*$/
|
|
7
|
+
export { COLOR_ROLES as CORE_TOKENS } from '../extensions/tokens.js'
|
|
8
|
+
// Readable colours a new palette starts from.
|
|
9
|
+
const STARTER_COLORS = {
|
|
10
|
+
light: { '--bg': '#ffffff', '--surface': '#f2f2f2', '--ink': '#111111', '--ink-soft': '#333333', '--muted': '#666666', '--rule': '#dadada', '--accent': '#0b6bcb', '--accent-2': '#c2410c', '--on-accent': '#ffffff' },
|
|
11
|
+
dark: { '--bg': '#111111', '--surface': '#1e1e1e', '--ink': '#f2f2f2', '--ink-soft': '#cfcfcf', '--muted': '#8f8f8f', '--rule': '#2e2e2e', '--accent': '#6aaeff', '--accent-2': '#fb923c', '--on-accent': '#111111' },
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const entries = table => Object.entries(table ?? {}).map(([name, value]) => ({ name, ...(value && typeof value === 'object' && !Array.isArray(value) ? value : { value }) }))
|
|
15
|
+
const table = (list, pick) => Object.fromEntries(list.filter(item => item.name).map(item => [item.name, pick(item)]))
|
|
16
|
+
const clean = object => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined && value !== '' && value !== null && !(Array.isArray(value) && !value.length)))
|
|
17
|
+
|
|
18
|
+
export function parseManifest(text) {
|
|
19
|
+
try { return { raw: parse(text) } } catch (error) { return { error: error.message.replace(/^Invalid TOML document: /, ''), line: error.line, column: error.column } }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Raw TOML object → editable model. Unknown fields are kept in `extra`.
|
|
23
|
+
export function toModel(raw) {
|
|
24
|
+
// `starter`, `source` and `file` come from registry listings, not manifests.
|
|
25
|
+
const { schema, kind, id, title, description, theme, light, dark, palette, palettes, appearance, fonts, files, tokens, params, frame, regions, properties, starter, source, file, ...extra } = raw
|
|
26
|
+
const model = { kind, id: id ?? '', title: title ?? '', description: description ?? '', extra, files: files ?? {} }
|
|
27
|
+
// A palette: its light and dark colours, and the theme it belongs to, if any.
|
|
28
|
+
if (kind === 'palette') return { ...model, theme: theme ?? '', light: entries(light), dark: entries(dark) }
|
|
29
|
+
if (kind === 'theme') return { ...model, palette: palette ?? '', palettes: palettes ?? [], appearance: appearance ?? 'light', fonts: fonts ?? [], tokens: entries(tokens), params: entries(params) }
|
|
30
|
+
if (kind === 'layout') return { ...model, frame: frame ?? 'standard', regions: entries(regions), properties: entries(properties).map(property => ({ ...property, enum: property.enum ?? [], items: property.items ?? null })) }
|
|
31
|
+
return model
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function toToml(model) {
|
|
35
|
+
const base = { schema: 1, kind: model.kind, id: model.id, title: model.title, ...clean({ description: model.description }) }
|
|
36
|
+
const tokens = table(model.tokens ?? [], token => token.value ?? '')
|
|
37
|
+
let raw
|
|
38
|
+
if (model.kind === 'palette') raw = { ...base, ...clean({ theme: model.theme }), ...model.extra, light: table(model.light ?? [], token => token.value ?? ''), dark: table(model.dark ?? [], token => token.value ?? '') }
|
|
39
|
+
else if (model.kind === 'theme') raw = {
|
|
40
|
+
...base, palette: model.palette, ...clean({ palettes: model.palettes }), ...(model.appearance === 'dark' ? { appearance: 'dark' } : {}), fonts: model.fonts ?? [], ...model.extra,
|
|
41
|
+
files: { styles: 'styles.css', ...(model.files ?? {}) }, tokens,
|
|
42
|
+
params: table(model.params ?? [], param => clean({ token: param.token, title: param.title, description: param.description })),
|
|
43
|
+
}
|
|
44
|
+
else if (model.kind === 'layout') raw = {
|
|
45
|
+
...base, ...(model.frame && model.frame !== 'standard' ? { frame: model.frame } : {}), ...model.extra, ...(Object.keys(model.files ?? {}).length ? { files: model.files } : {}),
|
|
46
|
+
regions: table(model.regions ?? [], region => clean({ description: region.description, required: region.required || undefined })),
|
|
47
|
+
properties: table(model.properties ?? [], property => propertySchema(property)),
|
|
48
|
+
}
|
|
49
|
+
else raw = { ...base, ...model.extra }
|
|
50
|
+
if (raw.params && !Object.keys(raw.params).length) delete raw.params
|
|
51
|
+
if (raw.properties && !Object.keys(raw.properties).length) delete raw.properties
|
|
52
|
+
return stringify(raw) + '\n'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function propertySchema(property) {
|
|
56
|
+
const schema = clean({ type: property.type ?? 'string', title: property.title, description: property.description, required: property.required || undefined,
|
|
57
|
+
enum: property.enum?.length ? property.enum : undefined, minimum: property.minimum, maximum: property.maximum, minItems: property.minItems, maxItems: property.maxItems })
|
|
58
|
+
if (property.default !== undefined && property.default !== null) schema.default = property.default
|
|
59
|
+
if (property.items && property.type === 'array') schema.items = clean({ type: property.items.type ?? 'string', minimum: property.items.minimum, maximum: property.items.maximum, enum: property.items.enum?.length ? property.items.enum : undefined })
|
|
60
|
+
return schema
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Manifest as the runtime expects it, for live previews of unsaved work.
|
|
64
|
+
export function toRuntimeManifest(model) {
|
|
65
|
+
const tokens = table(model.tokens ?? [], token => token.value ?? '')
|
|
66
|
+
const shared = { id: model.id, title: model.title, description: model.description ?? '' }
|
|
67
|
+
if (model.kind === 'palette') return { ...shared, ...(model.theme ? { theme: model.theme } : {}), light: table(model.light ?? [], token => token.value ?? ''), dark: table(model.dark ?? [], token => token.value ?? '') }
|
|
68
|
+
if (model.kind === 'theme') return {
|
|
69
|
+
...shared, palette: model.palette, ...(model.palettes?.length ? { palettes: model.palettes } : {}), appearance: model.appearance ?? 'light', fonts: model.fonts ?? [], tokens,
|
|
70
|
+
params: table(model.params ?? [], param => ({ token: param.token, default: tokens[param.token] ?? '', ...(param.title ? { title: param.title } : {}) })),
|
|
71
|
+
}
|
|
72
|
+
return shared
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Starting points for new extensions. `from` is a registry manifest to copy;
|
|
76
|
+
// a theme is always a copy, a palette may start from readable plain colours.
|
|
77
|
+
export function starterFiles(kind, id, title, from = null, fromFiles = {}) {
|
|
78
|
+
if (kind === 'palette') {
|
|
79
|
+
const light = from?.light ? entries(from.light) : entries(STARTER_COLORS.light)
|
|
80
|
+
const dark = from?.dark ? entries(from.dark) : entries(STARTER_COLORS.dark)
|
|
81
|
+
return { [MANIFEST]: toToml({ kind, id, title, description: '', theme: '', light, dark, extra: {} }) }
|
|
82
|
+
}
|
|
83
|
+
if (kind !== 'theme' || !from) throw new Error('A new theme starts as a copy of another one')
|
|
84
|
+
const params = from.params && Object.fromEntries(Object.entries(from.params).map(([name, param]) => [name, { token: param.token, title: param.title, description: param.description }]))
|
|
85
|
+
return { [MANIFEST]: toToml({ ...toModel({ ...from, kind, params }), id, title, files: {} }), 'styles.css': fromFiles['styles.css'] ?? '' }
|
|
86
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const EDITABLE = 'input, textarea, select, [contenteditable]'
|
|
2
|
+
|
|
3
|
+
// Undo and redo work everywhere (textarea values derive from the source, so
|
|
4
|
+
// native undo would fight the model). Navigation and structure shortcuts stay
|
|
5
|
+
// out of text fields.
|
|
6
|
+
export function installShortcuts(handlers, target = window) {
|
|
7
|
+
const onKey = event => {
|
|
8
|
+
const mod = event.metaKey || event.ctrlKey
|
|
9
|
+
const key = event.key.toLowerCase()
|
|
10
|
+
if (mod && key === 'z') { event.preventDefault(); return event.shiftKey ? handlers.redo?.() : handlers.undo?.() }
|
|
11
|
+
if (mod && key === 'y') { event.preventDefault(); return handlers.redo?.() }
|
|
12
|
+
if (event.target?.closest?.(EDITABLE)) return
|
|
13
|
+
if (mod && key === 'enter') { event.preventDefault(); return handlers.add?.() }
|
|
14
|
+
if (mod && key === 'backspace') { event.preventDefault(); return handlers.remove?.() }
|
|
15
|
+
if (mod) return
|
|
16
|
+
if (key === 'arrowup' || key === 'arrowleft' || key === 'pageup') { event.preventDefault(); return handlers.prev?.() }
|
|
17
|
+
if (key === 'arrowdown' || key === 'arrowright' || key === 'pagedown') { event.preventDefault(); return handlers.next?.() }
|
|
18
|
+
}
|
|
19
|
+
target.addEventListener('keydown', onKey)
|
|
20
|
+
return () => target.removeEventListener('keydown', onKey)
|
|
21
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Read-only helpers that derive editor views from the parsed deck and the
|
|
2
|
+
// extension manifests.
|
|
3
|
+
import { marked } from 'marked'
|
|
4
|
+
export { slideTitle } from '../core/outline.js'
|
|
5
|
+
|
|
6
|
+
// Body first, then the manifest's regions in order, then explicit regions the
|
|
7
|
+
// manifest does not know about.
|
|
8
|
+
export function regionsFor(slide, manifest) {
|
|
9
|
+
const declared = manifest?.regions ?? { body: {} }
|
|
10
|
+
const names = ['body', ...Object.keys(declared).filter(name => name !== 'body')]
|
|
11
|
+
const known = new Set(names)
|
|
12
|
+
const list = names.map(name => ({
|
|
13
|
+
name, description: declared[name]?.description ?? '', required: Boolean(declared[name]?.required),
|
|
14
|
+
present: Boolean(slide.regions[name]), content: slide.regions[name]?.content ?? '', unknown: false,
|
|
15
|
+
}))
|
|
16
|
+
for (const name of Object.keys(slide.regions)) {
|
|
17
|
+
if (!known.has(name)) list.push({ name, description: '', required: false, present: true, content: slide.regions[name].content, unknown: true })
|
|
18
|
+
}
|
|
19
|
+
return list
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Where an authored property value lives: under props, as a legacy top-level
|
|
23
|
+
// key (built-in layouts accept image:, alt:, fit:, position:, overlay:), or nowhere.
|
|
24
|
+
export function propSource(authoredMeta = {}, key) {
|
|
25
|
+
if (authoredMeta.props && Object.hasOwn(authoredMeta.props, key)) return 'props'
|
|
26
|
+
if (Object.hasOwn(authoredMeta, key) && key !== 'props') return 'legacy'
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function slideDiagnostics(diagnostics, slide) {
|
|
31
|
+
return diagnostics.filter(d => d.offset >= slide.source.start && d.offset < Math.max(slide.source.end, slide.source.start + 1))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function deckDiagnostics(diagnostics, deck) {
|
|
35
|
+
return diagnostics.filter(d => !deck.slides.some(slide => d.offset >= slide.source.start && d.offset < Math.max(slide.source.end, slide.source.start + 1)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { marked }
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The sample deck: every built-in layout and kind of content, once, in
|
|
2
|
+
// ordinary amounts. The design page previews themes and palettes on it, and
|
|
3
|
+
// `npm run test:themes` checks every theme against it.
|
|
4
|
+
|
|
5
|
+
export const SAMPLE_PICTURE = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300"><rect width="400" height="300" fill="#6b7f99"/><circle cx="140" cy="150" r="80" fill="#e0c068"/><rect x="220" y="80" width="120" height="140" fill="#2d3e50"/></svg>'
|
|
6
|
+
|
|
7
|
+
// `picture` is where the slides find the picture: a file beside the deck, or
|
|
8
|
+
// a data URL where there is no such file.
|
|
9
|
+
export function sampleDeck(picture = './picture.svg') {
|
|
10
|
+
return `---
|
|
11
|
+
meta:
|
|
12
|
+
title: "Sample talk"
|
|
13
|
+
author: "A. Author"
|
|
14
|
+
organization: "Institute"
|
|
15
|
+
date: "2026-10-06"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
layout: title
|
|
20
|
+
---
|
|
21
|
+
# A talk about *trying* themes.
|
|
22
|
+
## Every layout and every kind of content, once.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
layout: title
|
|
26
|
+
image: ./picture.svg
|
|
27
|
+
alt: "Shapes"
|
|
28
|
+
---
|
|
29
|
+
# A title with a picture.
|
|
30
|
+
## And a subtitle beside it.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
layout: chapter
|
|
34
|
+
number: 2
|
|
35
|
+
part: Methods
|
|
36
|
+
---
|
|
37
|
+
# How it was *measured*
|
|
38
|
+
|
|
39
|
+
One sentence about what this chapter covers.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
layout: chapter
|
|
43
|
+
number: 3
|
|
44
|
+
image: ./picture.svg
|
|
45
|
+
alt: "Shapes"
|
|
46
|
+
---
|
|
47
|
+
# A chapter with a picture
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
# Lists, emphasis and code
|
|
51
|
+
|
|
52
|
+
- A point with *emphasis* and **strong** words
|
|
53
|
+
- A second point with \`inline code\`
|
|
54
|
+
- A nested point
|
|
55
|
+
- Another nested point
|
|
56
|
+
|
|
57
|
+
\`\`\`js
|
|
58
|
+
const total = values.reduce((sum, value) => sum + value, 0)
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
# Numbers in a table
|
|
63
|
+
|
|
64
|
+
| Group | Mean | Spread |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| Control | 4.2 | 0.8 |
|
|
67
|
+
| Treatment | 5.1 | 0.6 |
|
|
68
|
+
| Follow-up | 5.0 | 0.7 |
|
|
69
|
+
|
|
70
|
+
The difference is $\\Delta = 0.9$, with a footnote.[^1]
|
|
71
|
+
|
|
72
|
+
[^1]: A source for the claim.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
# Callouts
|
|
76
|
+
|
|
77
|
+
::: tip
|
|
78
|
+
A tip in one sentence.
|
|
79
|
+
:::
|
|
80
|
+
|
|
81
|
+
::: warning
|
|
82
|
+
A warning in one sentence.
|
|
83
|
+
:::
|
|
84
|
+
|
|
85
|
+
::: note Your own title
|
|
86
|
+
A note with a title of its own.
|
|
87
|
+
:::
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
# Blocks for lectures
|
|
91
|
+
|
|
92
|
+
::: definition Definition 1 (Group)
|
|
93
|
+
A set with an associative operation, a neutral element and inverses.
|
|
94
|
+
:::
|
|
95
|
+
|
|
96
|
+
::: theorem Theorem 2 (Lagrange)
|
|
97
|
+
The order of a subgroup divides the order of the group.
|
|
98
|
+
:::
|
|
99
|
+
|
|
100
|
+
::: proof
|
|
101
|
+
The cosets partition the group into parts of equal size.
|
|
102
|
+
:::
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
# Two columns
|
|
106
|
+
|
|
107
|
+
:::columns
|
|
108
|
+
Some text on the left, with a formula:
|
|
109
|
+
|
|
110
|
+
$$
|
|
111
|
+
\\int_0^1 x^2 \\, dx = \\frac{1}{3}
|
|
112
|
+
$$
|
|
113
|
+
|
|
114
|
+
+++
|
|
115
|
+
|
|
116
|
+
1. First step
|
|
117
|
+
2. Second step
|
|
118
|
+
3. Third step
|
|
119
|
+
:::
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
layout: focus
|
|
123
|
+
eyebrow: Key point
|
|
124
|
+
attribution: "Someone wise"
|
|
125
|
+
---
|
|
126
|
+
# A statement that takes a line or two on the slide.
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
layout: image-text
|
|
130
|
+
image: ./picture.svg
|
|
131
|
+
alt: "Shapes"
|
|
132
|
+
---
|
|
133
|
+
# A picture beside words
|
|
134
|
+
|
|
135
|
+
Two sentences of text beside the picture. They explain what it shows.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
layout: split
|
|
139
|
+
---
|
|
140
|
+
# A split slide
|
|
141
|
+
:::slot left
|
|
142
|
+
\`\`\`python
|
|
143
|
+
def mean(values):
|
|
144
|
+
return sum(values) / len(values)
|
|
145
|
+
\`\`\`
|
|
146
|
+
:::
|
|
147
|
+
:::slot right
|
|
148
|
+
- What the code does
|
|
149
|
+
- Why it matters
|
|
150
|
+
:::
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
layout: full-bleed-image
|
|
154
|
+
image: ./picture.svg
|
|
155
|
+
alt: "Shapes"
|
|
156
|
+
overlay: true
|
|
157
|
+
---
|
|
158
|
+
# A picture that fills the slide.
|
|
159
|
+
`.replaceAll('./picture.svg', picture)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The picture as a data URL, for a sample deck that has no folder of its own.
|
|
163
|
+
export function sampleDataUrl() {
|
|
164
|
+
return `data:image/svg+xml,${encodeURIComponent(SAMPLE_PICTURE)}`
|
|
165
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Debounced, serialized autosave. The queue owns the base hash: every
|
|
2
|
+
// successful save advances it, a conflict pauses saving until the UI decides.
|
|
3
|
+
export function createSaveQueue({ save, delay = 400, setTimeout = globalThis.setTimeout.bind(globalThis), clearTimeout = globalThis.clearTimeout.bind(globalThis), onSaving = () => {}, onSaved = () => {}, onConflict = () => {}, onError = () => {} }) {
|
|
4
|
+
let base = null
|
|
5
|
+
let latest = null
|
|
6
|
+
let timer = null
|
|
7
|
+
let inflight = null
|
|
8
|
+
let paused = false
|
|
9
|
+
|
|
10
|
+
function push(source) {
|
|
11
|
+
latest = source
|
|
12
|
+
if (paused) return
|
|
13
|
+
clearTimeout(timer)
|
|
14
|
+
timer = setTimeout(flush, delay)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function flush() {
|
|
18
|
+
clearTimeout(timer)
|
|
19
|
+
timer = null
|
|
20
|
+
if (inflight) return inflight
|
|
21
|
+
if (latest == null || paused || base == null) return Promise.resolve()
|
|
22
|
+
const source = latest
|
|
23
|
+
latest = null
|
|
24
|
+
onSaving()
|
|
25
|
+
inflight = Promise.resolve()
|
|
26
|
+
.then(() => save(source, base))
|
|
27
|
+
.then(result => { base = result.hash; onSaved({ hash: result.hash, source }) })
|
|
28
|
+
.catch(error => {
|
|
29
|
+
if (error?.conflict) { paused = true; latest = latest ?? source; onConflict(error) }
|
|
30
|
+
else { latest = latest ?? source; onError(error) }
|
|
31
|
+
})
|
|
32
|
+
.finally(() => { inflight = null; if (latest != null && !paused) push(latest) })
|
|
33
|
+
return inflight
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
push,
|
|
38
|
+
flush,
|
|
39
|
+
setBase(hash) { base = hash },
|
|
40
|
+
base: () => base,
|
|
41
|
+
// After a conflict: overwrite keeps the local text and saves it over the disk version.
|
|
42
|
+
overwrite(hash) { base = hash; paused = false; return flush() },
|
|
43
|
+
// Reload drops the local text in favour of the disk version.
|
|
44
|
+
reload(hash) { base = hash; paused = false; latest = null },
|
|
45
|
+
pending: () => Boolean(latest != null || inflight || timer),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Maps layout property schemas to form controls and back.
|
|
2
|
+
import yaml from 'js-yaml'
|
|
3
|
+
|
|
4
|
+
export function fieldKind(schema = {}) {
|
|
5
|
+
if (Array.isArray(schema.enum) && schema.enum.length) return 'select'
|
|
6
|
+
if (schema.type === 'boolean') return 'checkbox'
|
|
7
|
+
if (schema.type === 'number' || schema.type === 'integer') return 'number'
|
|
8
|
+
if (schema.type === 'array' || schema.type === 'object') return 'yaml'
|
|
9
|
+
return 'text'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Returns { value } or { error } for a control's raw input.
|
|
13
|
+
export function parseFieldValue(kind, raw, schema = {}) {
|
|
14
|
+
if (kind === 'checkbox') return { value: Boolean(raw) }
|
|
15
|
+
if (kind === 'number') {
|
|
16
|
+
if (String(raw).trim() === '') return { value: undefined }
|
|
17
|
+
const value = Number(raw)
|
|
18
|
+
if (!Number.isFinite(value)) return { error: 'Enter a number' }
|
|
19
|
+
if (schema.type === 'integer' && !Number.isInteger(value)) return { error: 'Enter a whole number' }
|
|
20
|
+
return { value }
|
|
21
|
+
}
|
|
22
|
+
if (kind === 'yaml') {
|
|
23
|
+
if (String(raw).trim() === '') return { value: undefined }
|
|
24
|
+
try {
|
|
25
|
+
const value = yaml.load(raw)
|
|
26
|
+
return { value }
|
|
27
|
+
} catch (error) { return { error: error.reason ?? error.message } }
|
|
28
|
+
}
|
|
29
|
+
return { value: String(raw) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatFieldValue(kind, value) {
|
|
33
|
+
if (value === undefined || value === null) return ''
|
|
34
|
+
if (kind === 'yaml') return yaml.dump(value, { flowLevel: Array.isArray(value) ? 0 : 1, lineWidth: -1, noRefs: true }).replace(/\n$/, '')
|
|
35
|
+
return String(value)
|
|
36
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The editor's single source of truth is the deck source string. Every change
|
|
2
|
+
// goes through `edit`, which applies a pure helper from src/core/editDeck.js,
|
|
3
|
+
// reparses, revalidates and records undo history.
|
|
4
|
+
import { parseSlides } from '../core/parseSlides.js'
|
|
5
|
+
import { validateDeck } from '../core/validateDeck.js'
|
|
6
|
+
|
|
7
|
+
const HISTORY_LIMIT = 200
|
|
8
|
+
const COALESCE_MS = 1000
|
|
9
|
+
|
|
10
|
+
function byId(list = []) { return Object.fromEntries(list.map(item => [item.id, item])) }
|
|
11
|
+
|
|
12
|
+
export function derive(source, manifests) {
|
|
13
|
+
const deck = parseSlides(source)
|
|
14
|
+
const diagnostics = validateDeck(deck, { layouts: manifests.layouts, themes: manifests.themes, palettes: manifests.palettes })
|
|
15
|
+
return { deck, diagnostics }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const initialState = {
|
|
19
|
+
loaded: false, designOnly: false, path: '', name: '', source: '', hash: null, savedSource: '',
|
|
20
|
+
deck: null, diagnostics: [], registry: null, manifests: { layouts: {}, themes: {}, palettes: {} }, warnings: [],
|
|
21
|
+
selectedIndex: 0, tab: 'slide', inspectorMode: 'form',
|
|
22
|
+
history: { past: [], future: [] },
|
|
23
|
+
status: 'saved', conflict: null, error: null, preview: { error: null, slideCount: 0 },
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const clamp = (index, deck) => Math.max(0, Math.min(index, deck.slides.length - 1))
|
|
27
|
+
|
|
28
|
+
export function selectedSlide(state) { return state.deck?.slides[state.selectedIndex] ?? null }
|
|
29
|
+
|
|
30
|
+
export function reduce(state, action) {
|
|
31
|
+
switch (action.type) {
|
|
32
|
+
case 'load': {
|
|
33
|
+
const manifests = { layouts: byId(action.registry?.layouts), themes: byId(action.registry?.themes), palettes: byId(action.registry?.palettes) }
|
|
34
|
+
const { deck, diagnostics } = derive(action.source, manifests)
|
|
35
|
+
return { ...state, loaded: true, designOnly: Boolean(action.designOnly), path: action.path, name: action.name, source: action.source, savedSource: action.source, hash: action.hash, deck, diagnostics,
|
|
36
|
+
registry: action.registry ?? null, manifests, warnings: action.warnings ?? [], selectedIndex: clamp(state.selectedIndex, deck), history: { past: [], future: [] }, status: 'saved', conflict: null, error: null }
|
|
37
|
+
}
|
|
38
|
+
case 'edit': {
|
|
39
|
+
const result = action.apply(state.deck)
|
|
40
|
+
const source = typeof result === 'string' ? result : result.source
|
|
41
|
+
const index = typeof result === 'string' ? action.selectIndex ?? state.selectedIndex : result.index
|
|
42
|
+
if (source === state.source) return index === state.selectedIndex ? state : { ...state, selectedIndex: clamp(index, state.deck) }
|
|
43
|
+
const now = action.now ?? Date.now()
|
|
44
|
+
const last = state.history.past.at(-1)
|
|
45
|
+
const coalesce = action.group && last?.group === action.group && now - last.time < COALESCE_MS
|
|
46
|
+
const entry = { source: state.source, selectedIndex: state.selectedIndex, group: action.group ?? null, time: now }
|
|
47
|
+
const past = coalesce ? [...state.history.past.slice(0, -1), { ...last, time: now }] : [...state.history.past, entry].slice(-HISTORY_LIMIT)
|
|
48
|
+
const { deck, diagnostics } = derive(source, state.manifests)
|
|
49
|
+
return { ...state, source, deck, diagnostics, selectedIndex: clamp(index, deck), history: { past, future: [] }, status: state.status === 'conflict' ? 'conflict' : 'unsaved' }
|
|
50
|
+
}
|
|
51
|
+
case 'undo':
|
|
52
|
+
case 'redo': {
|
|
53
|
+
const from = action.type === 'undo' ? state.history.past : state.history.future
|
|
54
|
+
const entry = from.at(-1)
|
|
55
|
+
if (!entry) return state
|
|
56
|
+
const current = { source: state.source, selectedIndex: state.selectedIndex, group: null, time: 0 }
|
|
57
|
+
const { deck, diagnostics } = derive(entry.source, state.manifests)
|
|
58
|
+
const history = action.type === 'undo'
|
|
59
|
+
? { past: from.slice(0, -1), future: [...state.history.future, current] }
|
|
60
|
+
: { past: [...state.history.past, current], future: from.slice(0, -1) }
|
|
61
|
+
return { ...state, source: entry.source, deck, diagnostics, selectedIndex: clamp(entry.selectedIndex, deck), history, status: state.status === 'conflict' ? 'conflict' : entry.source === state.savedSource ? 'saved' : 'unsaved' }
|
|
62
|
+
}
|
|
63
|
+
case 'select': return { ...state, selectedIndex: clamp(action.index, state.deck) }
|
|
64
|
+
case 'setTab': return { ...state, tab: action.tab }
|
|
65
|
+
case 'setInspectorMode': return { ...state, inspectorMode: action.mode }
|
|
66
|
+
case 'saving': return state.status === 'conflict' ? state : { ...state, status: 'saving', error: null }
|
|
67
|
+
case 'saved': return { ...state, hash: action.hash, savedSource: action.source, status: state.status === 'conflict' ? 'conflict' : action.source === state.source ? 'saved' : 'unsaved', error: null }
|
|
68
|
+
case 'saveError': return { ...state, status: 'error', error: action.message }
|
|
69
|
+
case 'conflict': return { ...state, status: 'conflict', conflict: { source: action.source, hash: action.hash } }
|
|
70
|
+
case 'resolveConflict': {
|
|
71
|
+
if (!state.conflict) return state
|
|
72
|
+
if (action.choice === 'overwrite') return { ...state, hash: state.conflict.hash, conflict: null, status: 'unsaved' }
|
|
73
|
+
const { deck, diagnostics } = derive(state.conflict.source, state.manifests)
|
|
74
|
+
const entry = { source: state.source, selectedIndex: state.selectedIndex, group: null, time: 0 }
|
|
75
|
+
return { ...state, source: state.conflict.source, savedSource: state.conflict.source, hash: state.conflict.hash, deck, diagnostics, selectedIndex: clamp(state.selectedIndex, deck), history: { past: [...state.history.past, entry].slice(-HISTORY_LIMIT), future: [] }, conflict: null, status: 'saved' }
|
|
76
|
+
}
|
|
77
|
+
case 'externalChange': {
|
|
78
|
+
if (action.hash === state.hash) return state
|
|
79
|
+
if (state.status !== 'saved') return reduce(state, { type: 'conflict', source: action.source, hash: action.hash })
|
|
80
|
+
const { deck, diagnostics } = derive(action.source, state.manifests)
|
|
81
|
+
const entry = { source: state.source, selectedIndex: state.selectedIndex, group: null, time: 0 }
|
|
82
|
+
return { ...state, source: action.source, savedSource: action.source, hash: action.hash, deck, diagnostics, selectedIndex: clamp(state.selectedIndex, deck), history: { past: [...state.history.past, entry].slice(-HISTORY_LIMIT), future: [] }, status: 'saved' }
|
|
83
|
+
}
|
|
84
|
+
case 'setRegistry': {
|
|
85
|
+
const manifests = { layouts: byId(action.registry?.layouts), themes: byId(action.registry?.themes), palettes: byId(action.registry?.palettes) }
|
|
86
|
+
const { deck, diagnostics } = derive(state.source, manifests)
|
|
87
|
+
return { ...state, registry: action.registry, manifests, warnings: action.registry?.warnings ?? state.warnings, deck, diagnostics }
|
|
88
|
+
}
|
|
89
|
+
case 'previewRendered': return { ...state, preview: { error: action.error ?? null, slideCount: action.slideCount ?? 0 } }
|
|
90
|
+
default: return state
|
|
91
|
+
}
|
|
92
|
+
}
|