presently 0.17.2 → 0.18.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbd618c70d381e757d204404a473452d88cc0c1891909c6dea520364ff9eca8d
4
- data.tar.gz: 487397bb18e96b517ca941dabc902ed23e65cf9f1acc4aece5c741aa9a649088
3
+ metadata.gz: e645cb300c0b3005e89e9c384c420e5b0702d48798738dbd5caae47fdf829059
4
+ data.tar.gz: d3ed14a08c1df1ee1af2b3ae7188aa40742b2f2f08e202aaf46cd6d31646020e
5
5
  SHA512:
6
- metadata.gz: aecbc0cae0200a68de57dc5f5a97ab9b92db64bf06383fe6eb96aeb4eb4a41ae3c4ff86ef8f7df29498d3f4054426c3b86aca20133e1404b72adacb1184a9232
7
- data.tar.gz: cb70f29e9b067804054868a486a017fd262d9407a5b2885797e1b33357060b6e39c24f2977508cc7b95dcf9e579fa5c4e688fd37f75c165036eb1c8f250e1b4c
6
+ metadata.gz: b3509544227b7ab61cdec9f70bcaf3487d098dc0f7a5e5c94aea4eff58b828c7c3502122e56e0f1f19cdce21846019b60fd35adfc8f4654e675dc65be25eb6a0
7
+ data.tar.gz: 8e701e3f6253f6c9a4385ce745a789df17027763193da89c31e72b5e8136ceec588800400d2576a1c743a25da094f768a96fab38a08096ddc8da19644b5355df
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,259 @@
1
+ # Animated Diagrams
2
+
3
+ This guide explains how to design responsive, lifecycle-safe animated diagrams in Presently using semantic markup, slide-specific CSS, and Anime.js choreography.
4
+
5
+ ## Why Animated Diagrams?
6
+
7
+ Animation is useful when a diagram describes change rather than merely structure. A carefully paced diagram can show causality, ordering, concurrency, or data movement without presenting every relationship simultaneously.
8
+
9
+ Use an animated diagram when you need:
10
+
11
+ - **A process unfolding over time:** Requests, protocols, state machines, and deployment workflows.
12
+ - **Attention control:** Introduce one relationship at a time while keeping the complete layout stable.
13
+ - **Coordinated motion:** Move packets, highlight participants, update counters, or trace paths together.
14
+
15
+ Prefer a static diagram when motion does not add meaning. Prefer Presently's build effects or `slide.after()` when elements only need to appear sequentially.
16
+
17
+ ## Separate Structure, Appearance, and Choreography
18
+
19
+ A maintainable diagram has three layers:
20
+
21
+ 1. The slide's Markdown file contains the complete semantic structure.
22
+ 2. A matching sidecar stylesheet defines layout and visual language.
23
+ 3. The slide script describes how the scene changes over time.
24
+
25
+ For `slides/040-request-flow.md`, place its styles in `slides/040-request-flow.css`. Presently scopes that stylesheet to the matching slide automatically.
26
+
27
+ Keeping the complete scene in the document makes the diagram understandable without animation, prevents layout shifts, and gives agentic tools clear boundaries for editing each concern.
28
+
29
+ ## Choose the Right Rendering Medium
30
+
31
+ Presently does not require SVG. Choose the simplest medium that expresses the diagram:
32
+
33
+ | Medium | Best suited to |
34
+ |---|---|
35
+ | HTML with Grid or Flexbox | Cards, services, queues, labels, dashboards, and responsive process diagrams. |
36
+ | SVG with a `viewBox` | Edges, paths, graphs, precise coordinates, and shapes that must scale as one scene. |
37
+ | Canvas | Large numbers of particles or frequently redrawn objects where retained DOM elements become expensive. |
38
+ | Mixed HTML and SVG | Accessible HTML nodes over an SVG layer containing connectors and moving paths. |
39
+
40
+ Start with HTML and CSS. Introduce SVG when relationships or geometry require it, not merely because the result is called a diagram.
41
+
42
+ ## Create a Scoped Anime.js Timeline
43
+
44
+ `slide.anime(callback)` creates an Anime.js scope rooted at the current slide body. The callback receives Anime.js's exports and the raw scope:
45
+
46
+ ``` javascript
47
+ slide.anime(({createTimeline, stagger}, scope) => {
48
+ const timeline = createTimeline({
49
+ autoplay: slide.animated,
50
+ loop: slide.animated,
51
+ loopDelay: 1200,
52
+ })
53
+ .add(".diagram-node", {
54
+ opacity: [0.35, 1],
55
+ y: [12, 0],
56
+ delay: stagger(100),
57
+ })
58
+
59
+ if (!slide.animated) timeline.seek(timeline.duration)
60
+ })
61
+ ```
62
+
63
+ String selectors are resolved within the slide rather than the whole document. The same scope is returned from every call to `slide.anime()` and can be used for advanced Anime.js features:
64
+
65
+ ``` javascript
66
+ const scope = slide.anime()
67
+ ```
68
+
69
+ Presently calls `scope.revert()` when the slide is deactivated. This cancels its animations and restores properties modified through the scope. Additional resources such as audio, observers, and third-party controls should still use `slide.defer()` or `slide.signal`.
70
+
71
+ ## Reuse a Diagram Across Slides
72
+
73
+ Shared Markdown can define both a diagram and its Anime.js timeline. Mark the reusable setup with a `javascript presently` fence so Presently removes it from the rendered diagram and executes it before the slide-specific script:
74
+
75
+ ```` markdown
76
+ <div class="request-flow">
77
+ <!-- Shared diagram structure. -->
78
+ </div>
79
+
80
+ ```javascript presently
81
+ slide.anime(({createTimeline}, scope) => {
82
+ scope.data.timeline = createTimeline({autoplay: false})
83
+ .label("request")
84
+ // Define the complete shared choreography.
85
+ })
86
+ ```
87
+ ````
88
+
89
+ Include that fragment in each slide:
90
+
91
+ ``` markdown
92
+ ![[shared/request-flow.md]]
93
+ ```
94
+
95
+ The ordinary JavaScript block in the slide's presenter notes runs afterward and can select the state appropriate for that slide:
96
+
97
+ ``` javascript
98
+ const timeline = slide.anime().data.timeline
99
+ timeline.seek("request")
100
+ timeline.play()
101
+ ```
102
+
103
+ Every executable block has its own JavaScript lexical scope but receives the same `slide` object. Use the Anime scope's `data` or `methods` properties for intentional communication between shared setup and slide-specific control. All of those resources remain local to the rendered slide and are reverted together when it is deactivated.
104
+
105
+ ## Design the Static Scene First
106
+
107
+ Build and style the final diagram before adding animation. Every node should have a stable position, and hidden elements should still reserve the space they require.
108
+
109
+ Use a descriptive accessible label for a primarily visual scene:
110
+
111
+ ``` html
112
+ <div
113
+ class="request-flow"
114
+ role="img"
115
+ aria-label="A request travels from the browser through the application to the database, then returns as a response."
116
+ >
117
+ <!-- Complete diagram structure. -->
118
+ </div>
119
+ ```
120
+
121
+ If individual controls are interactive, keep them accessible instead of hiding the entire subtree behind `role="img"`.
122
+
123
+ ## Choreograph Meaning, Not Decoration
124
+
125
+ Organize the timeline into meaningful phases. Introduce the structure once, then repeat only the activity that represents ongoing work. The following example reveals a stable diagram before repeatedly sending a request and response through a traffic lane:
126
+
127
+ ``` javascript
128
+ slide.anime(({createTimeline, stagger}) => {
129
+ const traffic = createTimeline({
130
+ autoplay: false,
131
+ loop: true,
132
+ loopDelay: 900,
133
+ defaults: {ease: "inOutQuad"},
134
+ })
135
+ .add(".request", {
136
+ left: ["0%", "100%"],
137
+ opacity: [0, 1, 1, 0],
138
+ duration: 2200,
139
+ })
140
+ .add(".response", {
141
+ left: ["100%", "0%"],
142
+ opacity: [0, 1, 1, 0],
143
+ duration: 1800,
144
+ }, "+=350")
145
+
146
+ const intro = createTimeline({
147
+ autoplay: slide.animated,
148
+ onComplete: () => {
149
+ if (slide.animated) traffic.restart()
150
+ },
151
+ })
152
+ .add(".diagram-lane", {
153
+ opacity: [0, 1],
154
+ scaleX: [0.85, 1],
155
+ duration: 450,
156
+ })
157
+ .add(".diagram-node", {
158
+ opacity: [0.35, 1],
159
+ y: [10, 0],
160
+ delay: stagger(120),
161
+ duration: 500,
162
+ }, 150)
163
+ .add(".event", {
164
+ opacity: [0, 1],
165
+ y: [8, 0],
166
+ delay: stagger(180),
167
+ duration: 450,
168
+ }, 550)
169
+
170
+ if (!slide.animated) intro.seek(intro.duration)
171
+ })
172
+ ```
173
+
174
+ Place each moving element inside the lane which defines its path. This makes `0%` and `100%` meaningful endpoints and keeps traffic from obscuring node labels. Use separate lanes only when they communicate a meaningful distinction, such as concurrent channels or different routes.
175
+
176
+ Give moving elements a hidden initial state in the sidecar stylesheet, since the repeating timeline remains paused while the scene is introduced:
177
+
178
+ ``` css
179
+ .request,
180
+ .response {
181
+ opacity: 0;
182
+ }
183
+ ```
184
+
185
+ Prefer transformations and opacity for continuous movement. Use CSS Grid, Flexbox, percentages, container query units, or an SVG `viewBox` to keep geometry responsive. Avoid repeatedly measuring layout inside animation callbacks.
186
+
187
+ ## Establish a Consistent Visual Language
188
+
189
+ Use semantic classes and CSS custom properties so that meaning remains separate from a particular color or coordinate:
190
+
191
+ ``` css
192
+ .diagram-node {
193
+ --node-color: var(--accent);
194
+ border: 0.08em solid var(--node-color);
195
+ background: color-mix(in srgb, var(--node-color) 10%, var(--slide-bg));
196
+ }
197
+
198
+ .diagram-node[data-tone="storage"] {
199
+ --node-color: #f90;
200
+ }
201
+
202
+ .diagram-node[data-state="active"] {
203
+ box-shadow: 0 0 1em color-mix(in srgb, var(--node-color) 25%, transparent);
204
+ }
205
+ ```
206
+
207
+ Useful conventions include:
208
+
209
+ - `data-tone` for stable semantic roles such as client, processing, storage, success, and failure.
210
+ - `data-state` for runtime states such as idle, active, pending, complete, and unavailable.
211
+ - CSS variables for colors, line widths, spacing, timing, and repeated dimensions.
212
+ - Short labels and restrained color usage so motion remains the primary cue.
213
+
214
+ These conventions help separate agents generate diagrams that still look and behave like parts of the same presentation.
215
+
216
+ ## Static Export and Reduced Motion
217
+
218
+ `slide.animated` is false during static export and when the viewer prefers reduced motion. Do not autoplay or loop in that case. Seek to a frame which communicates the diagram's result:
219
+
220
+ ``` javascript
221
+ const timeline = createTimeline({
222
+ autoplay: slide.animated,
223
+ loop: slide.animated,
224
+ })
225
+
226
+ // Add the complete choreography before selecting the static frame.
227
+
228
+ if (!slide.animated) timeline.seek(timeline.duration)
229
+ ```
230
+
231
+ The final frame is not always the best summary. When necessary, add a timeline label and seek to that position instead.
232
+
233
+ ## Common Pitfalls
234
+
235
+ - Do not create or remove the primary layout repeatedly during animation. Construct it once and animate its state.
236
+ - Do not select from `document` when a selector should be scoped to the current slide.
237
+ - Do not leave timelines, event listeners, media, or observers active after navigation.
238
+ - Do not rely on color alone to communicate state.
239
+ - Do not animate every available property. Motion should explain the system rather than compete with it.
240
+ - Do not assume animation will run during export or for every viewer.
241
+
242
+ ## Instructions for Agentic Authoring
243
+
244
+ When asking an agent to create a diagram, provide the following constraints:
245
+
246
+ ``` text
247
+ Create a Presently diagram slide that explains [process].
248
+
249
+ Keep semantic structure in the Markdown slide, appearance in the matching
250
+ sidecar CSS file, and choreography in the slide's JavaScript block. Construct
251
+ the complete scene before animating it. Use HTML/CSS unless SVG materially
252
+ simplifies connectors or geometry. Use slide.anime() for coordinated motion,
253
+ respect slide.animated, and choose a meaningful static export frame. Keep all
254
+ selectors scoped to the slide, use data-state/data-tone for semantic state,
255
+ include an accessible description, and ensure cleanup is owned by the slide.
256
+ Use motion only to clarify causality, ordering, concurrency, or data flow.
257
+ ```
258
+
259
+ Ask the agent to verify the diagram at the audience display size and in the presenter preview. It should remain legible before the animation begins, at its busiest frame, and in its static exported state.
@@ -218,6 +218,32 @@ button.addEventListener("click", handleClick, {signal: slide.signal})
218
218
 
219
219
  Aborting the signal removes this event listener; it does not invoke `handleClick`. Use `slide.defer(...)` for resources which do not accept an `AbortSignal`.
220
220
 
221
+ ## Anime.js
222
+
223
+ Build effects and `slide.after()` work well for simple reveals. For coordinated motion, path animation, staggered elements, or a longer visual narrative, use `slide.anime(callback)` to create a lifecycle-managed [Anime.js](https://animejs.com/) scope.
224
+
225
+ ``` javascript
226
+ slide.anime(({createTimeline, stagger}) => {
227
+ const timeline = createTimeline({
228
+ autoplay: slide.animated,
229
+ loop: slide.animated,
230
+ })
231
+ .add(".diagram-node", {
232
+ opacity: [0, 1],
233
+ y: [12, 0],
234
+ delay: stagger(100),
235
+ })
236
+
237
+ if (!slide.animated) timeline.seek(timeline.duration)
238
+ })
239
+ ```
240
+
241
+ The callback receives the Anime.js module API and runs inside a scope rooted at the slide body. Selectors cannot match elements in another slide. Presently reuses the scope across calls and automatically invokes `scope.revert()` when the slide is deactivated.
242
+
243
+ `slide.animated` is false during static export and when the browser requests reduced motion. Configure timelines not to autoplay or loop in that case, then seek to a meaningful static frame.
244
+
245
+ See [Animated Diagrams](../animated-diagrams/) for layout, choreography, accessibility, and agent-authoring guidance.
246
+
221
247
  ## Looping Animations with `slide.loop()`
222
248
 
223
249
  To repeat an animation indefinitely, use `slide.loop()`. The callback receives a fresh `SlideContext` each iteration and can use `after()` to schedule steps in the same way as a regular chain. The loop waits for all steps to complete and then restarts, with an optional extra pause between iterations.
@@ -251,26 +277,28 @@ All slide templates support absolutely positioned elements since the slide conta
251
277
  </div>
252
278
  ~~~
253
279
 
254
- In the `diagram` template, all direct `<div>` children are `position: absolute` by default, so you can build free-form layouts without repeating the positioning declaration:
280
+ The `diagram` template centers its content by default. For coordinate-based layouts, use a `.diagram-freeform` wrapper; it fills the slide canvas and absolutely positions each direct child:
255
281
 
256
282
  ~~~ markdown
257
283
  ---
258
284
  template: diagram
259
285
  ---
260
286
 
261
- <div style="left: 10%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
262
- Node A
263
- </div>
287
+ <div class="diagram-freeform">
288
+ <div style="left: 10%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
289
+ Node A
290
+ </div>
264
291
 
265
- <div style="left: 55%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
266
- Node B
292
+ <div style="left: 55%; top: 20%; width: 35%; height: 30%; background: var(--surface-light);">
293
+ Node B
294
+ </div>
267
295
  </div>
268
296
  ~~~
269
297
 
270
298
  Combine with the scripting system to animate diagram elements into place:
271
299
 
272
300
  ``` javascript
273
- const nodes = slide.find("div").builder({group: "node", effect: "fade"})
301
+ const nodes = slide.find(".diagram-freeform > div").builder({group: "node", effect: "fade"})
274
302
  nodes.show(0)
275
303
  slide
276
304
  .after(400, () => nodes.next())
@@ -336,7 +336,7 @@ System architecture overview
336
336
 
337
337
  ### Diagram
338
338
 
339
- A free-form layout slide with a `position: relative` container. Direct `<div>` children are `position: absolute` by default, so you can place elements precisely using inline styles. Use this for custom diagrams, annotated layouts, or any slide that doesn't fit a standard template.
339
+ A centered canvas for diagrams and other custom visual layouts, with an optional title. A single grid or flex container is usually enough to create a diagram that remains centered as the slide scales:
340
340
 
341
341
  ``` markdown
342
342
  ---
@@ -344,15 +344,21 @@ template: diagram
344
344
  duration: 60
345
345
  ---
346
346
 
347
- <div style="left: 10%; top: 20%; width: 35%; height: 25%;">
348
- Browser
349
- </div>
347
+ # Title
348
+
349
+ Request lifecycle
350
350
 
351
- <div style="left: 55%; top: 20%; width: 35%; height: 25%;">
352
- Server
351
+ # Body
352
+
353
+ <div style="display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 2em; width: 80%;">
354
+ <div>Browser</div>
355
+ <div>→</div>
356
+ <div>Server</div>
353
357
  </div>
354
358
  ```
355
359
 
360
+ For coordinate-based layouts, wrap the elements in `<div class="diagram-freeform">`. The wrapper fills the canvas and absolutely positions each direct child.
361
+
356
362
  All other templates also support absolutely positioned overlays since the slide container is `position: relative`. This lets you add callouts, badges, or annotations on top of any template's normal content.
357
363
 
358
364
  ## Transitions
data/context/index.yaml CHANGED
@@ -14,3 +14,8 @@ files:
14
14
  title: Animating Slides
15
15
  description: This guide explains how to animate content within slides using the
16
16
  slide scripting system.
17
+ - path: animated-diagrams.md
18
+ title: Animated Diagrams
19
+ description: This guide explains how to design responsive, lifecycle-safe animated
20
+ diagrams in Presently using semantic markup, slide-specific CSS, and Anime.js
21
+ choreography.
@@ -60,7 +60,7 @@ module Presently
60
60
  return unless slide
61
61
 
62
62
  builder.tag(:div, class: "display", data: {transition: slide.transition}) do
63
- builder.tag(:div, class: "slide-container") do
63
+ builder.tag(:div, class: "slide-container slide-viewport") do
64
64
  @slide_renderer.render(builder, slide)
65
65
  end
66
66
 
@@ -117,7 +117,7 @@ module Presently
117
117
  builder.text("Elapsed: #{format_duration(elapsed)}")
118
118
  end
119
119
  builder.tag(:span, class: "export-duration") do
120
- builder.text("Slide: #{format_duration(slide.duration)}")
120
+ builder.text("Duration: #{format_duration(slide.duration)}")
121
121
  end
122
122
  end
123
123
 
@@ -14,7 +14,8 @@
14
14
  {
15
15
  "imports": {
16
16
  "@socketry/presently": "/_components/@socketry/presently/Presently.js",
17
- "@socketry/syntax": "/_components/@socketry/syntax/Syntax.js"
17
+ "@socketry/syntax": "/_components/@socketry/syntax/Syntax.js",
18
+ "animejs": "/_components/animejs/dist/bundles/anime.esm.min.js"
18
19
  }
19
20
  }
20
21
  </script>
@@ -25,7 +26,7 @@
25
26
  <body class="export">
26
27
  <?r self.slides.each_with_index do |slide, index| ?>
27
28
  <div class="export-page" style="width: #{self.page_size.slide_width_px}px; height: #{self.page_size.slide_height_px + (self.notes ? self.page_size.notes_height_px : 0)}px;">
28
- <div class="export-slide-area" style="width: #{self.page_size.slide_width_px}px; height: #{self.page_size.slide_height_px}px;">
29
+ <div class="export-slide-area slide-viewport" style="width: #{self.page_size.slide_width_px}px; height: #{self.page_size.slide_height_px}px;">
29
30
  #{self.render_slide(slide)}
30
31
  </div>
31
32
  <?r if self.notes ?>
@@ -24,6 +24,7 @@ module Presently
24
24
  "morphdom" => "/_components/morphdom/morphdom-esm.js",
25
25
  "@socketry/presently" => "/_components/@socketry/presently/Presently.js",
26
26
  "@socketry/syntax" => "/_components/@socketry/syntax/Syntax.js",
27
+ "animejs" => "/_components/animejs/dist/bundles/anime.esm.min.js",
27
28
  }.freeze
28
29
  MODULES = ["/application.js"].freeze
29
30
 
@@ -19,7 +19,8 @@
19
19
  {
20
20
  "imports": {
21
21
  "@socketry/presently": "/_components/@socketry/presently/Presently.js",
22
- "@socketry/syntax": "/_components/@socketry/syntax/Syntax.js"
22
+ "@socketry/syntax": "/_components/@socketry/syntax/Syntax.js",
23
+ "animejs": "/_components/animejs/dist/bundles/anime.esm.min.js"
23
24
  }
24
25
  }
25
26
  </script>
@@ -30,7 +31,7 @@
30
31
  <body class="playback" data-autoplay="#{self.autoplay}" data-controls="#{self.controls}">
31
32
  <main class="playback-stage">
32
33
  <?r self.slides.each_with_index do |slide, index| ?>
33
- <div class="playback-frame" data-index="#{index}" data-transition="#{slide.transition}" data-duration="#{slide.duration}"<?r if index != 0 ?> hidden<?r end ?>>
34
+ <div class="playback-frame slide-viewport" data-index="#{index}" data-transition="#{slide.transition}" data-duration="#{slide.duration}"<?r if index != 0 ?> hidden<?r end ?>>
34
35
  #{self.render_slide(slide)}
35
36
  </div>
36
37
  <?r end ?>
@@ -3,6 +3,8 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2026, by Samuel Williams.
5
5
 
6
+ require "console"
7
+
6
8
  require_relative "clock"
7
9
  require_relative "presentation"
8
10
  require_relative "state"
@@ -21,9 +23,9 @@ module Presently
21
23
  @current_index = 0
22
24
  @clock = Clock.new
23
25
  @listeners = []
24
- @state = state
25
26
 
26
- @state&.restore(self)
27
+ state&.restore(self)
28
+ @state = state
27
29
  end
28
30
 
29
31
  # @attribute [Presentation] The underlying presentation data.
@@ -110,7 +110,7 @@ module Presently
110
110
  else "on-time"
111
111
  end
112
112
 
113
- builder.tag(:div, class: "timing-info #{pacing_class}") do
113
+ builder.tag(:div, class: "toolbar timing-info #{pacing_class}") do
114
114
  builder.tag(:button,
115
115
  class: "pause-button",
116
116
  onClick: forward_event(action: "pause")
@@ -151,7 +151,7 @@ module Presently
151
151
 
152
152
  if slide
153
153
  builder.tag(:span, class: "slide-duration") do
154
- builder.text("Slide: #{format_duration(slide.duration)}")
154
+ builder.text("Duration: #{format_duration(slide.duration)}")
155
155
  end
156
156
  end
157
157
 
@@ -191,21 +191,26 @@ module Presently
191
191
 
192
192
  builder.tag(:div, class: "presenter") do
193
193
  # Controls bar
194
- builder.tag(:div, class: "controls") do
194
+ builder.tag(:div, class: "toolbar controls") do
195
195
  builder.tag(:button,
196
196
  onClick: forward_event(action: "previous")
197
197
  ) do
198
198
  builder.text("← Previous")
199
199
  end
200
200
 
201
+ builder.tag(:button,
202
+ onClick: forward_event(action: "next")
203
+ ) do
204
+ builder.text("Next →")
205
+ end
206
+
201
207
  builder.tag(:span, class: "slide-info") do
202
- builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
208
+ builder.tag(:span, class: "slide-position") do
209
+ builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
210
+ end
203
211
 
204
212
  if slide
205
- builder.text(" · ")
206
- builder.tag(:code, class: "slide-path") do
207
- builder.text(slide.path)
208
- end
213
+ render_slide_path(builder, slide.path)
209
214
 
210
215
  if editor_url = editor_url_for(slide.source_path)
211
216
  builder.tag(:a, href: editor_url, class: "edit-link") do
@@ -215,12 +220,6 @@ module Presently
215
220
  end
216
221
  end
217
222
 
218
- builder.tag(:button,
219
- onClick: forward_event(action: "next")
220
- ) do
221
- builder.text("Next →")
222
- end
223
-
224
223
  # Jump-to dropdown for marked slides
225
224
  markers = []
226
225
  @controller.slides.each_with_index do |s, i|
@@ -232,7 +231,7 @@ module Presently
232
231
  unless markers.empty?
233
232
  builder.tag(:select,
234
233
  class: "jump-to",
235
- data: {live_id: @id}
234
+ "data-live-id": @id
236
235
  ) do
237
236
  builder.tag(:option, value: "", disabled: true, selected: true) do
238
237
  builder.text("Jump to…")
@@ -259,7 +258,7 @@ module Presently
259
258
  # Current slide
260
259
  builder.tag(:div, class: "preview current-preview") do
261
260
  builder.tag(:h3){builder.text("Current")}
262
- builder.tag(:div, class: "preview-frame") do
261
+ builder.tag(:div, class: "preview-frame slide-viewport") do
263
262
  @preview_renderer.render(builder, slide)
264
263
  end
265
264
  end
@@ -267,7 +266,7 @@ module Presently
267
266
  # Next slide
268
267
  builder.tag(:div, class: "preview next-preview") do
269
268
  builder.tag(:h3){builder.text("Next")}
270
- builder.tag(:div, class: "preview-frame") do
269
+ builder.tag(:div, class: "preview-frame slide-viewport") do
271
270
  if next_slide
272
271
  @preview_renderer.render(builder, next_slide)
273
272
  else
@@ -80,7 +80,7 @@ module Presently
80
80
  render_navigation(builder, slide)
81
81
 
82
82
  builder.tag(:div, class: "recording-workspace") do
83
- builder.tag(:div, class: "recording-preview") do
83
+ builder.tag(:div, class: "recording-preview slide-viewport") do
84
84
  @slide_renderer.render(builder, slide)
85
85
  end
86
86
 
@@ -124,20 +124,21 @@ module Presently
124
124
  # @parameter builder [XRB::Builder] The HTML builder.
125
125
  # @parameter slide [Slide] The current slide.
126
126
  def render_navigation(builder, slide)
127
- builder.tag(:div, class: "controls recording-navigation") do
127
+ builder.tag(:div, class: "toolbar controls recording-navigation") do
128
128
  builder.tag(:button, onClick: forward_event(action: "previous")){builder.text("← Previous")}
129
+ builder.tag(:button, onClick: forward_event(action: "next")){builder.text("Next →")}
129
130
 
130
131
  builder.tag(:span, class: "slide-info") do
131
- builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count} · ")
132
- builder.tag(:code, class: "slide-path"){builder.text(slide.path)}
132
+ builder.tag(:span, class: "slide-position") do
133
+ builder.text("Slide #{@controller.current_index + 1} of #{@controller.slide_count}")
134
+ end
135
+ render_slide_path(builder, slide.path)
133
136
 
134
137
  if editor_url = editor_url_for(slide.source_path)
135
138
  builder.tag(:a, href: editor_url, class: "edit-link"){builder.text("✎")}
136
139
  end
137
140
  end
138
141
 
139
- builder.tag(:button, onClick: forward_event(action: "next")){builder.text("Next →")}
140
-
141
142
  markers = @controller.slides.each_with_index.filter_map do |candidate, index|
142
143
  [index, candidate.marker] if candidate.marker
143
144
  end