presently 0.6.0 → 0.7.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/presently/version.rb +1 -1
- data/public/_static/index.css +6 -6
- data/public/application.js +10 -2
- data/public/slide.js +188 -26
- data/readme.md +13 -0
- data/releases.md +13 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e5c00d36415375cd15afef5fbc6b2669200b981ebb8744ccd5f04d770ea71877
|
|
4
|
+
data.tar.gz: b154f921b651b3572b307b529cc6b630601ccabce70a8a06e089bada66a211e8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b28384ce74c14c486da7fa1d80a81fd6b2b3a386f8ceac6e2a0ffbf6f6a5aa39325a31550badf722446a772ebf2cc11071eec354ec0d33b8f5bc1912b518726c
|
|
7
|
+
data.tar.gz: c555c644843daa56b0366575d14c3d85e7dd358bfa897654c47fd3b291c8a6ae14f0764af13139d54af4fced6cb46acc2aa38fd74bd3ee5f0b62ab377b242606
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/presently/version.rb
CHANGED
data/public/_static/index.css
CHANGED
|
@@ -423,7 +423,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
/* Fade */
|
|
426
|
-
|
|
426
|
+
.build-fade {
|
|
427
427
|
animation: vt-fade-in 0.4s ease;
|
|
428
428
|
}
|
|
429
429
|
|
|
@@ -433,7 +433,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
433
433
|
to { transform: translateX(0); opacity: 1; }
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
|
|
436
|
+
.build-fly-left {
|
|
437
437
|
animation: build-fly-in-left 0.4s ease;
|
|
438
438
|
}
|
|
439
439
|
|
|
@@ -443,7 +443,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
443
443
|
to { transform: translateX(0); opacity: 1; }
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
|
|
446
|
+
.build-fly-right {
|
|
447
447
|
animation: build-fly-in-right 0.4s ease;
|
|
448
448
|
}
|
|
449
449
|
|
|
@@ -453,7 +453,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
453
453
|
to { transform: translateY(0); opacity: 1; }
|
|
454
454
|
}
|
|
455
455
|
|
|
456
|
-
|
|
456
|
+
.build-fly-up {
|
|
457
457
|
animation: build-fly-in-up 0.4s ease;
|
|
458
458
|
}
|
|
459
459
|
|
|
@@ -463,7 +463,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
463
463
|
to { transform: translateY(0); opacity: 1; }
|
|
464
464
|
}
|
|
465
465
|
|
|
466
|
-
|
|
466
|
+
.build-fly-down {
|
|
467
467
|
animation: build-fly-in-down 0.4s ease;
|
|
468
468
|
}
|
|
469
469
|
|
|
@@ -473,7 +473,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
|
|
|
473
473
|
to { transform: scale(1); opacity: 1; }
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
-
|
|
476
|
+
.build-scale {
|
|
477
477
|
animation: build-scale-in 0.4s ease;
|
|
478
478
|
}
|
|
479
479
|
|
data/public/application.js
CHANGED
|
@@ -68,23 +68,28 @@ function applyCodeFocus() {
|
|
|
68
68
|
|
|
69
69
|
// Run the script for a single slide element.
|
|
70
70
|
// Wrapped in try/catch so syntax errors don't crash the presentation.
|
|
71
|
+
// Passes a tracked setTimeout so pending timeouts can be cancelled on slide change.
|
|
71
72
|
function runScript(slideEl) {
|
|
72
73
|
const scriptEl = slideEl.querySelector('script[type="text/slide-script"]');
|
|
73
74
|
if (!scriptEl) return;
|
|
74
75
|
|
|
75
76
|
const container = slideEl.querySelector('.slide-body') ?? slideEl;
|
|
76
77
|
const slide = new Slide(container);
|
|
78
|
+
currentSlides.push(slide);
|
|
77
79
|
|
|
78
80
|
try {
|
|
79
|
-
const fn = new Function('slide', scriptEl.textContent);
|
|
80
|
-
fn(slide);
|
|
81
|
+
const fn = new Function('slide', 'setTimeout', scriptEl.textContent);
|
|
82
|
+
fn(slide, slide.setTimeout.bind(slide));
|
|
81
83
|
} catch (error) {
|
|
82
84
|
console.error('Slide script error:', error);
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
// Run scripts for all slide elements currently in the DOM.
|
|
89
|
+
// Cancels any pending timeouts from the previous slide's scripts first.
|
|
87
90
|
function runSlideScripts() {
|
|
91
|
+
currentSlides.forEach(slide => slide.cancelTimeouts());
|
|
92
|
+
currentSlides = [];
|
|
88
93
|
document.querySelectorAll('.slide').forEach(runScript);
|
|
89
94
|
}
|
|
90
95
|
|
|
@@ -98,6 +103,9 @@ function detectTransition(html) {
|
|
|
98
103
|
// Track the active view transition so we can skip overlapping ones.
|
|
99
104
|
let activeTransition = null;
|
|
100
105
|
|
|
106
|
+
// Track Slide instances from the current scripts so we can cancel their timeouts on slide change.
|
|
107
|
+
let currentSlides = [];
|
|
108
|
+
|
|
101
109
|
// Wrap Live's update method to support view transitions.
|
|
102
110
|
const originalUpdate = live.update.bind(live);
|
|
103
111
|
live.update = function(id, html, options) {
|
data/public/slide.js
CHANGED
|
@@ -1,44 +1,178 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// Stateful builder for a set of slide elements.
|
|
2
|
+
// Wraps a raw element array with a cached position so callers can use next()
|
|
3
|
+
// instead of tracking count manually. Created via SlideElements#builder(options).
|
|
4
|
+
export class SlideBuilder {
|
|
5
|
+
#elements;
|
|
6
|
+
#prefix;
|
|
7
|
+
#defaultEffect;
|
|
8
|
+
#slide;
|
|
9
|
+
#step = 0;
|
|
10
|
+
|
|
11
|
+
constructor(slide, elements, options = {}) {
|
|
12
|
+
this.#slide = slide;
|
|
13
|
+
this.#elements = elements;
|
|
14
|
+
this.#prefix = options.group || 'build';
|
|
15
|
+
this.#defaultEffect = options.effect || null;
|
|
6
16
|
}
|
|
7
17
|
|
|
8
|
-
//
|
|
9
|
-
// Assigns view-transition-names and applies
|
|
10
|
-
// @parameter
|
|
11
|
-
// @parameter
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
// Reveal elements up to `count`, using the default effect unless overridden.
|
|
19
|
+
// Assigns view-transition-names, sets visibility, and applies entry animation.
|
|
20
|
+
// @parameter count [Integer] Number of elements to show.
|
|
21
|
+
// @parameter overrides [Object] Option overrides for this step (e.g. a different effect).
|
|
22
|
+
// @returns [Promise] Resolves when the animation completes (or immediately if no effect).
|
|
23
|
+
show(count, overrides = {}) {
|
|
24
|
+
const effect = overrides.effect !== undefined ? overrides.effect : this.#defaultEffect;
|
|
25
|
+
let revealedElement = null;
|
|
16
26
|
|
|
17
|
-
this.
|
|
18
|
-
element.style.viewTransitionName = `${prefix}-${index + 1}`;
|
|
27
|
+
this.#elements.forEach((element, index) => {
|
|
28
|
+
element.style.viewTransitionName = `${this.#prefix}-${index + 1}`;
|
|
19
29
|
|
|
20
|
-
if (index <
|
|
30
|
+
if (index < count) {
|
|
21
31
|
element.style.visibility = 'visible';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
element.style.viewTransitionClass = '';
|
|
33
|
+
|
|
34
|
+
if (index === count - 1 && effect) {
|
|
35
|
+
element.classList.add(`build-${effect}`);
|
|
36
|
+
revealedElement = element;
|
|
37
|
+
}
|
|
27
38
|
} else {
|
|
28
39
|
element.style.visibility = 'hidden';
|
|
29
|
-
//
|
|
30
|
-
//
|
|
40
|
+
// Keep viewTransitionClass set so morph transitions can suppress
|
|
41
|
+
// crossfading on hidden elements when called inside startViewTransition.
|
|
31
42
|
element.style.viewTransitionClass = 'build-hidden';
|
|
32
43
|
}
|
|
33
44
|
});
|
|
45
|
+
|
|
46
|
+
this.#step = count;
|
|
47
|
+
|
|
48
|
+
if (revealedElement) {
|
|
49
|
+
const animationClass = `build-${effect}`;
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
revealedElement.addEventListener('animationend', () => {
|
|
52
|
+
revealedElement.classList.remove(animationClass);
|
|
53
|
+
resolve();
|
|
54
|
+
}, {once: true});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reveal the next element. Only touches the single newly revealed element —
|
|
62
|
+
// all others are already in the correct state from the previous call.
|
|
63
|
+
// @parameter overrides [Object] Option overrides for this step.
|
|
64
|
+
// @returns [Promise]
|
|
65
|
+
next(overrides = {}) {
|
|
66
|
+
if (this.finished) return Promise.resolve();
|
|
67
|
+
|
|
68
|
+
const effect = overrides.effect !== undefined ? overrides.effect : this.#defaultEffect;
|
|
69
|
+
const element = this.#elements[this.#step];
|
|
70
|
+
|
|
71
|
+
element.style.viewTransitionName = `${this.#prefix}-${this.#step + 1}`;
|
|
72
|
+
element.style.visibility = 'visible';
|
|
73
|
+
element.style.viewTransitionClass = '';
|
|
74
|
+
|
|
75
|
+
this.#step += 1;
|
|
76
|
+
|
|
77
|
+
if (effect) {
|
|
78
|
+
const animationClass = `build-${effect}`;
|
|
79
|
+
element.classList.add(animationClass);
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
element.addEventListener('animationend', () => {
|
|
82
|
+
element.classList.remove(animationClass);
|
|
83
|
+
resolve();
|
|
84
|
+
}, {once: true});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Promise.resolve();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Reveal all remaining elements in sequence, with `interval` milliseconds between each.
|
|
92
|
+
// An optional callback is invoked after each reveal — if it returns false, playback stops.
|
|
93
|
+
// Requires the builder to have been created via slide.find(...).builder() so that
|
|
94
|
+
// the timeouts are tracked and cancelled on slide change.
|
|
95
|
+
// @parameter interval [Number] Delay in milliseconds between each reveal.
|
|
96
|
+
// @parameter callback [Function | null] Optional. Receives the builder after each next().
|
|
97
|
+
// Return false to stop playback early.
|
|
98
|
+
play(interval, callback = null) {
|
|
99
|
+
if (this.finished) return;
|
|
100
|
+
|
|
101
|
+
const playNext = () => {
|
|
102
|
+
this.next();
|
|
103
|
+
const shouldContinue = callback ? callback(this) !== false : true;
|
|
104
|
+
if (!this.finished && shouldContinue) {
|
|
105
|
+
this.#slide.setTimeout(playNext, interval);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
this.#slide.setTimeout(playNext, interval);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Returns true when all elements have been revealed.
|
|
113
|
+
get finished() {
|
|
114
|
+
return this.#step >= this.#elements.length;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Represents a collection of elements within a slide to be revealed sequentially.
|
|
119
|
+
// Has no side effects until show() is called.
|
|
120
|
+
export class SlideElements {
|
|
121
|
+
#elements;
|
|
122
|
+
#slide;
|
|
123
|
+
|
|
124
|
+
constructor(slide, elements) {
|
|
125
|
+
this.#slide = slide;
|
|
126
|
+
this.#elements = elements;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Create a stateful SlideBuilder for this element collection with default options.
|
|
130
|
+
// @parameter options [Object] Default options applied to every show() / next() call.
|
|
131
|
+
// group: prefix for view-transition-name (default: "build")
|
|
132
|
+
// effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
|
|
133
|
+
// @returns [SlideBuilder]
|
|
134
|
+
builder(options = {}) {
|
|
135
|
+
return new SlideBuilder(this.#slide, this.#elements, options);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Show the first `count` elements and hide the rest.
|
|
139
|
+
// Delegates to SlideBuilder for the actual implementation.
|
|
140
|
+
// @parameter count [Integer] Number of elements to show.
|
|
141
|
+
// @parameter options [Object]
|
|
142
|
+
// group: prefix for view-transition-name (default: "build")
|
|
143
|
+
// effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
|
|
144
|
+
// @returns [Promise] Resolves when the animation completes (or immediately if no effect).
|
|
145
|
+
show(count, options = {}) {
|
|
146
|
+
return new SlideBuilder(this.#slide, this.#elements, options).show(count);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Returned by Slide#after to enable relative delay chaining.
|
|
151
|
+
// Each .after(delay, callback) fires that many milliseconds after the previous step.
|
|
152
|
+
class SlideChain {
|
|
153
|
+
#slide;
|
|
154
|
+
#elapsed;
|
|
155
|
+
|
|
156
|
+
constructor(slide, elapsed) {
|
|
157
|
+
this.#slide = slide;
|
|
158
|
+
this.#elapsed = elapsed;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
after(delay, callback) {
|
|
162
|
+
this.#elapsed += delay;
|
|
163
|
+
this.#slide.setTimeout(callback, this.#elapsed);
|
|
164
|
+
return this;
|
|
34
165
|
}
|
|
35
166
|
}
|
|
36
167
|
|
|
37
168
|
// The scripting context passed to each slide's javascript block.
|
|
38
169
|
// Scopes element queries to the slide body.
|
|
39
170
|
export class Slide {
|
|
171
|
+
#container;
|
|
172
|
+
#timeouts = [];
|
|
173
|
+
|
|
40
174
|
constructor(container) {
|
|
41
|
-
this
|
|
175
|
+
this.#container = container;
|
|
42
176
|
}
|
|
43
177
|
|
|
44
178
|
// Find elements within this slide matching the given CSS selector.
|
|
@@ -46,7 +180,35 @@ export class Slide {
|
|
|
46
180
|
// @parameter selector [String] A CSS selector scoped to the slide body.
|
|
47
181
|
// @returns [SlideElements]
|
|
48
182
|
find(selector) {
|
|
49
|
-
const elements = Array.from(this.
|
|
50
|
-
return new SlideElements(elements);
|
|
183
|
+
const elements = Array.from(this.#container.querySelectorAll(selector));
|
|
184
|
+
return new SlideElements(this, elements);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Tracked setTimeout — use this in slide scripts instead of the global.
|
|
188
|
+
// Registered timeouts are automatically cancelled when the slide changes.
|
|
189
|
+
// @parameter callback [Function] The function to call after the delay.
|
|
190
|
+
// @parameter delay [Number] Delay in milliseconds.
|
|
191
|
+
// @returns [Number] The timeout ID.
|
|
192
|
+
setTimeout(callback, delay) {
|
|
193
|
+
const timeoutId = window.setTimeout(callback, delay);
|
|
194
|
+
this.#timeouts.push(timeoutId);
|
|
195
|
+
return timeoutId;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Schedule a callback after a delay, returning a chainable object so
|
|
199
|
+
// subsequent .after(delay) calls are relative to the previous step.
|
|
200
|
+
// All timeouts are tracked and cancelled automatically on slide change.
|
|
201
|
+
// @parameter delay [Number] Delay in milliseconds from now (or previous step).
|
|
202
|
+
// @parameter callback [Function] The function to call after the delay.
|
|
203
|
+
// @returns [SlideChain]
|
|
204
|
+
after(delay, callback) {
|
|
205
|
+
this.setTimeout(callback, delay);
|
|
206
|
+
return new SlideChain(this, delay);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Cancel all pending timeouts registered by this slide's script.
|
|
210
|
+
cancelTimeouts() {
|
|
211
|
+
this.#timeouts.forEach(timeoutId => clearTimeout(timeoutId));
|
|
212
|
+
this.#timeouts = [];
|
|
51
213
|
}
|
|
52
214
|
}
|
data/readme.md
CHANGED
|
@@ -29,6 +29,19 @@ Please see the [project documentation](https://socketry.github.io/presently/) fo
|
|
|
29
29
|
|
|
30
30
|
Please see the [project releases](https://socketry.github.io/presently/releases/index) for all releases.
|
|
31
31
|
|
|
32
|
+
### v0.7.0
|
|
33
|
+
|
|
34
|
+
- Rework build effects to use direct CSS class animation rather than `view-transition-class`. `build-fade`, `build-fly-up`, etc. are now regular `@keyframes` classes applied to the revealed element, rather than view transition pseudo-element selectors. This decouples in-slide sequential animation from slide-level morph transitions.
|
|
35
|
+
- Rename `SlideElements#build` to `SlideElements#show` for clarity — `boxes.show(0)` / `boxes.show(3)` more clearly describes the outcome from the audience's perspective.
|
|
36
|
+
- Add `SlideElements#builder(options)` — returns a `SlideBuilder` with default options (group, effect) and a cached position, so callers can use `next()` instead of tracking the step count manually.
|
|
37
|
+
- Add `SlideBuilder#show(count, overrides)` — set visibility state to an arbitrary position. Returns a `Promise` that resolves when the reveal animation completes (or immediately when no effect is given).
|
|
38
|
+
- Add `SlideBuilder#next(overrides)` — reveal the next element with the builder's default effect, optionally overridden per call. O(1): only touches the single newly revealed element. Returns a `Promise`.
|
|
39
|
+
- Add `SlideBuilder#play(interval, callback)` — reveals all remaining elements in sequence with `interval` milliseconds between each. An optional callback is invoked after each reveal; return `false` to stop playback early. Requires the builder to be created via `slide.find(...).builder()` so timeouts are tracked and cancelled on slide change.
|
|
40
|
+
- Add `SlideBuilder#finished` getter — returns `true` when all elements have been revealed.
|
|
41
|
+
- Add `Slide#after(delay, callback)` — schedules a callback after a delay in milliseconds and returns a `SlideChain`. Subsequent `.after(delay, callback)` calls on the chain fire relative to the previous step, making sequential reveal timing easy to read and adjust.
|
|
42
|
+
- Add `Slide#setTimeout(callback, delay)` — a tracked replacement for the global `setTimeout`. All timeouts registered this way are automatically cancelled when the slide changes, preventing stale callbacks from firing after navigation. The global `setTimeout` in slide scripts is shadowed by this method automatically.
|
|
43
|
+
- Add `Slide#cancelTimeouts()` — cancels all pending timeouts registered by the slide's script. Called automatically by the presentation engine on every slide change.
|
|
44
|
+
|
|
32
45
|
### v0.6.0
|
|
33
46
|
|
|
34
47
|
- Add `bake presently:slides:speakers` task to print a timing breakdown grouped by speaker. Each speaker's slides are listed in presentation order with individual and total durations, making it easy to balance talk time in multi-speaker presentations. Slides without a `speaker` key are grouped under `(no speaker)`.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.7.0
|
|
4
|
+
|
|
5
|
+
- Rework build effects to use direct CSS class animation rather than `view-transition-class`. `build-fade`, `build-fly-up`, etc. are now regular `@keyframes` classes applied to the revealed element, rather than view transition pseudo-element selectors. This decouples in-slide sequential animation from slide-level morph transitions.
|
|
6
|
+
- Rename `SlideElements#build` to `SlideElements#show` for clarity — `boxes.show(0)` / `boxes.show(3)` more clearly describes the outcome from the audience's perspective.
|
|
7
|
+
- Add `SlideElements#builder(options)` — returns a `SlideBuilder` with default options (group, effect) and a cached position, so callers can use `next()` instead of tracking the step count manually.
|
|
8
|
+
- Add `SlideBuilder#show(count, overrides)` — set visibility state to an arbitrary position. Returns a `Promise` that resolves when the reveal animation completes (or immediately when no effect is given).
|
|
9
|
+
- Add `SlideBuilder#next(overrides)` — reveal the next element with the builder's default effect, optionally overridden per call. O(1): only touches the single newly revealed element. Returns a `Promise`.
|
|
10
|
+
- Add `SlideBuilder#play(interval, callback)` — reveals all remaining elements in sequence with `interval` milliseconds between each. An optional callback is invoked after each reveal; return `false` to stop playback early. Requires the builder to be created via `slide.find(...).builder()` so timeouts are tracked and cancelled on slide change.
|
|
11
|
+
- Add `SlideBuilder#finished` getter — returns `true` when all elements have been revealed.
|
|
12
|
+
- Add `Slide#after(delay, callback)` — schedules a callback after a delay in milliseconds and returns a `SlideChain`. Subsequent `.after(delay, callback)` calls on the chain fire relative to the previous step, making sequential reveal timing easy to read and adjust.
|
|
13
|
+
- Add `Slide#setTimeout(callback, delay)` — a tracked replacement for the global `setTimeout`. All timeouts registered this way are automatically cancelled when the slide changes, preventing stale callbacks from firing after navigation. The global `setTimeout` in slide scripts is shadowed by this method automatically.
|
|
14
|
+
- Add `Slide#cancelTimeouts()` — cancels all pending timeouts registered by the slide's script. Called automatically by the presentation engine on every slide change.
|
|
15
|
+
|
|
3
16
|
## v0.6.0
|
|
4
17
|
|
|
5
18
|
- Add `bake presently:slides:speakers` task to print a timing breakdown grouped by speaker. Each speaker's slides are listed in presentation order with individual and total durations, making it easy to balance talk time in multi-speaker presentations. Slides without a `speaker` key are grouped under `(no speaker)`.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|