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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 806fa6100505250fcb875b1d751bd96df78108d835bb60ad65c0484e0850e296
4
- data.tar.gz: 723cdf3db680519971054969cff462752b85b7df67cab51c0bc50f8585c00890
3
+ metadata.gz: e5c00d36415375cd15afef5fbc6b2669200b981ebb8744ccd5f04d770ea71877
4
+ data.tar.gz: b154f921b651b3572b307b529cc6b630601ccabce70a8a06e089bada66a211e8
5
5
  SHA512:
6
- metadata.gz: 12e369d511548176c671fbd42718cd136d3d628f173d0c6901f36c805fa87612067765ca1a29d808ec806b223c11ce2f2ea0c59d879b6eb97fa13e27828fd935
7
- data.tar.gz: 10b3df45c9ed1e72e2dd01bae21daef286cea851e2f5630f7684b0de32771295bbce64a05ee81a028d20992ccb5235151277d2c3515df8cb469253d78d5b2a30
6
+ metadata.gz: b28384ce74c14c486da7fa1d80a81fd6b2b3a386f8ceac6e2a0ffbf6f6a5aa39325a31550badf722446a772ebf2cc11071eec354ec0d33b8f5bc1912b518726c
7
+ data.tar.gz: c555c644843daa56b0366575d14c3d85e7dd358bfa897654c47fd3b291c8a6ae14f0764af13139d54af4fced6cb46acc2aa38fd74bd3ee5f0b62ab377b242606
checksums.yaml.gz.sig CHANGED
Binary file
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Presently
8
- VERSION = "0.6.0"
8
+ VERSION = "0.7.0"
9
9
  end
@@ -423,7 +423,7 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
423
423
  }
424
424
 
425
425
  /* Fade */
426
- ::view-transition-new(.build-fade) {
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
- ::view-transition-new(.build-fly-left) {
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
- ::view-transition-new(.build-fly-right) {
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
- ::view-transition-new(.build-fly-up) {
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
- ::view-transition-new(.build-fly-down) {
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
- ::view-transition-new(.build-scale) {
476
+ .build-scale {
477
477
  animation: build-scale-in 0.4s ease;
478
478
  }
479
479
 
@@ -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
- // Represents a collection of elements within a slide to be revealed sequentially.
2
- // Has no side effects until build() is called.
3
- export class SlideElements {
4
- constructor(elements) {
5
- this._elements = elements;
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
- // Show the first n elements and hide the rest.
9
- // Assigns view-transition-names and applies build visibility in one step.
10
- // @parameter n [Integer] Number of elements to show.
11
- // @parameter options [Object]
12
- // group: prefix for view-transition-name (default: "build")
13
- // effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
14
- build(n, options = {}) {
15
- const prefix = options.group || 'build';
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._elements.forEach((element, index) => {
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 < n) {
30
+ if (index < count) {
21
31
  element.style.visibility = 'visible';
22
- // Newly revealed element: apply the enter effect.
23
- // Already-visible elements: clear any class so they morph normally.
24
- element.style.viewTransitionClass = (index === n - 1 && options.effect)
25
- ? `build-${options.effect}`
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
- // Hidden elements: suppress both pseudo-elements so they don't
30
- // crossfade in or out during the transition.
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._container = container;
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._container.querySelectorAll(selector));
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
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: presently
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
metadata.gz.sig CHANGED
Binary file