presently 0.14.1 → 0.16.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: de6eb2668cd67cd34eb8f02c51dce3bc3f823f71b8e553835d27c2476d3f793e
4
- data.tar.gz: 37601886d71cc24f89510ed65d918dbb87bcd1de1d0c48718f96b15820790e23
3
+ metadata.gz: 3c2eaa843e93a1ef3f3c97b0e89c80a3ee4484ddbee4c105ca39bc7c324671ea
4
+ data.tar.gz: bc27aa664ca531b38da8b807e3fcb438a41c4338a180c1ddd39fc1daa0c549ed
5
5
  SHA512:
6
- metadata.gz: 5d272168a8f3de208c69e56fb44083c2417987d5178afc6e021f9ae4c9c2ddd5985f3251637581e21aea109e9698778c147865baece48bd8a6cb7dd652c7c888
7
- data.tar.gz: 1d30ea352a266fcf97def9fdba931adb9cec25933f74bebbac85ecb53e35ceaee46c2faebdcbde473bb853c2959e135a6a2b0b5309eaff91646a632f50ddc043
6
+ metadata.gz: 98959ac207c29fa8a4bf444e0d1e476e584183615d04abed09c8f74cda3a338ed61f9517f67d5e7e5bb40aa624c352100bf1c2f36c61663a81f61ec509fe52d8
7
+ data.tar.gz: 30ca2a30e72d1f0c0dbc71b31786349ecd8d8c70eae5d69f29e701e77cd2f39487909a71a4699b2be9b70169cc170065313da35e46394ab92d0b378f3849e38a
checksums.yaml.gz.sig CHANGED
Binary file
@@ -50,12 +50,15 @@ def pdf(output: "presentation.pdf", slides_root: "slides", notes: true, speaker:
50
50
  begin
51
51
  environment = Async::Service::Environment.build(
52
52
  Presently::Environment::Application,
53
- root: context.root,
54
- slides_root: File.expand_path(slides_root, context.root),
53
+ root: Dir.pwd,
54
+ slides_root: slides_root,
55
55
  endpoint: Async::HTTP::Endpoint.parse("http://localhost", bound_endpoint)
56
56
  )
57
57
 
58
- evaluator = environment.evaluator
58
+ # PDF export always requires an HTTP server, regardless of the configured
59
+ # Lively transport:
60
+ transport = environment.with(environment.evaluator.http_environment)
61
+ evaluator = transport.evaluator
59
62
  server = evaluator.make_server(evaluator.endpoint)
60
63
  server_task = task.async{server.run}
61
64
 
@@ -21,8 +21,8 @@ end
21
21
  #
22
22
  # @parameter slides_root [String] The slides directory. Default: `slides`.
23
23
  # @parameter speaker [String | Nil] Only rehearse slides for this speaker.
24
- # @parameter from [String | Nil] Substring match — start at the first slide whose filename contains this.
25
- # @parameter to [String | Nil] Substring match — stop after the first slide whose filename contains this.
24
+ # @parameter from [String | Nil] Substring match — start at the first slide whose relative path contains this.
25
+ # @parameter to [String | Nil] Substring match — stop after the first slide whose relative path contains this.
26
26
  # @parameter resume [Boolean] Resume from the slide after the last one recorded in the log. Keeps prior records and appends new ones.
27
27
  # @parameter log [String] Path to write the JSON log. Default: `tmp/rehearsal.json`.
28
28
  def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: false, log: "tmp/rehearsal.json")
@@ -31,12 +31,12 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
31
31
  slides = slides.select{|slide| slide.speaker == speaker} if speaker
32
32
 
33
33
  if from
34
- start_index = slides.index{|slide| File.basename(slide.path).include?(from)}
34
+ start_index = slides.index{|slide| slide.path.include?(from)}
35
35
  slides = slides[start_index..] if start_index
36
36
  end
37
37
 
38
38
  if to
39
- end_index = slides.index{|slide| File.basename(slide.path).include?(to)}
39
+ end_index = slides.index{|slide| slide.path.include?(to)}
40
40
  slides = slides[..end_index] if end_index
41
41
  end
42
42
 
@@ -49,7 +49,7 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
49
49
 
50
50
  prior_records = JSON.parse(File.read(log), symbolize_names: true)
51
51
  last_path = prior_records.last&.dig(:path)
52
- resume_index = slides.index{|slide| File.basename(slide.path) == last_path}
52
+ resume_index = slides.index{|slide| slide.path == last_path}
53
53
 
54
54
  if resume_index.nil?
55
55
  puts "Last rehearsed slide (#{last_path}) not found in current slide set. Nothing to resume."
@@ -81,14 +81,14 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
81
81
  records = []
82
82
  offset = prior_records.length
83
83
  slides.each_with_index do |slide, index|
84
- basename = File.basename(slide.path)
84
+ relative_path = slide.path
85
85
  headline = extract_headline(slide)
86
86
  absolute = offset + index + 1
87
87
  total = offset + slides.length
88
88
 
89
89
  puts
90
90
  puts "─" * 72
91
- puts "[#{absolute}/#{total}] #{basename} planned: #{format_duration(slide.duration)} speaker: #{slide.speaker || "—"}"
91
+ puts "[#{absolute}/#{total}] #{relative_path} planned: #{format_duration(slide.duration)} speaker: #{slide.speaker || "—"}"
92
92
  puts " #{headline}" unless headline.empty?
93
93
  if slide.notes
94
94
  notes = slide.notes.to_commonmark.strip
@@ -105,7 +105,7 @@ def rehearse(slides_root: "slides", speaker: nil, from: nil, to: nil, resume: fa
105
105
 
106
106
  records << {
107
107
  index: absolute,
108
- path: basename,
108
+ path: relative_path,
109
109
  speaker: slide.speaker,
110
110
  planned: slide.duration,
111
111
  actual: elapsed.round(1),
@@ -58,7 +58,7 @@ def speakers(slides_root: "slides")
58
58
  puts "#{speaker} — #{format_duration(total_seconds)}"
59
59
 
60
60
  slides.each do |slide|
61
- puts " #{format_duration(slide.duration)} #{slide.title} (#{File.basename(slide.path)})"
61
+ puts " #{format_duration(slide.duration)} #{slide.title} (#{slide.path})"
62
62
  end
63
63
 
64
64
  puts
@@ -69,9 +69,10 @@ end
69
69
 
70
70
  # Renumber slide files sequentially with a consistent step size.
71
71
  #
72
- # Renames all `.md` files in the slides directory to have sequential
72
+ # Renames all `.md` files directly in the given directory to have sequential
73
73
  # numeric prefixes (010, 020, 030, ...), preserving their current order.
74
- # The descriptive part of the filename (after the number prefix) is kept.
74
+ # The descriptive part of the filename (after the number prefix) is kept. It
75
+ # does not recurse; pass a nested directory as `slides_root` to renumber it.
75
76
  #
76
77
  # @parameter slides_root [String] The slides directory. Default: `slides`.
77
78
  # @parameter step [Integer] The step between slide numbers. Default: `10`.
@@ -1,14 +1,6 @@
1
1
  # Animating Slides
2
2
 
3
- This guide explains how to animate content within slides using the `morph` transition and the slide scripting system.
4
-
5
- ## How Morph Works
6
-
7
- The `morph` transition uses the browser's View Transitions API. When navigating between two slides, any element that has a `view-transition-name` style on both the old and new slide is matched — the browser captures its position and appearance in both states and animates between them.
8
-
9
- Elements without a matching name crossfade. The slide background stays completely still (no container animation).
10
-
11
- This is the mechanism that makes build sequences possible: the same element appears on consecutive slides with the same name, so it stays pinned in place while hidden elements appear around it.
3
+ This guide explains how to animate content within slides using the slide scripting system.
12
4
 
13
5
  ## Slide Scripts
14
6
 
@@ -18,7 +10,6 @@ Any slide can include a JavaScript block at the end of its presenter notes secti
18
10
  ---
19
11
  template: default
20
12
  duration: 30
21
- transition: morph
22
13
  ---
23
14
 
24
15
  - First point
@@ -30,7 +21,7 @@ transition: morph
30
21
  Your presenter notes here.
31
22
 
32
23
  ```javascript
33
- slide.find("li").show(1, {group: "bullet"})
24
+ slide.find("li").show(1)
34
25
  ```
35
26
  ~~~
36
27
 
@@ -52,7 +43,7 @@ slide.find(".callout") // elements with a specific class
52
43
 
53
44
  ### `elements.show(n, options)`
54
45
 
55
- Shows the first `n` elements in the collection and hides the rest. Assigns `view-transition-name` to each element so the morph transition can match them across consecutive slides. Returns a `Promise` that resolves when any reveal animation completes.
46
+ Shows the first `n` elements in the collection and hides the rest. Returns a `Promise` that resolves when any reveal animation completes.
56
47
 
57
48
  ``` javascript
58
49
  slide.find("li").show(0) // all hidden
@@ -64,7 +55,6 @@ Options:
64
55
 
65
56
  | Option | Description |
66
57
  |---|---|
67
- | `group` | Name prefix for `view-transition-name` — must be consistent across slides for morph to match elements. Defaults to `"build"`. |
68
58
  | `effect` | Entry animation for the newly revealed element. See effects below. |
69
59
 
70
60
  ### `elements.builder(options)`
@@ -72,7 +62,7 @@ Options:
72
62
  Creates a `SlideBuilder` with default options and a cached position. Use this instead of calling `show()` manually when you want to reveal elements one at a time from a script.
73
63
 
74
64
  ``` javascript
75
- const bullets = slide.find("li").builder({group: "bullet", effect: "fly-up"})
65
+ const bullets = slide.find("li").builder({effect: "fly-up"})
76
66
  bullets.show(0) // hide all initially
77
67
  bullets.next() // reveal first, plays fly-up
78
68
  bullets.next() // reveal second, plays fly-up
@@ -115,7 +105,7 @@ A build sequence is a series of consecutive slides with the same content, each r
115
105
  ---
116
106
  template: default
117
107
  duration: 20
118
- transition: morph
108
+ transition: fade
119
109
  ---
120
110
 
121
111
  - Real-time synchronization
@@ -127,7 +117,7 @@ transition: morph
127
117
  Let's walk through the key features.
128
118
 
129
119
  ```javascript
130
- slide.find("li").show(0, {group: "bullet"})
120
+ slide.find("li").show(0)
131
121
  ```
132
122
  ~~~
133
123
 
@@ -136,7 +126,7 @@ slide.find("li").show(0, {group: "bullet"})
136
126
  ---
137
127
  template: default
138
128
  duration: 20
139
- transition: morph
129
+ transition: fade
140
130
  ---
141
131
 
142
132
  - Real-time synchronization
@@ -148,12 +138,10 @@ transition: morph
148
138
  The display and presenter stay in sync over a WebSocket connection.
149
139
 
150
140
  ```javascript
151
- slide.find("li").show(1, {group: "bullet"})
141
+ slide.find("li").show(1)
152
142
  ```
153
143
  ~~~
154
144
 
155
- The `group` option must be identical across all slides in the sequence so the browser matches the same elements. Without it, each slide uses the default `"build"` prefix — which is fine as long as only one build sequence is active per slide.
156
-
157
145
  Because all elements are in the DOM from the start (just hidden), the vertical layout stays consistent throughout the sequence — there is no shift as elements appear.
158
146
 
159
147
  ## Build Effects
@@ -161,7 +149,7 @@ Because all elements are in the DOM from the start (just hidden), the vertical l
161
149
  Pass an `effect` option to animate the newly revealed element as it appears. The effect plays as a CSS animation on the element and is removed automatically once it completes.
162
150
 
163
151
  ``` javascript
164
- slide.find("li").show(2, {group: "bullet", effect: "fly-up"})
152
+ slide.find("li").show(2, {effect: "fly-up"})
165
153
  ```
166
154
 
167
155
  Available effects:
@@ -181,8 +169,8 @@ A slide can have multiple independent build groups. Each `find().show()` call is
181
169
 
182
170
  ``` javascript
183
171
  // Reveal list items as one group, callout div as another
184
- slide.find("li").show(3, {group: "bullet"})
185
- slide.find(".callout").show(1, {group: "callout", effect: "fly-up"})
172
+ slide.find("li").show(3)
173
+ slide.find(".callout").show(1, {effect: "fly-up"})
186
174
  ```
187
175
 
188
176
  ## In-Slide Animation with `slide.after()`
@@ -190,8 +178,8 @@ slide.find(".callout").show(1, {group: "callout", effect: "fly-up"})
190
178
  For sequential reveals within a single slide (without navigating to the next slide), use `slide.after()`. Each step fires a delay in milliseconds relative to the previous step. Returns a `SlideContext` so subsequent `.after()` calls chain naturally.
191
179
 
192
180
  ``` javascript
193
- const panes = slide.find(".pane").builder({group: "pane", effect: "fade"})
194
- const items = slide.find(".item").builder({group: "item", effect: "fly-up"})
181
+ const panes = slide.find(".pane").builder({effect: "fade"})
182
+ const items = slide.find(".item").builder({effect: "fly-up"})
195
183
  panes.show(0)
196
184
  items.show(0)
197
185
 
@@ -32,7 +32,27 @@ $ mkdir slides
32
32
 
33
33
  ### Writing Slides
34
34
 
35
- Each slide is a Markdown file in the `slides/` directory. Files are ordered alphabetically, so prefix them with numbers:
35
+ Each slide is a Markdown file in the `slides/` directory. Files are ordered by path, and every directory and slide filename must begin with a numeric prefix:
36
+
37
+ ``` text
38
+ slides/
39
+ ├── 010-welcome.md
40
+ ├── 020-performance/
41
+ │ ├── 010-introduction.md
42
+ │ └── 020-results.md
43
+ └── 030-conclusion.md
44
+ ```
45
+
46
+ This allows related slides to be grouped in nested directories while retaining an unambiguous presentation order. Markdown files with any unnumbered path component, such as `slides/shared/example.md`, are not treated as slides and can be used for included content.
47
+
48
+ The `presently:slides:renumber` task only renumbers files directly inside the selected directory. Run it without arguments for the top-level slides, or pass a nested directory explicitly:
49
+
50
+ ``` shell
51
+ $ bake presently:slides:renumber
52
+ $ bake presently:slides:renumber slides_root=slides/020-performance
53
+ ```
54
+
55
+ A slide file contains Markdown with optional frontmatter and presenter notes:
36
56
 
37
57
  ``` markdown
38
58
  ---
@@ -263,9 +283,6 @@ Available transitions:
263
283
  | `fade` | Crossfade between slides |
264
284
  | `slide-left` | Current slide exits left, next enters from right |
265
285
  | `slide-right` | Current slide exits right, next enters from left |
266
- | `morph` | Matched elements animate between positions; everything else crossfades |
267
-
268
- The `morph` transition uses the browser's View Transitions API to smoothly animate individual elements between slides. Elements with the same `view-transition-name` across two consecutive slides are matched and interpolated.
269
286
 
270
287
  ## Presenter Notes
271
288
 
data/context/index.yaml CHANGED
@@ -13,4 +13,4 @@ files:
13
13
  - path: animating-slides.md
14
14
  title: Animating Slides
15
15
  description: This guide explains how to animate content within slides using the
16
- `morph` transition and the slide scripting system.
16
+ slide scripting system.
@@ -49,7 +49,7 @@ module Presently
49
49
  def controller
50
50
  @controller ||= begin
51
51
  templates = Templates.for(@templates_roots)
52
- presentation = Presentation.load(@slides_root, templates: templates)
52
+ presentation = Presentation.load(@slides_root, templates)
53
53
 
54
54
  PresentationController.new(presentation, state: State.new)
55
55
  end
@@ -81,7 +81,7 @@ module Presently
81
81
 
82
82
  if path == "/export"
83
83
  options = Export.options_from_query(query)
84
- presentation = Presentation.load(@slides_root, templates: controller.templates)
84
+ presentation = Presentation.load(@slides_root, controller.templates)
85
85
  export = Export.new(presentation: presentation, **options)
86
86
  return Protocol::HTTP::Response[200, [["content-type", "text/html"]], [export.call]]
87
87
  end
@@ -20,8 +20,16 @@ module Presently
20
20
  PageSize = Struct.new(:slide_width_px, :slide_height_px, :notes_height_px, keyword_init: true) do
21
21
  PX_PER_CM = 96.0 / 2.54
22
22
 
23
+ # Convert the slide width from CSS pixels to centimetres.
24
+ # @returns [Float] The slide width in centimetres.
23
25
  def slide_width_cm = (slide_width_px / PX_PER_CM).round(4)
26
+
27
+ # Convert the slide height from CSS pixels to centimetres.
28
+ # @returns [Float] The slide height in centimetres.
24
29
  def slide_height_cm = (slide_height_px / PX_PER_CM).round(4)
30
+
31
+ # Convert the notes panel height from CSS pixels to centimetres.
32
+ # @returns [Float] The notes panel height in centimetres.
25
33
  def notes_height_cm = (notes_height_px / PX_PER_CM).round(4)
26
34
  end
27
35
 
@@ -98,7 +106,7 @@ module Presently
98
106
  end
99
107
 
100
108
  builder.tag(:span, class: "export-filename") do
101
- builder.tag(:code){builder.text(File.basename(slide.path))}
109
+ builder.tag(:code){builder.text(slide.path)}
102
110
  end
103
111
 
104
112
  if @timing
@@ -9,40 +9,33 @@ require_relative "templates"
9
9
  module Presently
10
10
  # An immutable collection of slides with configuration.
11
11
  #
12
- # Use {.load} to create a presentation from a directory of Markdown files,
13
- # or initialize directly with an array of {Slide} instances.
12
+ # Use {.load} to create a presentation from a directory of Markdown files.
13
+ # Each loaded {Slide} is attached to its presentation and identified by a
14
+ # path relative to the presentation root.
14
15
  class Presentation
15
- # Load and sort slide files from a directory.
16
- # @parameter slides_root [String] The directory containing `.md` slide files.
17
- # @returns [Array(Slide)] The loaded, sorted, non-skipped slides.
18
- def self.slides_from(slides_root)
19
- Dir.glob(File.join(slides_root, "*.md")).sort.map{|path| Slide.load(path)}.reject(&:skip?)
20
- end
21
-
22
16
  # Load a presentation from a directory of Markdown slide files.
23
- # @parameter slides_root [String] The directory containing `.md` slide files.
24
- # @parameter options [Hash] Additional options passed to {#initialize}.
17
+ # @parameter root [String] The directory containing `.md` slide files.
18
+ # @parameter templates [Templates] The template resolver for loading slide templates.
25
19
  # @returns [Presentation] A new presentation with slides loaded from the directory.
26
- def self.load(slides_root = "slides", **options)
27
- new(slides_from(slides_root), slides_root: slides_root, **options)
20
+ def self.load(root = "slides", templates = Templates.for)
21
+ new(root, templates)
28
22
  end
29
23
 
30
24
  # Initialize a new presentation.
31
- # @parameter slides [Array(Slide)] The ordered list of slides.
32
- # @parameter slides_root [String | Nil] The directory slides were loaded from, used by {#reload}.
25
+ # @parameter root [String] The root directory containing slide files.
33
26
  # @parameter templates [Templates] The template resolver for loading slide templates.
34
- def initialize(slides = [], slides_root: nil, templates: Templates.for)
35
- @slides = slides
36
- @slides_root = slides_root
27
+ def initialize(root = "slides", templates = Templates.for)
28
+ @root = File.expand_path(root)
37
29
  @templates = templates
30
+ @slides = load_slides
38
31
  end
39
32
 
33
+ # @attribute [String] The absolute root directory containing slide files.
34
+ attr :root
35
+
40
36
  # @attribute [Array(Slide)] The ordered list of slides.
41
37
  attr :slides
42
38
 
43
- # @attribute [String | Nil] The directory slides were loaded from.
44
- attr :slides_root
45
-
46
39
  # @attribute [Templates] The template resolver.
47
40
  attr :templates
48
41
 
@@ -66,12 +59,23 @@ module Presently
66
59
  end
67
60
 
68
61
  # Return a new {Presentation} with freshly loaded slides and a cleared template cache.
69
- # Only works if the presentation was created with {.load}.
70
- # @returns [Presentation] A new presentation instance, or `self` if no slides root is set.
62
+ # @returns [Presentation] A new presentation instance.
71
63
  def reload
72
- return self unless @slides_root
73
-
74
- self.class.new(self.class.slides_from(@slides_root), slides_root: @slides_root, templates: @templates.reload)
64
+ self.class.new(@root, @templates.reload)
65
+ end
66
+
67
+ private
68
+
69
+ # Load slides recursively in relative path order.
70
+ # @returns [Array(Slide)] The loaded, sorted, non-skipped slides.
71
+ def load_slides
72
+ Dir.glob("**/*.md", base: @root).sort.filter_map do |path|
73
+ components = path.split(File::SEPARATOR)
74
+ next unless components.all?{|component| component.match?(/\A\d/)}
75
+
76
+ slide = Slide.load(self, path)
77
+ slide unless slide.skip?
78
+ end
75
79
  end
76
80
  end
77
81
  end
@@ -204,10 +204,10 @@ module Presently
204
204
  if slide
205
205
  builder.text(" · ")
206
206
  builder.tag(:code, class: "slide-path") do
207
- builder.text(File.basename(slide.path))
207
+ builder.text(slide.path)
208
208
  end
209
209
 
210
- if editor_url = editor_url_for(slide.path)
210
+ if editor_url = editor_url_for(slide.source_path)
211
211
  builder.tag(:a, href: editor_url, class: "edit-link") do
212
212
  builder.text("✎")
213
213
  end
@@ -61,15 +61,17 @@ module Presently
61
61
  module_function
62
62
 
63
63
  # Parse the file and return a {Slide}.
64
- # @parameter path [String] The file path to parse.
64
+ # @parameter presentation [Presentation] The presentation which owns the slide.
65
+ # @parameter path [String] The slide path relative to the presentation root.
65
66
  # @returns [Slide]
66
- def load(path)
67
- raw = File.read(path)
67
+ def load(presentation, path)
68
+ source_path = File.join(presentation.root, path)
69
+ raw = File.read(source_path)
68
70
 
69
71
  # Parse once, with native front matter support.
70
72
  document = Markly.parse(raw, flags: Markly::UNSAFE | Markly::FRONT_MATTER, extensions: Fragment::EXTENSIONS)
71
73
 
72
- expand_includes!(document, File.dirname(path))
74
+ expand_includes!(document, File.dirname(source_path))
73
75
 
74
76
  # Extract front matter from the first AST node if present.
75
77
  front_matter = nil
@@ -111,7 +113,7 @@ module Presently
111
113
  script = nil
112
114
  end
113
115
 
114
- Slide.new(path, front_matter: front_matter, content: content, notes: notes, script: script)
116
+ Slide.new(presentation, path, front_matter: front_matter, content: content, notes: notes, script: script)
115
117
  end
116
118
 
117
119
  # Expand `![[path/to/file.md]]` include directives in a parsed document.
@@ -183,19 +185,22 @@ module Presently
183
185
  end
184
186
 
185
187
  # Load and parse a slide from a Markdown file.
186
- # @parameter path [String] The file path to the Markdown slide.
188
+ # @parameter presentation [Presentation] The presentation which owns the slide.
189
+ # @parameter path [String] The slide path relative to the presentation root.
187
190
  # @returns [Slide]
188
- def self.load(path)
189
- Parser.load(path)
191
+ def self.load(presentation, path)
192
+ Parser.load(presentation, path)
190
193
  end
191
194
 
192
195
  # Initialize a slide with pre-parsed data.
193
- # @parameter path [String] The file path of the slide.
196
+ # @parameter presentation [Presentation] The presentation which owns the slide.
197
+ # @parameter path [String] The slide path relative to the presentation root.
194
198
  # @parameter front_matter [Hash | Nil] The parsed YAML front_matter.
195
199
  # @parameter content [Hash(String, Fragment)] Content sections keyed by heading name.
196
200
  # @parameter notes [Fragment | Nil] The presenter notes as a Markly AST fragment.
197
201
  # @parameter script [String | Nil] JavaScript to execute after the slide renders.
198
- def initialize(path, front_matter: nil, content: {}, notes: nil, script: nil)
202
+ def initialize(presentation, path, front_matter: nil, content: {}, notes: nil, script: nil)
203
+ @presentation = presentation
199
204
  @path = path
200
205
  @front_matter = front_matter
201
206
  @content = content
@@ -203,9 +208,18 @@ module Presently
203
208
  @script = script
204
209
  end
205
210
 
206
- # @attribute [String] The file path of the slide.
211
+ # @attribute [Presentation] The presentation which owns the slide.
212
+ attr :presentation
213
+
214
+ # @attribute [String] The slide path relative to the presentation root.
207
215
  attr :path
208
216
 
217
+ # The absolute path of the slide source file.
218
+ # @returns [String]
219
+ def source_path
220
+ File.join(@presentation.root, @path)
221
+ end
222
+
209
223
  # @attribute [Hash | Nil] The parsed YAML front_matter.
210
224
  attr :front_matter
211
225
 
@@ -249,7 +263,7 @@ module Presently
249
263
  end
250
264
 
251
265
  # The transition type for animating into this slide.
252
- # @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"morph"`), or `nil` for instant swap.
266
+ # @returns [String | Nil] The transition name (e.g. `"fade"`, `"slide-left"`, `"slide-right"`), or `nil` for instant swap.
253
267
  def transition
254
268
  @front_matter&.fetch("transition", nil)
255
269
  end
@@ -5,5 +5,5 @@
5
5
 
6
6
  # @namespace
7
7
  module Presently
8
- VERSION = "0.14.1"
8
+ VERSION = "0.16.0"
9
9
  end
@@ -364,18 +364,6 @@ html[data-transition="slide-right"]::view-transition-new(slide-container) {
364
364
  animation: vt-slide-in-left 0.4s ease-in-out;
365
365
  }
366
366
 
367
- /* Magic move — the browser interpolates position/size for matched
368
- view-transition-name elements. No cross-fade on the container
369
- to avoid background dimming. */
370
- html[data-transition="morph"]::view-transition-old(slide-container) {
371
- animation: none;
372
- opacity: 0;
373
- }
374
-
375
- html[data-transition="morph"]::view-transition-new(slide-container) {
376
- animation: none;
377
- }
378
-
379
367
  @keyframes vt-fade-out {
380
368
  from { opacity: 1; }
381
369
  to { opacity: 0; }
@@ -410,13 +398,6 @@ html[data-transition="morph"]::view-transition-new(slide-container) {
410
398
  BUILD EFFECTS
411
399
  ======================== */
412
400
 
413
- /* Suppress both pseudo-elements for hidden build elements so they
414
- neither crossfade in nor crossfade out during the transition. */
415
- ::view-transition-old(.build-hidden),
416
- ::view-transition-new(.build-hidden) {
417
- display: none;
418
- }
419
-
420
401
  /* Fade */
421
402
  .build-fade {
422
403
  animation: vt-fade-in 0.4s ease;
data/public/slide.js CHANGED
@@ -3,7 +3,6 @@
3
3
  // instead of tracking count manually. Created via SlideElements#builder(options).
4
4
  export class SlideBuilder {
5
5
  #elements;
6
- #prefix;
7
6
  #defaultEffect;
8
7
  #slide;
9
8
  #step = 0;
@@ -11,7 +10,6 @@ export class SlideBuilder {
11
10
  constructor(slide, elements, options = {}) {
12
11
  this.#slide = slide;
13
12
  this.#elements = elements;
14
- this.#prefix = options.group || 'build';
15
13
  this.#defaultEffect = options.effect || null;
16
14
  }
17
15
 
@@ -26,16 +24,8 @@ export class SlideBuilder {
26
24
  let revealedElement = null;
27
25
 
28
26
  this.#elements.forEach((element, index) => {
29
- // Only assign a group name if the element doesn't already have an explicit one.
30
- // Preserving explicit names allows elements to participate in morph transitions
31
- // to other slides while still being managed by the build system.
32
- if (!element.style.viewTransitionName || element.style.viewTransitionName === 'none') {
33
- element.style.viewTransitionName = `${this.#prefix}-${index + 1}`;
34
- }
35
-
36
27
  if (index < count) {
37
28
  element.style.visibility = 'visible';
38
- element.style.viewTransitionClass = '';
39
29
 
40
30
  if (index === count - 1 && effect) {
41
31
  element.classList.add(`build-${effect}`);
@@ -43,9 +33,6 @@ export class SlideBuilder {
43
33
  }
44
34
  } else {
45
35
  element.style.visibility = 'hidden';
46
- // Keep viewTransitionClass set so morph transitions can suppress
47
- // crossfading on hidden elements when called inside startViewTransition.
48
- element.style.viewTransitionClass = 'build-hidden';
49
36
  }
50
37
  });
51
38
 
@@ -75,11 +62,7 @@ export class SlideBuilder {
75
62
  const effect = this.#slide.animated ? (overrides.effect !== undefined ? overrides.effect : this.#defaultEffect) : null;
76
63
  const element = this.#elements[this.#step];
77
64
 
78
- if (!element.style.viewTransitionName || element.style.viewTransitionName === 'none') {
79
- element.style.viewTransitionName = `${this.#prefix}-${this.#step + 1}`;
80
- }
81
65
  element.style.visibility = 'visible';
82
- element.style.viewTransitionClass = '';
83
66
 
84
67
  this.#step += 1;
85
68
 
@@ -141,7 +124,6 @@ export class SlideElements {
141
124
 
142
125
  // Create a stateful SlideBuilder for this element collection with default options.
143
126
  // @parameter options [Object] Default options applied to every show() / next() call.
144
- // group: prefix for view-transition-name (default: "build")
145
127
  // effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
146
128
  // @returns [SlideBuilder]
147
129
  builder(options = {}) {
@@ -152,7 +134,6 @@ export class SlideElements {
152
134
  // Delegates to SlideBuilder for the actual implementation.
153
135
  // @parameter count [Integer] Number of elements to show.
154
136
  // @parameter options [Object]
155
- // group: prefix for view-transition-name (default: "build")
156
137
  // effect: "fade", "fly-up", "fly-down", "fly-left", "fly-right", "scale"
157
138
  // @returns [Promise] Resolves when the animation completes (or immediately if no effect).
158
139
  show(count, options = {}) {
data/readme.md CHANGED
@@ -23,12 +23,23 @@ Please see the [project documentation](https://socketry.github.io/presently/) fo
23
23
 
24
24
  - [Getting Started](https://socketry.github.io/presently/guides/getting-started/index) - This guide explains how to use `presently` to create and deliver web-based presentations using Markdown slides.
25
25
 
26
- - [Animating Slides](https://socketry.github.io/presently/guides/animating-slides/index) - This guide explains how to animate content within slides using the `morph` transition and the slide scripting system.
26
+ - [Animating Slides](https://socketry.github.io/presently/guides/animating-slides/index) - This guide explains how to animate content within slides using the slide scripting system.
27
27
 
28
28
  ## Releases
29
29
 
30
30
  Please see the [project releases](https://socketry.github.io/presently/releases/index) for all releases.
31
31
 
32
+ ### v0.16.0
33
+
34
+ - Add support for organizing slides in nested directories. Every directory and slide filename must begin with a numeric prefix, and slides are ordered by relative path.
35
+
36
+ ### v0.15.0
37
+
38
+ q
39
+
40
+ - Export works from current working directory.
41
+ - Remove explicit support for `morph` transition.
42
+
32
43
  ### v0.14.0
33
44
 
34
45
  - Increase code font size by 50%.
@@ -73,14 +84,6 @@ Please see the [project releases](https://socketry.github.io/presently/releases/
73
84
  - 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.
74
85
  - Add `Slide#cancelTimeouts()` — cancels all pending timeouts registered by the slide's script. Called automatically by the presentation engine on every slide change.
75
86
 
76
- ### v0.6.0
77
-
78
- - 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)`.
79
-
80
- ### v0.5.0
81
-
82
- - Add optional `speaker` front matter key to slides. When present, the current speaker's name is shown in the timing bar. If the next slide has a different speaker, a handoff indicator (e.g. `→ Next Speaker`) is shown alongside, giving presenters an at-a-glance cue for tag-team talks.
83
-
84
87
  ## See Also
85
88
 
86
89
  - [lively](https://github.com/socketry/lively) — The real-time application framework that powers Presently.
data/releases.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Releases
2
2
 
3
+ ## v0.16.0
4
+
5
+ - Add support for organizing slides in nested directories. Every directory and slide filename must begin with a numeric prefix, and slides are ordered by relative path.
6
+
7
+ ## v0.15.0
8
+
9
+ q
10
+
11
+ - Export works from current working directory.
12
+ - Remove explicit support for `morph` transition.
13
+
3
14
  ## v0.14.0
4
15
 
5
16
  - Increase code font size by 50%.
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.14.1
4
+ version: 0.16.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -44,14 +44,14 @@ dependencies:
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: '0.16'
47
+ version: '0.18'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.16'
54
+ version: '0.18'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: markly
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -243,7 +243,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
243
243
  - !ruby/object:Gem::Version
244
244
  version: '0'
245
245
  requirements: []
246
- rubygems_version: 4.0.6
246
+ rubygems_version: 4.0.10
247
247
  specification_version: 4
248
248
  summary: A web-based presentation tool built with Lively.
249
249
  test_files: []
metadata.gz.sig CHANGED
Binary file