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.
Files changed (268) hide show
  1. package/CHANGELOG.md +259 -0
  2. package/LICENSE +21 -0
  3. package/README.md +156 -0
  4. package/assets/base.css +196 -0
  5. package/assets/extensions/layouts/chapter/extension.toml +26 -0
  6. package/assets/extensions/layouts/chapter/layout.jsx +21 -0
  7. package/assets/extensions/layouts/chapter/starter.md +8 -0
  8. package/assets/extensions/layouts/focus/extension.toml +8 -0
  9. package/assets/extensions/layouts/focus/layout.jsx +10 -0
  10. package/assets/extensions/layouts/focus/starter.md +4 -0
  11. package/assets/extensions/layouts/full-bleed-image/extension.toml +31 -0
  12. package/assets/extensions/layouts/full-bleed-image/layout.jsx +19 -0
  13. package/assets/extensions/layouts/full-bleed-image/starter.md +6 -0
  14. package/assets/extensions/layouts/generic/extension.toml +8 -0
  15. package/assets/extensions/layouts/generic/layout.jsx +8 -0
  16. package/assets/extensions/layouts/generic/starter.md +4 -0
  17. package/assets/extensions/layouts/image-text/extension.toml +25 -0
  18. package/assets/extensions/layouts/image-text/layout.jsx +17 -0
  19. package/assets/extensions/layouts/image-text/starter.md +6 -0
  20. package/assets/extensions/layouts/split/extension.toml +25 -0
  21. package/assets/extensions/layouts/split/layout.jsx +19 -0
  22. package/assets/extensions/layouts/split/starter.md +14 -0
  23. package/assets/extensions/layouts/title/extension.toml +26 -0
  24. package/assets/extensions/layouts/title/layout.jsx +14 -0
  25. package/assets/extensions/layouts/title/starter.md +5 -0
  26. package/assets/extensions/palettes/brand/extension.toml +29 -0
  27. package/assets/extensions/palettes/cobalt/extension.toml +27 -0
  28. package/assets/extensions/palettes/ember/extension.toml +27 -0
  29. package/assets/extensions/palettes/forest/extension.toml +27 -0
  30. package/assets/extensions/palettes/graphite/extension.toml +27 -0
  31. package/assets/extensions/palettes/lagoon/extension.toml +27 -0
  32. package/assets/extensions/palettes/neon/extension.toml +27 -0
  33. package/assets/extensions/palettes/nordic/extension.toml +27 -0
  34. package/assets/extensions/palettes/pastel/extension.toml +30 -0
  35. package/assets/extensions/palettes/phosphor/extension.toml +27 -0
  36. package/assets/extensions/palettes/swiss/extension.toml +27 -0
  37. package/assets/extensions/palettes/terra/extension.toml +27 -0
  38. package/assets/extensions/themes/academic/extension.toml +53 -0
  39. package/assets/extensions/themes/academic/styles.css +631 -0
  40. package/assets/extensions/themes/aurora/extension.toml +37 -0
  41. package/assets/extensions/themes/aurora/styles.css +644 -0
  42. package/assets/extensions/themes/duet/extension.toml +46 -0
  43. package/assets/extensions/themes/duet/styles.css +573 -0
  44. package/assets/extensions/themes/editorial/extension.toml +46 -0
  45. package/assets/extensions/themes/editorial/styles.css +574 -0
  46. package/assets/extensions/themes/fhnw/extension.toml +61 -0
  47. package/assets/extensions/themes/fhnw/styles.css +604 -0
  48. package/assets/extensions/themes/neue/extension.toml +46 -0
  49. package/assets/extensions/themes/neue/styles.css +489 -0
  50. package/assets/extensions/themes/sketch/extension.toml +48 -0
  51. package/assets/extensions/themes/sketch/styles.css +633 -0
  52. package/assets/extensions/themes/terminal/extension.toml +38 -0
  53. package/assets/extensions/themes/terminal/styles.css +550 -0
  54. package/assets/logo/avatar.png +0 -0
  55. package/assets/logo/logo-dark.svg +10 -0
  56. package/assets/logo/logo-light.svg +10 -0
  57. package/assets/logo/mark.svg +15 -0
  58. package/bin/mdeck.js +2 -0
  59. package/docs/README.md +8 -0
  60. package/docs/reference/authoring.md +795 -0
  61. package/docs/reference/claude-skill.md +96 -0
  62. package/docs/reference/extensions.md +250 -0
  63. package/docs/reference/layouts.md +153 -0
  64. package/docs/reference/palettes.md +106 -0
  65. package/docs/reference/structured-slides.md +120 -0
  66. package/docs/reference/themes.md +133 -0
  67. package/docs/site/assets/favicon.svg +15 -0
  68. package/docs/site/assets/github-logo.svg +1 -0
  69. package/docs/site/assets/site.css +162 -0
  70. package/docs/site/assets/site.js +123 -0
  71. package/docs/site/build.js +148 -0
  72. package/docs/site/content/appearance.md +203 -0
  73. package/docs/site/content/audience.md +178 -0
  74. package/docs/site/content/commands.md +153 -0
  75. package/docs/site/content/components.md +134 -0
  76. package/docs/site/content/drawing.md +72 -0
  77. package/docs/site/content/editing.md +59 -0
  78. package/docs/site/content/getting-started.md +103 -0
  79. package/docs/site/content/layouts.md +126 -0
  80. package/docs/site/content/media.md +107 -0
  81. package/docs/site/content/more-content.md +130 -0
  82. package/docs/site/content/presenting.md +93 -0
  83. package/docs/site/content/reusable-layouts.md +74 -0
  84. package/docs/site/content/sharing.md +88 -0
  85. package/docs/site/content/theme-authoring.md +144 -0
  86. package/docs/site/content/theme-repository.md +76 -0
  87. package/docs/site/content/troubleshooting.md +92 -0
  88. package/docs/site/content/views.md +56 -0
  89. package/docs/site/content/welcome.md +71 -0
  90. package/docs/site/content/writing.md +121 -0
  91. package/docs/site/examples/first-talk.md +88 -0
  92. package/docs/site/examples/notebook.svg +58 -0
  93. package/docs/site/images/themes/academic-dark.webp +0 -0
  94. package/docs/site/images/themes/academic-light.webp +0 -0
  95. package/docs/site/images/themes/academic.webp +0 -0
  96. package/docs/site/images/themes/aurora-dark.webp +0 -0
  97. package/docs/site/images/themes/aurora-light.webp +0 -0
  98. package/docs/site/images/themes/aurora.webp +0 -0
  99. package/docs/site/images/themes/duet-dark.webp +0 -0
  100. package/docs/site/images/themes/duet-light.webp +0 -0
  101. package/docs/site/images/themes/duet.webp +0 -0
  102. package/docs/site/images/themes/editorial-dark.webp +0 -0
  103. package/docs/site/images/themes/editorial-light.webp +0 -0
  104. package/docs/site/images/themes/editorial.webp +0 -0
  105. package/docs/site/images/themes/fhnw.webp +0 -0
  106. package/docs/site/images/themes/neue-dark.webp +0 -0
  107. package/docs/site/images/themes/neue-light.webp +0 -0
  108. package/docs/site/images/themes/neue.webp +0 -0
  109. package/docs/site/images/themes/sketch-dark.webp +0 -0
  110. package/docs/site/images/themes/sketch-light.webp +0 -0
  111. package/docs/site/images/themes/sketch.webp +0 -0
  112. package/docs/site/images/themes/terminal-dark.webp +0 -0
  113. package/docs/site/images/themes/terminal-light.webp +0 -0
  114. package/docs/site/images/themes/terminal.webp +0 -0
  115. package/docs/site/package.json +9 -0
  116. package/docs/site/pages.js +27 -0
  117. package/docs/site/server.js +58 -0
  118. package/examples/custom-layouts/extensions/comparison/extension.toml +35 -0
  119. package/examples/custom-layouts/extensions/comparison/layout.jsx +15 -0
  120. package/examples/custom-layouts/extensions/comparison/starter.md +17 -0
  121. package/examples/custom-layouts/extensions/comparison/styles.css +4 -0
  122. package/examples/custom-layouts/slides.md +60 -0
  123. package/examples/poll/slides.md +127 -0
  124. package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T08-36-47Z.json +19 -0
  125. package/examples/python/.mdeck-backups/slides.drawings-2026-10-06T11-27-30Z.json +6 -0
  126. package/examples/python/slides.drawings.json +6 -0
  127. package/examples/python/slides.md +493 -0
  128. package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T08-08-30Z.json +10 -0
  129. package/examples/showcase/.mdeck-backups/slides.drawings-2026-10-06T11-41-35Z.json +13 -0
  130. package/examples/showcase/img/image.jpg +0 -0
  131. package/examples/showcase/img/mdeck.svg +66 -0
  132. package/examples/showcase/slides.drawings.json +11 -0
  133. package/examples/showcase/slides.md +427 -0
  134. package/package.json +75 -0
  135. package/skills/write-slides/SKILL.md +144 -0
  136. package/src/build/check.js +49 -0
  137. package/src/build/chrome.js +130 -0
  138. package/src/build/components.js +44 -0
  139. package/src/build/config.js +61 -0
  140. package/src/build/drawings.js +14 -0
  141. package/src/build/editorPlugin.js +272 -0
  142. package/src/build/editorServer.js +36 -0
  143. package/src/build/fonts.js +84 -0
  144. package/src/build/homePlugin.js +208 -0
  145. package/src/build/inkPlugin.js +192 -0
  146. package/src/build/ownWrites.js +18 -0
  147. package/src/build/pairing.js +81 -0
  148. package/src/build/pdf.js +34 -0
  149. package/src/build/relayAccess.js +85 -0
  150. package/src/build/slidesPlugin.js +371 -0
  151. package/src/build/themeCheck.js +151 -0
  152. package/src/build/tunnelClient.js +184 -0
  153. package/src/cli/args.js +76 -0
  154. package/src/cli/index.js +758 -0
  155. package/src/cli/migrate.js +123 -0
  156. package/src/cli/skill.js +40 -0
  157. package/src/cli/themes.js +140 -0
  158. package/src/components/CodeBlock.jsx +168 -0
  159. package/src/components/ConnectedViews.jsx +25 -0
  160. package/src/components/Icon.jsx +8 -0
  161. package/src/components/Poll.jsx +67 -0
  162. package/src/components/QrCode.jsx +29 -0
  163. package/src/components/QrCodeTag.jsx +27 -0
  164. package/src/components/Question.jsx +28 -0
  165. package/src/components/Scale.jsx +47 -0
  166. package/src/components/VideoPlayer.jsx +108 -0
  167. package/src/components/WordCloud.jsx +106 -0
  168. package/src/components/activity.jsx +35 -0
  169. package/src/components/callout.css +78 -0
  170. package/src/components/code-block.css +123 -0
  171. package/src/components/columns.css +11 -0
  172. package/src/components/poll.css +81 -0
  173. package/src/components/video-player.css +14 -0
  174. package/src/core/devPath.js +4 -0
  175. package/src/core/editDeck.js +230 -0
  176. package/src/core/icons.js +69 -0
  177. package/src/core/ink.js +221 -0
  178. package/src/core/labels.js +144 -0
  179. package/src/core/outline.js +33 -0
  180. package/src/core/parseSlides.js +152 -0
  181. package/src/core/source.js +137 -0
  182. package/src/core/urls.js +34 -0
  183. package/src/core/validateDeck.js +139 -0
  184. package/src/editor/api.js +67 -0
  185. package/src/editor/components/App.jsx +75 -0
  186. package/src/editor/components/ConflictBanner.jsx +10 -0
  187. package/src/editor/components/DeckSettings.jsx +108 -0
  188. package/src/editor/components/DesignApp.jsx +252 -0
  189. package/src/editor/components/Diagnostics.jsx +10 -0
  190. package/src/editor/components/ExtensionEditor.jsx +66 -0
  191. package/src/editor/components/ExtensionForms.jsx +134 -0
  192. package/src/editor/components/ExtensionsPanel.jsx +114 -0
  193. package/src/editor/components/Field.jsx +24 -0
  194. package/src/editor/components/Inspector.jsx +36 -0
  195. package/src/editor/components/LayoutPicker.jsx +23 -0
  196. package/src/editor/components/LayoutSelect.jsx +25 -0
  197. package/src/editor/components/LookThumbnail.jsx +8 -0
  198. package/src/editor/components/MetaFields.jsx +24 -0
  199. package/src/editor/components/Outline.jsx +28 -0
  200. package/src/editor/components/PalettePicker.jsx +32 -0
  201. package/src/editor/components/Preview.jsx +63 -0
  202. package/src/editor/components/PropsForm.jsx +46 -0
  203. package/src/editor/components/RawSlideEditor.jsx +9 -0
  204. package/src/editor/components/RegionEditor.jsx +15 -0
  205. package/src/editor/components/Resizer.jsx +18 -0
  206. package/src/editor/designLink.js +19 -0
  207. package/src/editor/editor.css +139 -0
  208. package/src/editor/extensions.js +86 -0
  209. package/src/editor/keyboard.js +21 -0
  210. package/src/editor/model.js +38 -0
  211. package/src/editor/sampleDeck.js +165 -0
  212. package/src/editor/saveQueue.js +47 -0
  213. package/src/editor/schema.js +36 -0
  214. package/src/editor/state.js +92 -0
  215. package/src/editor/useDeckDocument.js +67 -0
  216. package/src/extensions/appearance.js +79 -0
  217. package/src/extensions/contrast.js +57 -0
  218. package/src/extensions/discover.js +126 -0
  219. package/src/extensions/manifest.js +209 -0
  220. package/src/extensions/packs.js +268 -0
  221. package/src/extensions/tokens.js +25 -0
  222. package/src/home/Home.jsx +362 -0
  223. package/src/home/home.css +148 -0
  224. package/src/layouts/layoutApi.jsx +205 -0
  225. package/src/layouts/layoutProps.js +30 -0
  226. package/src/layouts/renderSlide.jsx +17 -0
  227. package/src/live/answer/answer.css +34 -0
  228. package/src/live/answer/answer.html +14 -0
  229. package/src/live/answer/answer.js +130 -0
  230. package/src/live/client.js +290 -0
  231. package/src/live/code.js +19 -0
  232. package/src/live/follow.js +17 -0
  233. package/src/live/pairing.js +34 -0
  234. package/src/live/presence.js +33 -0
  235. package/src/live/roomTag.js +38 -0
  236. package/src/live/rooms.js +249 -0
  237. package/src/live/server.js +82 -0
  238. package/src/live/tunnel.js +180 -0
  239. package/src/paths.js +9 -0
  240. package/src/runtime/ReaderView.jsx +193 -0
  241. package/src/runtime/SlideErrorBoundary.jsx +28 -0
  242. package/src/runtime/chrome.jsx +55 -0
  243. package/src/runtime/deck-stage.js +1533 -0
  244. package/src/runtime/design.html +12 -0
  245. package/src/runtime/design.jsx +5 -0
  246. package/src/runtime/editor.html +12 -0
  247. package/src/runtime/editor.jsx +5 -0
  248. package/src/runtime/editorBridge.js +58 -0
  249. package/src/runtime/fit.js +149 -0
  250. package/src/runtime/home.html +13 -0
  251. package/src/runtime/home.jsx +5 -0
  252. package/src/runtime/index.html +15 -0
  253. package/src/runtime/ink/InkLayer.jsx +17 -0
  254. package/src/runtime/ink/Toolbar.jsx +145 -0
  255. package/src/runtime/ink/attach.js +69 -0
  256. package/src/runtime/ink/bus.js +126 -0
  257. package/src/runtime/ink/controller.js +132 -0
  258. package/src/runtime/ink/room.js +101 -0
  259. package/src/runtime/ink/select.js +124 -0
  260. package/src/runtime/ink/server.js +75 -0
  261. package/src/runtime/ink/store.js +55 -0
  262. package/src/runtime/ink/toolbar.css +79 -0
  263. package/src/runtime/main.jsx +863 -0
  264. package/src/runtime/markedSetup.js +147 -0
  265. package/src/runtime/reader.css +59 -0
  266. package/src/runtime/registry.jsx +26 -0
  267. package/src/runtime/syncOrder.js +32 -0
  268. package/src/runtime/themeLoader.js +68 -0
@@ -0,0 +1,120 @@
1
+ # Structured slides and validation
2
+
3
+ Existing decks keep their `---` frontmatter syntax. Code fences protect literal
4
+ `---` and `:::notes` examples. A deck can also start directly with Markdown.
5
+ Legacy slide metadata is recognized by a known field at the start of the block
6
+ (such as `layout:`, `section:` or `image:`). For arbitrary metadata, or to remove
7
+ that ambiguity, use an explicit metadata block at the start of a slide:
8
+
9
+ ```markdown
10
+ ---
11
+ :::meta
12
+ layout: split
13
+ id: deployment-options
14
+ :::
15
+ # Deployment options
16
+
17
+ :::slot left
18
+ ## Directory bundle
19
+ - Separate media files
20
+ :::
21
+
22
+ :::slot right
23
+ ## Single HTML file
24
+ - Easy to transfer
25
+ :::
26
+
27
+ :::notes
28
+ Explain the tradeoff.
29
+ :::
30
+ ```
31
+
32
+ `:::meta` belongs to the slide, while the optional initial deck configuration
33
+ continues to use YAML between `---` lines. Put a blank line before a directive
34
+ when combining it with ordinary Markdown. Directives can nest: for example, a
35
+ callout inside columns inside a slot. `+++` separates columns only at that
36
+ column block's top level. Slots, notes and metadata are top-level slide blocks.
37
+
38
+ Unassigned Markdown is the `body` region. `split` accepts `left` and `right`
39
+ regions with the shared body above them; without named regions it retains the
40
+ legacy first-block-left behavior. An explicit `:::slot body` is also supported,
41
+ but cannot be combined with unassigned content. Duplicate or unclosed regions
42
+ are errors. Layouts declare the other regions they accept.
43
+
44
+ Set `id:` for a durable link such as `#deployment-options`. Numeric links such
45
+ as `#3` remain supported. Without an authored ID, the parser assigns positional
46
+ IDs (`slide-1`, etc.); these are not stable across reordering.
47
+
48
+ Run `mdeck check talk.md` to validate YAML, dimensions, IDs, metadata and local
49
+ asset references. `--strict` also fails on warnings. Builds reject source errors;
50
+ unknown layouts warn and retain the generic fallback for compatibility.
51
+
52
+ ## Source model for integrations
53
+
54
+ `parseSlides(source)` returns the original `source`, `deckConfig`, `configSource`,
55
+ `slides` and `diagnostics`. Each slide has its resolved `meta`, `authoredMeta`
56
+ (the authored YAML fields before section inheritance), `id`, `content`, `regions`, directive `blocks`, and
57
+ `source`, `metaSource`, `bodySource` ranges. Ranges are half-open UTF-16 offsets
58
+ into the original string, including its whitespace and line endings.
59
+
60
+ Regions contain Markdown `content`. Explicit regions also carry their original
61
+ `raw` body and contiguous `source` range. The implicit body has `ranges` because
62
+ notes and slots may interrupt it. This is a source-preserving document model,
63
+ not a normalized Markdown serializer. Speaker notes come from `:::notes` body
64
+ blocks and are available as `slide.meta.notes`; they are not authored metadata.
65
+
66
+ `serializeDeck(deck)` returns the original source unchanged. Use
67
+ `replaceRegion(deck, slideId, regionName, markdown)` for targeted explicit-region
68
+ edits, or `applySourceEdits(source, edits)` for general changes. Edits specify
69
+ `start`, `end`, `text` and optionally `expected` text; overlaps and stale expected
70
+ text are rejected. Reparse the returned source after editing. Callers saving to
71
+ disk must also compare against the current file to detect external changes.
72
+
73
+ ## Editing helpers
74
+
75
+ `src/core/editDeck.js` builds on the source model to change a deck without
76
+ disturbing anything else in the file. Every function takes the parsed deck,
77
+ never mutates it, and returns the new source (or `{ source, index }` for
78
+ structural changes); reparse the result before the next edit. Untouched bytes
79
+ stay identical, including comments, blank lines and the file's line endings.
80
+
81
+ | Function | Effect |
82
+ |---|---|
83
+ | `setRegion(deck, slideId, name, markdown)` | Replaces an explicit `:::slot` body, the implicit body (discontiguous ranges collapse into the first one), or appends a new slot |
84
+ | `removeRegion(deck, slideId, name)` | Removes a slot block |
85
+ | `setSlideMeta(deck, slideId, patch)` | Patches top-level keys of the slide's YAML; `undefined` deletes a key; works for `:::meta` and the legacy `---` form, inserting or removing the block as needed |
86
+ | `setSlideNotes(deck, slideId, markdown)` | Edits or removes a `:::notes` block, or appends one when absent |
87
+ | `setDeckConfig(deck, patch)` | Patches the frontmatter, inserting it when absent |
88
+ | `insertSlide(deck, index, markdown)` | Inserts a slide, keeping delimiters and blank lines tidy |
89
+ | `removeSlide(deck, slideId)` / `moveSlide(deck, slideId, toIndex)` | Structural changes that return the index to select next |
90
+ | `replaceSlideSource(deck, slideId, text)` | Replaces one slide's Markdown verbatim |
91
+
92
+ Metadata patches rewrite only the block of each changed key
93
+ (`patchYamlMapping` in `source.js`), so comments on other keys survive.
94
+ Comments inside an edited nested block such as `props:` are lost.
95
+
96
+ ## Editor API
97
+
98
+ `mdeck edit` serves a small JSON API from the Vite dev server for the browser
99
+ editor. It binds to the loopback interface, refuses requests whose `Origin`
100
+ differs from its own host, and only ever writes the deck file and folders
101
+ under the deck's `extensions/` directory.
102
+
103
+ | Route | Purpose |
104
+ |---|---|
105
+ | `GET /__mdeck/deck` | `{ path, name, source, hash, registry, warnings }` |
106
+ | `POST /__mdeck/source` | `{ source, base }` writes when `base` equals the hash of the file on disk; otherwise `409 { source, hash }` |
107
+ | `GET /__mdeck/extension/:kind/:id` | The extension's manifest and text files |
108
+ | `PUT /__mdeck/extension/:kind/:id` | `{ files: { name: text \| null } }` writes into `extensions/<id>/` after validating the manifest against the files that will exist; built-ins are refused |
109
+ | `DELETE /__mdeck/extension/:kind/:id` | Removes a deck-local extension folder |
110
+
111
+ Two custom websocket events keep the editor in step with the disk:
112
+ `mdeck:deck-changed { hash }` when another program saved the deck, and
113
+ `mdeck:extensions-changed { file }` when an extension file changed.
114
+
115
+ The preview iframe runs the normal deck runtime with `?view=deck&editor=1`. The editor
116
+ posts `{ deckSource: { source, selection, config, overrides } }` to it;
117
+ `config` merges over the deck settings (for example to preview a palette) and
118
+ `overrides` carries unsaved theme and palette manifests. The runtime answers
119
+ with `{ deckEditorReady }`, `{ deckRendered: { slideCount, diagnostics, error } }`
120
+ and the usual `{ deckStateChanged }` messages.
@@ -0,0 +1,133 @@
1
+ # Themes
2
+
3
+ A theme defines how slides look: typography, spacing and layout structure. Its
4
+ colours come from a [palette](palettes.md): each theme names a default, and the
5
+ deck may choose another. A theme is one kind of [extension](extensions.md).
6
+
7
+ ## Using a theme
8
+
9
+ Set `theme:` in the deck frontmatter:
10
+
11
+ ```yaml
12
+ ---
13
+ theme: neue
14
+ ---
15
+ ```
16
+
17
+ | Theme | Look | Default palette |
18
+ |---|---|---|
19
+ | `neue` | Swiss style: large flush-left type, rules, red on white | `swiss` |
20
+ | `aurora` | Northern lights: curtains of light, night-sky chapters, frosted glass | `neon` |
21
+ | `duet` | Two voices: heavy slab headings, split slides as two colour fields, diagonal chapters | `cobalt` |
22
+ | `editorial` | Magazine spread: masthead rule, drop caps, pull quotes, Roman chapter numerals | `terra` |
23
+ | `fhnw` | The FHNW corporate design | `brand` (the only one) |
24
+ | `terminal` | A terminal session: prompts, a blinking cursor, a tmux status bar | `phosphor`, dark |
25
+ | `academic` | A lecture: compact type, the heading fixed at the top and the content centred below, theorem and definition blocks, booktabs tables, a footnote rule | `nordic` |
26
+ | `sketch` | A sketchbook: handwriting on dotted paper, marker scribbles, sticky-note callouts, taped photos | `pastel` |
27
+
28
+ `mdeck list my-talk.md` lists them with their descriptions.
29
+
30
+ ## Theme structure
31
+
32
+ ```text
33
+ assets/extensions/themes/my-theme/ built-in
34
+ extensions/my-theme/ beside a deck
35
+ extension.toml identity, default palette, fonts, tokens, parameters
36
+ styles.css slide frame and layout rules
37
+ ```
38
+
39
+ No registration step is needed: any theme folder in either place is found
40
+ automatically, and the `mdeck new` wizard offers it.
41
+
42
+ ### `extension.toml`
43
+
44
+ ```toml
45
+ schema = 1
46
+ kind = "theme"
47
+ id = "my-theme"
48
+ title = "My Theme"
49
+ palette = "lagoon"
50
+ fonts = ["https://fonts.googleapis.com/css2?family=..."]
51
+
52
+ [tokens]
53
+ "--fs-display" = "128px"
54
+ "--fs-h" = "64px"
55
+ "--fs-body" = "34px"
56
+ "--pad-x" = "140px"
57
+ "--pad-y" = "110px"
58
+ "--font-display" = "\"My Display Font\", sans-serif"
59
+ "--font-body" = "\"My Body Font\", sans-serif"
60
+
61
+ [params.fontDisplay]
62
+ token = "--font-display"
63
+ title = "Display font"
64
+ ```
65
+
66
+ `palette` names the default palette. `tokens` hold the typography and spacing
67
+ values the stylesheet uses; mdeck writes them into a `:root` block ahead of the
68
+ stylesheet. A theme may not set the nine palette colours.
69
+
70
+ `params` connect author-facing names to tokens. Users set them in the deck:
71
+
72
+ ```yaml
73
+ params:
74
+ fontDisplay: "Georgia, serif"
75
+ ```
76
+
77
+ A parameter's default is its token's value. Unknown parameter names are
78
+ reported by `mdeck check`.
79
+
80
+ Optional settings: `description`, `appearance = "dark"` for a theme that starts
81
+ dark, and `palettes = [...]` to offer only those palettes (the FHNW theme offers
82
+ only `brand`). `[files] styles` may name one stylesheet or a list.
83
+
84
+ ### `styles.css`
85
+
86
+ Layout CSS for all slide types. It refers to the palette colours (`--bg`,
87
+ `--ink`, `--accent` …) and the theme's tokens for every colour and spacing value
88
+ and contains no `:root` defaults of its own. It must style:
89
+
90
+ - `.slide` — base slide
91
+ - `.slide-header`, `.slide-footer`, `.slide-body` — structural rails
92
+ - `.slide--title`, `.slide--chapter`, `.slide--focus`, `.slide--image-text`, `.slide--split`, `.slide--full-bleed-image` — the built-in layouts
93
+
94
+ ## Custom layouts
95
+
96
+ Define reusable slide structures with [deck-local layouts](layouts.md).
97
+ They provide named regions, typed properties, and shared slide chrome without
98
+ editing the framework. Theme CSS continues to style those structures.
99
+
100
+ For compatibility, an unrecognised `layout:` value warns and falls through to the
101
+ generic renderer. It renders the full Markdown body with standard slide chrome
102
+ and emits a `.slide--<layout-name>` class. Prefer a declared layout for new layouts.
103
+
104
+ To style a custom layout, target `.slide--<layout-name>` in a CSS file loaded alongside your deck, or use inline styles in your markdown HTML.
105
+
106
+ Because `.slide-body` sets baseline typography (`font-family`, `font-size`, `color`, `line-height`) as inheritable defaults, any raw HTML you write inside a custom slide automatically inherits the theme's body text style. You only need inline styles for structural concerns like grid layout:
107
+
108
+ ```markdown
109
+ ---
110
+ layout: four-columns
111
+ ---
112
+ # My heading
113
+
114
+ <div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 48px;">
115
+ <div><strong>One</strong> — description text</div>
116
+ <div><strong>Two</strong> — description text</div>
117
+ <div><strong>Three</strong> — description text</div>
118
+ <div><strong>Four</strong> — description text</div>
119
+ </div>
120
+ ```
121
+
122
+ Standard markdown elements (`h1`–`h6`, `p`, `ul`, `ol`, `strong`, `em`, `code`) are styled by the theme and work without any inline styles.
123
+
124
+ ## Colour contract
125
+
126
+ A theme uses only the nine palette colours, and mixes of them
127
+ (`color-mix(in oklab, var(--accent) 14%, var(--bg))`). This is what makes every
128
+ palette fit every theme. For an inverted slide it reads the palette's other
129
+ variant, `--inverse-bg`, `--inverse-ink`, `--inverse-accent` and so on, which is
130
+ tuned to be read on its own background.
131
+
132
+ Themes may define additional tokens for type, spacing and shapes; those are
133
+ theme-specific and are not affected by palettes.
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
2
+ <title>mdeck</title>
3
+ <rect width="512" height="512" rx="96" fill="#0d9488"/>
4
+ <rect x="72" y="136" width="368" height="240" rx="18" fill="#f4f1e8"/>
5
+ <g fill="#0d9488">
6
+ <rect x="132" y="176" width="26" height="160" rx="13"/>
7
+ <rect x="184" y="176" width="26" height="160" rx="13"/>
8
+ <rect x="102" y="222" width="140" height="26" rx="13"/>
9
+ <rect x="102" y="266" width="140" height="26" rx="13"/>
10
+ </g>
11
+ <g fill="#1b1a17" opacity="0.85">
12
+ <rect x="268" y="226" width="130" height="20" rx="10"/>
13
+ <rect x="268" y="276" width="88" height="20" rx="10"/>
14
+ </g>
15
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="20" height="20"><path fill="#24292f" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
@@ -0,0 +1,162 @@
1
+ :root {
2
+ --paper: #fff;
3
+ --mist: #eef2f7;
4
+ --ink: #203148;
5
+ --secondary: #54647a;
6
+ --blue: #244ec7;
7
+ --blue-soft: #e8eefc;
8
+ --rule: #dbe3ee;
9
+ --code: #f5f7fa;
10
+ --font: 'Avenir Next', Avenir, 'Segoe UI', sans-serif;
11
+ --mono: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
12
+ color: var(--ink);
13
+ background: var(--paper);
14
+ font: 17px/1.7 var(--font);
15
+ scroll-padding-top: 110px;
16
+ }
17
+ * { box-sizing: border-box; }
18
+ body { margin: 0; }
19
+ a { color: var(--blue); text-decoration-thickness: 1px; text-underline-offset: .2em; }
20
+ a:hover { text-decoration-thickness: 2px; }
21
+ button, input, select { font: inherit; }
22
+ button, select { cursor: pointer; }
23
+ button { color: var(--ink); }
24
+ button:disabled { cursor: default; }
25
+ :focus-visible { outline: 3px solid var(--blue); outline-offset: 4px; border-radius: 2px; }
26
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
27
+ .skip-link { position: fixed; top: -100px; left: 16px; z-index: 100; padding: 12px 20px; background: white; border: 2px solid var(--blue); }
28
+ .skip-link:focus { top: 12px; }
29
+ .site-header { height: 82px; padding: 0 38px; display: flex; align-items: center; justify-content: space-between; gap: 30px; background: var(--paper); border-bottom: 1px solid var(--rule); position: sticky; top: 0; z-index: 10; }
30
+ .brand { display: inline-flex; align-items: center; gap: 13px; color: var(--ink); text-decoration: none; font-size: 27px; font-weight: 750; letter-spacing: -.05em; }
31
+ .brand img { width: 30px; height: 30px; border-radius: 7px; }
32
+ .brand-guide { font-size: 15px; font-weight: 450; color: var(--secondary); letter-spacing: 0; margin-left: 12px; padding-left: 20px; border-left: 1px solid var(--rule); }
33
+ .header-tools { display: flex; align-items: center; gap: 14px; }
34
+ .repo-link { display: inline-flex; align-items: center; gap: 7px; padding: 8px 12px; border: 1px solid var(--rule); border-radius: 6px; color: var(--ink); text-decoration: none; font-size: 14px; font-weight: 500; white-space: nowrap; }
35
+ .repo-link:hover { background: var(--mist); }
36
+ .repo-link img { width: 20px; height: 20px; }
37
+ @media (max-width: 640px) { .repo-link { font-size: 0; gap: 0; padding: 8px; } }
38
+ .search-box { width: min(340px, 45vw); position: relative; }
39
+ .search-box input { appearance: none; width: 100%; padding: 9px 38px 9px 15px; color: var(--ink); background: var(--mist); border: 1px solid transparent; border-radius: 6px; font-size: 14px; }
40
+ .search-box input::placeholder { color: var(--secondary); }
41
+ .search-box kbd { position: absolute; right: 14px; top: 9px; font: 14px/24px var(--mono); color: var(--secondary); pointer-events: none; }
42
+ #search-results { position: absolute; right: 0; top: calc(100% + 10px); width: min(530px, 90vw); max-height: 65vh; overflow: auto; padding: 8px; background: white; border: 1px solid var(--rule); border-radius: 8px; box-shadow: 0 12px 35px #20314820; }
43
+ #search-results[hidden] { display: none; }
44
+ #search-results a { display: block; padding: 12px; text-decoration: none; border-radius: 4px; }
45
+ #search-results a:hover, #search-results a:focus-visible { background: var(--blue-soft); }
46
+ #search-results strong { display: block; font-size: 15px; }
47
+ #search-results span, #search-results p { font-size: 13px; line-height: 1.5; color: var(--secondary); }
48
+ #search-results p { margin: 12px; }
49
+ .site-layout { display: grid; grid-template-columns: 256px minmax(0, 820px) 200px; max-width: 1430px; padding: 0 38px; margin: auto; column-gap: 46px; }
50
+ .nav-shell { position: sticky; top: 82px; height: calc(100dvh - 82px); overflow: auto; padding: 32px 10px 28px 0; scrollbar-width: thin; }
51
+ .nav-shell > summary { display: none; }
52
+ .nav-group { margin: 0 0 23px; }
53
+ .nav-group > p, .advanced-nav > summary { margin: 0 0 8px; color: var(--ink); font-size: 13px; font-weight: 700; }
54
+ .nav-group a, .advanced-nav a { display: block; padding: 6px 12px; margin: 2px 0; border-radius: 4px; font-size: 13px; line-height: 1.55; text-decoration: none; color: var(--secondary); }
55
+ .nav-group a:hover, .advanced-nav a:hover { background: var(--mist); color: var(--ink); }
56
+ .nav-shell a[aria-current] { background: var(--blue-soft); color: var(--blue); font-weight: 650; }
57
+ .advanced-nav { padding-top: 10px; border-top: 1px solid var(--rule); }
58
+ .advanced-nav > summary { padding: 6px 0; cursor: pointer; }
59
+ .nav-help { display: block; margin-top: 27px; padding-top: 18px; border-top: 1px solid var(--rule); color: var(--secondary); text-decoration: none; font-size: 11px; }
60
+ .nav-help code { display: block; margin-top: 3px; color: var(--ink); font-size: 12px; }
61
+ main { min-width: 0; padding: 49px 0 20px; }
62
+ article { max-width: 76ch; }
63
+ article img { display: block; max-width: 100%; height: auto; border: 1px solid var(--rule); border-radius: 6px; }
64
+ article a:has(> img) { display: block; margin: 16px 0 8px; }
65
+ .breadcrumb { color: var(--secondary); font-size: 13px; margin: 0 0 12px; }
66
+ h1, h2, h3 { line-height: 1.2; letter-spacing: -.035em; font-weight: 700; color: var(--ink); }
67
+ h1 { font-size: clamp(32px, 3vw, 46px); margin: 0 0 24px; max-width: 23ch; }
68
+ .welcome h1 { font-size: clamp(40px, 4.5vw, 63px); line-height: 1.08; max-width: 14ch; letter-spacing: -.055em; }
69
+ .welcome article > p:first-of-type { font-size: 20px; line-height: 1.55; color: var(--secondary); max-width: 53ch; }
70
+ .welcome article > p:nth-of-type(3) > a { display: inline-block; background: var(--blue); color: white; text-decoration: none; padding: 10px 19px; border-radius: 5px; font-size: 15px; font-weight: 600; }
71
+ h2 { font-size: 28px; margin: 52px 0 17px; }
72
+ h3 { font-size: 21px; margin: 32px 0 12px; }
73
+ p { margin: 0 0 20px; }
74
+ li { padding-left: 3px; margin-bottom: 9px; }
75
+ ul, ol { padding-left: 25px; margin: 20px 0; }
76
+ strong { font-weight: 650; }
77
+ .heading-link { margin-left: 10px; font-size: .7em; font-weight: 400; opacity: 0; text-decoration: none; }
78
+ h2:hover .heading-link, h3:hover .heading-link, .heading-link:focus-visible { opacity: 1; }
79
+ code, kbd { font-family: var(--mono); }
80
+ :not(pre) > code { font-size: .84em; background: var(--mist); padding: 2px 5px; border-radius: 3px; overflow-wrap: anywhere; }
81
+ .code-example { margin: 25px 0; border: 1px solid var(--rule); border-radius: 7px; overflow: hidden; background: var(--code); }
82
+ .code-example figcaption { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; border-bottom: 1px solid var(--rule); font-size: 12px; color: var(--secondary); }
83
+ .code-example button { background: none; border: 0; font-size: 12px; padding: 4px 9px; border-radius: 3px; }
84
+ .code-example button:hover { background: var(--blue-soft); }
85
+ pre { margin: 0; padding: 20px; overflow-x: auto; font: 13px/1.8 var(--mono); tab-size: 2; }
86
+ pre code { white-space: pre; }
87
+ .token.comment, .token.prolog { color: #637084; }
88
+ .token.punctuation { color: #536279; }
89
+ .token.keyword, .token.tag, .token.important { color: #244ec7; }
90
+ .token.string, .token.attr-value { color: #17625c; }
91
+ .token.function, .token.attr-name, .token.key { color: #753fa1; }
92
+ .token.number, .token.boolean { color: #a33a51; }
93
+ .token.title, .token.bold { font-weight: 700; }
94
+ .table-scroll { overflow-x: auto; margin: 26px 0; border: 1px solid var(--rule); border-radius: 5px; }
95
+ table { border-collapse: collapse; width: 100%; font-size: 14px; text-align: left; }
96
+ th { background: var(--mist); font-weight: 650; }
97
+ th, td { padding: 12px 16px; border-bottom: 1px solid var(--rule); vertical-align: top; }
98
+ tr:last-child td { border-bottom: 0; }
99
+ td { min-width: 140px; }
100
+ blockquote, .advanced-notice { border-left: 3px solid var(--blue); padding: 12px 19px; margin: 24px 0; background: var(--blue-soft); font-size: 15px; }
101
+ blockquote p:last-child { margin: 0; }
102
+ .slide-example { margin: 32px 0; border-radius: 8px; overflow: hidden; background: var(--blue-soft); border: 1px solid #c6d3f2; }
103
+ .slide-example > figcaption { display: flex; justify-content: space-between; gap: 20px; padding: 13px 18px; font-size: 12px; color: #344d80; }
104
+ .slide-example iframe { display: block; width: calc(100% - 36px); aspect-ratio: 16 / 9; margin: 0 18px; border: 0; background: white; box-shadow: 0 3px 9px #20314815; }
105
+ .preview-controls { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 13px 18px; font-size: 12px; }
106
+ .preview-controls > div { display: flex; align-items: center; gap: 7px; }
107
+ .preview-controls button, .preview-controls select { padding: 5px 9px; border: 1px solid #bdcce9; background: white; border-radius: 4px; font-size: 12px; }
108
+ .preview-controls button:hover { border-color: var(--blue); color: var(--blue); }
109
+ .preview-controls output { margin-left: 5px; color: #344d80; }
110
+ .preview-controls label { display: flex; align-items: center; gap: 7px; color: #344d80; }
111
+ .page-outline { position: sticky; top: 120px; align-self: start; font-size: 12px; max-height: calc(100dvh - 150px); overflow: auto; padding-left: 18px; border-left: 1px solid var(--rule); margin-top: 53px; }
112
+ .page-outline > p { font-weight: 650; margin: 0 0 12px; }
113
+ .page-outline a { display: block; margin-bottom: 10px; color: var(--secondary); text-decoration: none; line-height: 1.5; }
114
+ .page-outline a:hover { color: var(--blue); }
115
+ .page-navigation { display: flex; justify-content: space-between; gap: 24px; margin: 64px 0 30px; padding-top: 25px; border-top: 1px solid var(--rule); font-size: 15px; }
116
+ .page-navigation a { flex: 1; text-decoration: none; }
117
+ .page-navigation a:last-child { text-align: right; }
118
+ .page-navigation small { display: block; font-size: 11px; color: var(--secondary); margin-bottom: 6px; }
119
+ .article-footer { font-size: 11px; color: var(--secondary); display: flex; justify-content: space-between; gap: 20px; padding-bottom: 24px; }
120
+ @media (min-width: 961px) { .nav-shell:not([open]) > nav { display: block; } }
121
+ @media (max-width: 1250px) { .site-layout { grid-template-columns: 224px minmax(0, 1fr); max-width: 1110px; gap: 38px; } .page-outline { display: none; } }
122
+ @media (max-width: 960px) {
123
+ .site-header { padding: 0 22px; height: 70px; }
124
+ .site-layout { display: block; padding: 0 24px; max-width: 780px; }
125
+ .nav-shell { position: static; height: auto; overflow: visible; padding: 0; border-bottom: 1px solid var(--rule); }
126
+ .nav-shell > summary { display: list-item; font-size: 14px; padding: 15px 0; cursor: pointer; }
127
+ .nav-shell nav { padding: 10px 0 20px; }
128
+ main { padding-top: 30px; }
129
+ h1 { max-width: none; }
130
+ }
131
+ @media (max-width: 520px) {
132
+ :root { font-size: 16px; }
133
+ .site-header { padding: 0 16px; gap: 16px; }
134
+ .brand { gap: 8px; font-size: 24px; }
135
+ .brand img, .brand svg { width: 28px; }
136
+ .brand-guide { display: none; }
137
+ .search-box { width: 49vw; }
138
+ .search-box input { font-size: 12px; }
139
+ .site-layout { padding: 0 18px; }
140
+ h2 { font-size: 25px; }
141
+ pre { padding: 15px; font-size: 12px; }
142
+ .preview-controls { flex-wrap: wrap; }
143
+ .slide-example > figcaption { flex-wrap: wrap; gap: 3px; }
144
+ .article-footer span { display: none; }
145
+ }
146
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { scroll-behavior: auto !important; } }
147
+ @media print { .site-header, .nav-shell, .page-outline, .page-navigation, .code-example button, .preview-controls { display: none; } .site-layout { display: block; padding: 0; } main { padding: 0; } pre { white-space: pre-wrap; } pre code { white-space: pre-wrap; } a { color: inherit; } }
148
+
149
+ .nav-project { margin-top: 24px; padding-top: 20px; border-top: 1px solid var(--rule); }
150
+ .article-footer { display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap; }
151
+ .footer-links { display: flex; gap: 14px; }
152
+ .footer-links a { color: var(--secondary); text-decoration: none; }
153
+ .footer-links a:hover { color: var(--blue); }
154
+
155
+ /* Tabs for the theme contact sheets */
156
+ .theme-sheets { margin: 20px 0 32px; }
157
+ .theme-tabs { display: flex; flex-wrap: wrap; gap: 4px; border-bottom: 1px solid var(--rule); margin-bottom: 16px; }
158
+ .theme-tabs [role="tab"] { font: inherit; font-size: 15px; color: var(--secondary); background: none; border: 0; border-bottom: 3px solid transparent; padding: 8px 14px; margin-bottom: -1px; cursor: pointer; }
159
+ .theme-tabs [role="tab"]:hover { color: var(--ink); }
160
+ .theme-tabs [role="tab"][aria-selected="true"] { color: var(--blue); border-bottom-color: var(--blue); font-weight: 600; }
161
+ .theme-tabs [role="tab"]:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; border-radius: 4px; }
162
+ .theme-sheet a { margin: 0; }
@@ -0,0 +1,123 @@
1
+ const search = document.querySelector('#search')
2
+ const results = document.querySelector('#search-results')
3
+ const status = document.querySelector('#search-status')
4
+ let searchIndex
5
+ let searchVersion = 0
6
+
7
+ async function runSearch() {
8
+ const version = ++searchVersion
9
+ const query = search.value.trim().toLowerCase()
10
+ results.replaceChildren()
11
+ results.hidden = !query
12
+ search.setAttribute('aria-expanded', String(!!query))
13
+ if (!query) { status.textContent = ''; return }
14
+ try {
15
+ searchIndex ??= fetch('search.json').then(response => { if (!response.ok) throw new Error(); return response.json() })
16
+ const index = await searchIndex
17
+ if (version !== searchVersion) return
18
+ const words = query.split(/\s+/)
19
+ const matches = index.map(page => {
20
+ const title = page.title.toLowerCase(), description = page.description.toLowerCase(), text = page.text.toLowerCase()
21
+ return { ...page, score: words.every(word => `${title} ${description} ${text}`.includes(word)) ? words.reduce((score, word) => score + (title.includes(word) ? 10 : description.includes(word) ? 4 : 1), 0) : 0 }
22
+ }).filter(page => page.score > 0).sort((a, b) => b.score - a.score).slice(0, 8)
23
+ status.textContent = `${matches.length} guides found.`
24
+ if (!matches.length) {
25
+ const message = document.createElement('p')
26
+ message.textContent = 'No guides found. Try “picture”, “notes”, or “share”.'
27
+ results.append(message)
28
+ }
29
+ for (const page of matches) {
30
+ const link = document.createElement('a'), title = document.createElement('strong'), description = document.createElement('span')
31
+ link.href = page.href
32
+ title.textContent = page.title
33
+ description.textContent = page.description
34
+ link.append(title, description)
35
+ results.append(link)
36
+ }
37
+ } catch {
38
+ searchIndex = undefined
39
+ const message = document.createElement('p')
40
+ message.textContent = 'Search could not load. Use the guide menu, or open these pages with mdeck docs.'
41
+ results.append(message)
42
+ }
43
+ }
44
+ search.addEventListener('input', runSearch)
45
+ search.addEventListener('focus', () => { if (search.value.trim()) runSearch() })
46
+ search.addEventListener('keydown', event => {
47
+ if (event.key === 'ArrowDown') { event.preventDefault(); results.querySelector('a')?.focus() }
48
+ if (event.key === 'Enter' && results.querySelector('a')) results.querySelector('a').click()
49
+ })
50
+ document.addEventListener('keydown', event => {
51
+ if (event.key === '/' && !event.target.closest('input, textarea, select, [contenteditable]') && !event.metaKey && !event.ctrlKey) { event.preventDefault(); search.focus() }
52
+ if (event.key === 'Escape') { results.hidden = true; search.setAttribute('aria-expanded', 'false'); if (results.contains(document.activeElement)) search.focus() }
53
+ })
54
+ document.addEventListener('click', event => { if (!event.target.closest('.search-box')) { results.hidden = true; search.setAttribute('aria-expanded', 'false') } })
55
+
56
+ const navigation = document.querySelector('.nav-shell')
57
+ const mobile = matchMedia('(max-width: 960px)')
58
+ function adaptNavigation() { navigation.open = !mobile.matches }
59
+ adaptNavigation()
60
+ mobile.addEventListener('change', adaptNavigation)
61
+
62
+ for (const button of document.querySelectorAll('[data-copy]')) {
63
+ button.addEventListener('click', async () => {
64
+ const code = button.closest('figure').querySelector('code')
65
+ try {
66
+ await navigator.clipboard.writeText(code.textContent)
67
+ button.textContent = 'Copied'
68
+ } catch {
69
+ const range = document.createRange()
70
+ range.selectNodeContents(code)
71
+ const selection = window.getSelection()
72
+ selection.removeAllRanges()
73
+ selection.addRange(range)
74
+ button.textContent = 'Selected — copy now'
75
+ }
76
+ setTimeout(() => { button.textContent = 'Copy' }, 2500)
77
+ })
78
+ }
79
+
80
+ for (const example of document.querySelectorAll('[data-preview]')) {
81
+ const frame = example.querySelector('iframe')
82
+ const output = example.querySelector('output')
83
+ let index = 0
84
+ for (const button of example.querySelectorAll('[data-control]')) {
85
+ button.addEventListener('click', () => frame.contentWindow.postMessage({ deckControl: { command: button.dataset.control } }, location.origin))
86
+ }
87
+ example.querySelector('select').addEventListener('change', event => {
88
+ const url = new URL(frame.src)
89
+ url.searchParams.set('theme', event.target.value)
90
+ url.hash = String(index + 1)
91
+ frame.src = url.toString()
92
+ })
93
+ window.addEventListener('message', event => {
94
+ if (event.source !== frame.contentWindow || event.origin !== location.origin || !event.data?.deckStateChanged) return
95
+ index = event.data.deckStateChanged.index
96
+ output.textContent = `Slide ${index + 1}`
97
+ })
98
+ }
99
+
100
+ // Tabs: one panel at a time; without the script every panel shows.
101
+ for (const group of document.querySelectorAll('[data-tabs]')) {
102
+ const tabs = [...group.querySelectorAll('[role="tab"]')]
103
+ const select = tab => {
104
+ for (const other of tabs) {
105
+ const selected = other === tab
106
+ other.setAttribute('aria-selected', String(selected))
107
+ other.tabIndex = selected ? 0 : -1
108
+ document.getElementById(other.getAttribute('aria-controls')).hidden = !selected
109
+ }
110
+ }
111
+ tabs.forEach(tab => tab.addEventListener('click', () => select(tab)))
112
+ group.querySelector('[role="tablist"]').addEventListener('keydown', event => {
113
+ const step = { ArrowRight: 1, ArrowLeft: -1 }[event.key]
114
+ const index = tabs.indexOf(document.activeElement)
115
+ if (index < 0) return
116
+ const next = step ? tabs[(index + step + tabs.length) % tabs.length] : event.key === 'Home' ? tabs[0] : event.key === 'End' ? tabs.at(-1) : null
117
+ if (!next) return
118
+ event.preventDefault()
119
+ select(next)
120
+ next.focus()
121
+ })
122
+ select(tabs.find(tab => tab.getAttribute('aria-selected') === 'true') ?? tabs[0])
123
+ }