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,193 @@
|
|
|
1
|
+
import { readerLink } from '../core/urls.js'
|
|
2
|
+
import { h } from 'preact'
|
|
3
|
+
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
|
|
4
|
+
import { marked } from 'marked'
|
|
5
|
+
import { deckOutline } from '../core/outline.js'
|
|
6
|
+
import { manifests, SlideRenderer } from '../layouts/renderSlide'
|
|
7
|
+
import { loadTheme, THEME_METAS } from './themeLoader'
|
|
8
|
+
import './reader.css'
|
|
9
|
+
import { t, stageLabels } from '../core/labels.js'
|
|
10
|
+
import { followActiveRooms } from '../live/follow.js'
|
|
11
|
+
import { Icon } from '../components/Icon.jsx'
|
|
12
|
+
|
|
13
|
+
// The reader view for decks sent around by email: outline, the slides at a
|
|
14
|
+
// comfortable size or stacked for reading, light or dark, PDF and a way back
|
|
15
|
+
// into the full-screen deck. The theme and palette stay the sender's: a sent
|
|
16
|
+
// file carries only those, with their fonts.
|
|
17
|
+
|
|
18
|
+
const lookKey = deckConfig => `mdeck-reader-look:${deckConfig.meta?.title ?? location.pathname}`
|
|
19
|
+
|
|
20
|
+
// The reader's choice of light or dark, '' for the deck's own.
|
|
21
|
+
function restoreAppearance(deckConfig) {
|
|
22
|
+
try {
|
|
23
|
+
const stored = JSON.parse(localStorage.getItem(lookKey(deckConfig)) ?? 'null')
|
|
24
|
+
if (['light', 'dark'].includes(stored?.appearance)) return stored.appearance
|
|
25
|
+
} catch {}
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Every button carries an icon so the bar reads at a glance.
|
|
30
|
+
const ReaderIcon = ({ name }) => <Icon name={name} class="reader-icon" />
|
|
31
|
+
|
|
32
|
+
function pdfLink() {
|
|
33
|
+
return document.querySelector('link[rel="alternate"][type="application/pdf"]')?.getAttribute('href') ?? null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function initialIndex(slides) {
|
|
37
|
+
const hash = decodeURIComponent(location.hash.slice(1))
|
|
38
|
+
const byId = slides.findIndex(slide => slide.id === hash)
|
|
39
|
+
if (byId >= 0) return byId
|
|
40
|
+
const n = parseInt(hash, 10)
|
|
41
|
+
return Number.isInteger(n) && n >= 1 && n <= slides.length ? n - 1 : 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ReadPage({ slide, index, total, deckConfig, width, height, scale, showNotes }) {
|
|
45
|
+
const notes = slide.meta.notes ?? ''
|
|
46
|
+
return <article class="reader-page" id={`reader-page-${index}`}>
|
|
47
|
+
<div class="reader-page-frame" style={{ aspectRatio: `${width} / ${height}` }}>
|
|
48
|
+
<div class="reader-page-scale" style={{ width: `${width}px`, height: `${height}px`, transform: `scale(${scale})` }}>
|
|
49
|
+
<SlideRenderer id={slide.id} regions={slide.regions} meta={slide.meta} content={slide.content} deckConfig={deckConfig} index={index} total={total} />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
{showNotes && notes && <div class="reader-page-notes" dangerouslySetInnerHTML={{ __html: marked.parse(notes) }} />}
|
|
53
|
+
</article>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function ReaderView({ deck, deckConfig }) {
|
|
57
|
+
const { slides } = deck
|
|
58
|
+
const width = deckConfig.width ?? 1920, height = deckConfig.height ?? 1080
|
|
59
|
+
const [appearance, setAppearance] = useState(() => restoreAppearance(deckConfig))
|
|
60
|
+
const [mode, setMode] = useState('slides')
|
|
61
|
+
const [index, setIndex] = useState(() => initialIndex(slides))
|
|
62
|
+
const [navOpen, setNavOpen] = useState(false)
|
|
63
|
+
const [toast, setToast] = useState('')
|
|
64
|
+
const [readScale, setReadScale] = useState(0.5)
|
|
65
|
+
const stageRef = useRef(null)
|
|
66
|
+
const readRef = useRef(null)
|
|
67
|
+
const outline = useMemo(() => deckOutline(deck, manifests, { slideName: n => t('reader.slide', { n }) }), [deck])
|
|
68
|
+
const ownAppearance = deckConfig.appearance ?? THEME_METAS[deckConfig.theme ?? 'neue']?.appearance ?? 'light'
|
|
69
|
+
const shown = appearance || ownAppearance
|
|
70
|
+
const toggleAllowed = deckConfig.reader?.themes !== false
|
|
71
|
+
const pdf = useMemo(pdfLink, [])
|
|
72
|
+
// Notes stay private unless the deck opts in; a file may carry them for the presenter.
|
|
73
|
+
const showNotes = deckConfig.reader?.notes === true && slides.some(slide => slide.meta.notes)
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
loadTheme({ ...deckConfig, appearance: shown }).catch(error => console.warn(error.message))
|
|
77
|
+
try { localStorage.setItem(lookKey(deckConfig), JSON.stringify({ appearance })) } catch {}
|
|
78
|
+
}, [appearance])
|
|
79
|
+
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
const stage = stageRef.current
|
|
82
|
+
if (!stage) return
|
|
83
|
+
stage.setLabels(stageLabels())
|
|
84
|
+
const stopFollowing = followActiveRooms(stage, slides)
|
|
85
|
+
const onChange = event => setIndex(event.detail.index)
|
|
86
|
+
stage.addEventListener('slidechange', onChange)
|
|
87
|
+
return () => { stage.removeEventListener('slidechange', onChange); stopFollowing() }
|
|
88
|
+
}, [mode])
|
|
89
|
+
|
|
90
|
+
// Read mode shows every slide finished: all steps revealed, as in print.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (mode !== 'read' || !readRef.current) return
|
|
93
|
+
readRef.current.querySelectorAll('[data-step]').forEach(step => step.setAttribute('data-step-visible', ''))
|
|
94
|
+
}, [mode])
|
|
95
|
+
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (mode !== 'read' || !readRef.current) return
|
|
98
|
+
const measure = () => { const page = readRef.current?.querySelector('.reader-page'); if (page) setReadScale(page.clientWidth / width) }
|
|
99
|
+
measure()
|
|
100
|
+
const observer = new ResizeObserver(measure)
|
|
101
|
+
observer.observe(readRef.current)
|
|
102
|
+
return () => observer.disconnect()
|
|
103
|
+
}, [mode, width])
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (mode !== 'read') return
|
|
107
|
+
const container = readRef.current
|
|
108
|
+
if (!container) return
|
|
109
|
+
const pages = [...container.querySelectorAll('.reader-page')]
|
|
110
|
+
const observer = new IntersectionObserver(entries => {
|
|
111
|
+
const visible = entries.filter(entry => entry.isIntersecting).sort((a, b) => b.intersectionRatio - a.intersectionRatio)[0]
|
|
112
|
+
if (visible) setIndex(pages.indexOf(visible.target))
|
|
113
|
+
}, { root: container, threshold: [0.5] })
|
|
114
|
+
pages.forEach(page => observer.observe(page))
|
|
115
|
+
return () => observer.disconnect()
|
|
116
|
+
}, [mode, readScale])
|
|
117
|
+
|
|
118
|
+
const goTo = target => {
|
|
119
|
+
const i = Math.max(0, Math.min(slides.length - 1, target))
|
|
120
|
+
setIndex(i)
|
|
121
|
+
setNavOpen(false)
|
|
122
|
+
if (mode === 'slides') stageRef.current?.goTo(i)
|
|
123
|
+
else document.getElementById(`reader-page-${i}`)?.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
124
|
+
history.replaceState(null, '', `#${encodeURIComponent(slides[i]?.id ?? String(i + 1))}`)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const switchMode = next => {
|
|
128
|
+
setMode(next)
|
|
129
|
+
if (next === 'read') setTimeout(() => document.getElementById(`reader-page-${index}`)?.scrollIntoView({ block: 'start' }), 50)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const copyLink = async () => {
|
|
133
|
+
const url = readerLink(location.href, slides[index]?.id ?? String(index + 1))
|
|
134
|
+
try { await navigator.clipboard.writeText(url.toString()); setToast(t('reader.linkCopied')) } catch { setToast(url.toString()) }
|
|
135
|
+
setTimeout(() => setToast(''), 1800)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const savePdf = () => {
|
|
139
|
+
if (mode !== 'slides') switchMode('slides')
|
|
140
|
+
setTimeout(() => window.print(), 150)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const meta = deckConfig.meta ?? {}
|
|
144
|
+
const metaLine = [meta.author, meta.organization, meta.date].filter(Boolean).join(' · ')
|
|
145
|
+
const other = shown === 'dark' ? 'light' : 'dark'
|
|
146
|
+
|
|
147
|
+
return <div class="reader-view">
|
|
148
|
+
<header class="reader-top">
|
|
149
|
+
<button class="reader-btn reader-nav-toggle" onClick={() => setNavOpen(open => !open)} aria-label={t('reader.outline')}><ReaderIcon name="menu" /></button>
|
|
150
|
+
<h1>{meta.title ?? t('reader.untitled')}</h1>
|
|
151
|
+
{metaLine && <span class="reader-meta">{metaLine}</span>}
|
|
152
|
+
<span class="reader-spacer" />
|
|
153
|
+
<span class="reader-count">{index + 1} / {slides.length}</span>
|
|
154
|
+
<button class={`reader-btn${mode === 'slides' ? ' is-active' : ''}`} onClick={() => switchMode('slides')}><ReaderIcon name="slides" />{t('reader.slides')}</button>
|
|
155
|
+
<button class={`reader-btn${mode === 'read' ? ' is-active' : ''}`} onClick={() => switchMode('read')}><ReaderIcon name="read" />{t('reader.read')}</button>
|
|
156
|
+
{toggleAllowed && <button class="reader-btn" onClick={() => setAppearance(other === ownAppearance ? '' : other)} title={t('reader.appearance')}><ReaderIcon name={other === 'dark' ? 'moon' : 'sun'} />{t(other === 'dark' ? 'reader.dark' : 'reader.light')}</button>}
|
|
157
|
+
{pdf
|
|
158
|
+
? <a class="reader-btn" href={pdf} download={`${(meta.title ?? 'slides').replace(/[^\w.-]+/g, '-')}.pdf`}><ReaderIcon name="download" />{t('reader.downloadPdf')}</a>
|
|
159
|
+
: <button class="reader-btn" onClick={savePdf} title={t('reader.savePdfHint')}><ReaderIcon name="download" />{t('reader.savePdf')}</button>}
|
|
160
|
+
<a class="reader-btn" href={`?view=deck#${encodeURIComponent(slides[index]?.id ?? String(index + 1))}`}><ReaderIcon name="present" />{t('reader.present')}</a>
|
|
161
|
+
</header>
|
|
162
|
+
<div class="reader-main">
|
|
163
|
+
<nav class={`reader-nav${navOpen ? ' is-open' : ''}`} aria-label={t('reader.slides')}>
|
|
164
|
+
<ol>
|
|
165
|
+
{outline.map(item => <li key={item.index} class={`${item.index === index ? 'is-current' : ''}${item.chapter ? ' is-chapter' : ''}`} onClick={() => goTo(item.index)}>
|
|
166
|
+
<span class="reader-num">{String(item.index + 1).padStart(2, '0')}</span>
|
|
167
|
+
<span class="reader-title">{item.chapter && item.part && <span class="reader-part">{item.part}</span>}{item.title}</span>
|
|
168
|
+
</li>)}
|
|
169
|
+
</ol>
|
|
170
|
+
</nav>
|
|
171
|
+
<section class="reader-content">
|
|
172
|
+
{mode === 'slides'
|
|
173
|
+
? <>
|
|
174
|
+
<div class="reader-stage-wrap">
|
|
175
|
+
<deck-stage ref={stageRef} width={width} height={height}>
|
|
176
|
+
{slides.map((slide, i) => <SlideRenderer key={slide.id} id={slide.id} regions={slide.regions} meta={slide.meta} content={slide.content} deckConfig={deckConfig} index={i} total={slides.length} />)}
|
|
177
|
+
</deck-stage>
|
|
178
|
+
</div>
|
|
179
|
+
<div class="reader-bottom">
|
|
180
|
+
<button class="reader-btn" onClick={() => stageRef.current?.prev('click')}><ReaderIcon name="prev" />{t('reader.previous')}</button>
|
|
181
|
+
<button class="reader-btn" onClick={() => stageRef.current?.next('click')}>{t('reader.next')}<ReaderIcon name="next" /></button>
|
|
182
|
+
<span class="reader-spacer" />
|
|
183
|
+
<button class="reader-btn" onClick={copyLink}><ReaderIcon name="link" />{t('reader.copyLink')}</button>
|
|
184
|
+
</div>
|
|
185
|
+
</>
|
|
186
|
+
: <div class="reader-read" ref={readRef} data-deck-static>
|
|
187
|
+
{slides.map((slide, i) => <ReadPage key={slide.id} slide={slide} index={i} total={slides.length} deckConfig={deckConfig} width={width} height={height} scale={readScale} showNotes={showNotes} />)}
|
|
188
|
+
</div>}
|
|
189
|
+
</section>
|
|
190
|
+
</div>
|
|
191
|
+
{toast && <div class="reader-toast">{toast}</div>}
|
|
192
|
+
</div>
|
|
193
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { h, Component } from 'preact'
|
|
2
|
+
|
|
3
|
+
// Keeps one broken slide from blanking the whole preview while editing. The
|
|
4
|
+
// stage still counts the slide, and the error clears when its source changes.
|
|
5
|
+
export class SlideErrorBoundary extends Component {
|
|
6
|
+
constructor(props) {
|
|
7
|
+
super(props)
|
|
8
|
+
this.state = { error: null, source: props.source }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static getDerivedStateFromProps(props, state) {
|
|
12
|
+
return props.source !== state.source ? { error: null, source: props.source } : null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
componentDidCatch(error) {
|
|
16
|
+
this.setState({ error: error?.message ?? String(error) })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
render({ id, children }, { error }) {
|
|
20
|
+
if (!error) return children
|
|
21
|
+
return <section class="slide slide--error" data-slide-id={id} data-label="Error">
|
|
22
|
+
<div class="slide-body" style={{ padding: '80px', fontFamily: 'ui-monospace, monospace', fontSize: '28px' }}>
|
|
23
|
+
<p>This slide could not be rendered.</p>
|
|
24
|
+
<pre style={{ whiteSpace: 'pre-wrap' }}>{error}</pre>
|
|
25
|
+
</div>
|
|
26
|
+
</section>
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { h } from 'preact'
|
|
2
|
+
|
|
3
|
+
// Dark control chrome shared by the presenter view and the editor; the editor's
|
|
4
|
+
// stylesheet and the launch page use the same values.
|
|
5
|
+
export const S = {
|
|
6
|
+
btn: {
|
|
7
|
+
appearance: 'none',
|
|
8
|
+
WebkitAppearance: 'none',
|
|
9
|
+
display: 'inline-flex',
|
|
10
|
+
alignItems: 'center',
|
|
11
|
+
justifyContent: 'center',
|
|
12
|
+
gap: '5px',
|
|
13
|
+
padding: '5px 12px',
|
|
14
|
+
borderRadius: '5px',
|
|
15
|
+
border: '1px solid #3a3a3a',
|
|
16
|
+
background: '#222',
|
|
17
|
+
color: '#e0e0e0',
|
|
18
|
+
cursor: 'pointer',
|
|
19
|
+
fontSize: '13px',
|
|
20
|
+
fontFamily: 'inherit',
|
|
21
|
+
lineHeight: '1.5',
|
|
22
|
+
outline: 'none',
|
|
23
|
+
},
|
|
24
|
+
select: {
|
|
25
|
+
padding: '5px 8px',
|
|
26
|
+
borderRadius: '5px',
|
|
27
|
+
border: '1px solid #3a3a3a',
|
|
28
|
+
background: '#222',
|
|
29
|
+
color: '#e0e0e0',
|
|
30
|
+
cursor: 'pointer',
|
|
31
|
+
fontSize: '13px',
|
|
32
|
+
fontFamily: 'inherit',
|
|
33
|
+
width: '100%',
|
|
34
|
+
},
|
|
35
|
+
label: {
|
|
36
|
+
color: '#a0a0a0',
|
|
37
|
+
fontSize: '11px',
|
|
38
|
+
textTransform: 'uppercase',
|
|
39
|
+
letterSpacing: '0.08em',
|
|
40
|
+
marginBottom: '5px',
|
|
41
|
+
display: 'block',
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function PaletteSwatches({ tokens }) {
|
|
46
|
+
const keys = ['--bg', '--surface', '--rule', '--accent', '--accent-2', '--ink']
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ display: 'flex', gap: '2px', alignItems: 'center', flexShrink: 0 }}>
|
|
49
|
+
{keys.map(k => tokens[k]
|
|
50
|
+
? <div key={k} style={{ width: '10px', height: '10px', borderRadius: '2px', background: tokens[k], border: '1px solid rgba(255,255,255,0.08)', flexShrink: 0 }} />
|
|
51
|
+
: null
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|