From 0085e7effaf832edec962f181f424509d1cbe388 Mon Sep 17 00:00:00 2001 From: Samuel Williams Date: Fri, 4 Sep 2026 15:13:06 +1200 Subject: [PATCH] Add colocated slide stylesheets Assisted-By: devx/eb377015-8099-4dd4-ae2e-835d6d6a6387 --- guides/getting-started/readme.md | 37 ++++++++++ lib/presently.rb | 1 + lib/presently/application.rb | 27 +++++--- lib/presently/export.rb | 5 ++ lib/presently/export.xrb | 3 + lib/presently/page.rb | 5 +- lib/presently/playback.rb | 5 ++ lib/presently/playback.xrb | 3 + lib/presently/presentation.rb | 5 ++ lib/presently/slide.rb | 38 ++++++++++- lib/presently/slide_assets.rb | 101 ++++++++++++++++++++++++++++ lib/presently/slide_renderer.rb | 3 +- lib/presently/stylesheet.rb | 112 +++++++++++++++++++++++++++++++ presently.gemspec | 3 +- readme.md | 1 + test/presently/application.rb | 62 +++++++++++++++++ test/presently/export.rb | 6 ++ test/presently/playback.rb | 6 ++ test/presently/slide.rb | 54 +++++++++++++++ test/presently/slide_renderer.rb | 20 ++++++ test/presently/stylesheet.rb | 71 ++++++++++++++++++++ 21 files changed, 550 insertions(+), 18 deletions(-) create mode 100644 lib/presently/slide_assets.rb create mode 100644 lib/presently/stylesheet.rb create mode 100644 test/presently/stylesheet.rb diff --git a/guides/getting-started/readme.md b/guides/getting-started/readme.md index b74435e..6678684 100644 --- a/guides/getting-started/readme.md +++ b/guides/getting-started/readme.md @@ -79,6 +79,43 @@ Each slide has three parts: 2. **Content** with Markdown headings that become named sections for the template. 3. **Presenter notes** after a `---` separator in the body (optional). +### Styling Slides + +CSS can be colocated with the slides it styles: + +``` text +slides/ +├── style.css +├── 010-introduction.md +└── 020-scheduling/ + ├── style.css + ├── 010-overview.md + ├── 020-queue.md + └── 020-queue.css +``` + +`slides/style.css` applies globally. A nested `style.css` applies to every slide below that directory, while a CSS file matching a Markdown filename applies only to that slide. In the example above, `020-scheduling/style.css` applies to both scheduling slides and `020-queue.css` applies only to `020-queue.md`. + +Presently automatically wraps nested and slide-specific stylesheets in an [`@scope`](https://developer.mozilla.org/en-US/docs/Web/CSS/@scope) rule rooted at the rendered slide. Write these files as scoped CSS fragments containing ordinary style rules and nestable grouping rules such as `@media`, `@supports`, `@container`, and `@layer`. Keep stylesheet-level rules such as `@charset`, `@import`, and `@namespace`, and globally named definitions such as `@font-face`, `@keyframes`, and `@property`, in the root `slides/style.css` or the existing `public/_static/custom.css`. + +Relative images and fonts remain adjacent to the stylesheet that uses them: + +``` css +.architecture { + background-image: url("architecture.svg"); +} +``` + +Presently uses `protocol-media-registry` to determine asset content types. Files with unrecognized media types are not served. + +Relative images embedded in Markdown resolve from the Markdown file's directory, including images in included Markdown files: + +``` markdown +![Architecture](architecture.svg) +``` + +Presently loads each discovered stylesheet once in deterministic presentation order. Directory styles are loaded from parent to child before the matching slide sidecar, so more specific styles naturally appear later in the cascade. The same stylesheets are used by the display, presenter, recorder, playback, and export interfaces. + ### Running the Presentation Start the server from your presentation directory: diff --git a/lib/presently.rb b/lib/presently.rb index 36ad529..f955c2e 100644 --- a/lib/presently.rb +++ b/lib/presently.rb @@ -6,6 +6,7 @@ require_relative "presently/version" require_relative "presently/recordings" +require_relative "presently/stylesheet" require_relative "presently/recordings/normalizer" require_relative "presently/recording_view" require_relative "presently/playback" diff --git a/lib/presently/application.rb b/lib/presently/application.rb index b983b23..e7dab53 100644 --- a/lib/presently/application.rb +++ b/lib/presently/application.rb @@ -11,6 +11,7 @@ require_relative "presenter_view" require_relative "recording_view" require_relative "recordings" +require_relative "slide_assets" require_relative "playback" require_relative "export" require_relative "page" @@ -34,7 +35,8 @@ def initialize(delegate, slides_root: "slides", templates_roots: [], recordings_ @recordings = Recordings.new(recordings_root || File.expand_path("../audio", slides_root)) @playback_recordings = Recordings.new(playback_recordings_root || File.expand_path("../audio-normalized", slides_root)) - super(delegate) + slide_assets = SlideAssets.new(delegate, root: @slides_root, stylesheets: ->{controller.presentation.stylesheets}) + super(slide_assets) end # The view classes that this application allows. @@ -66,12 +68,17 @@ def title "Presently" end + # Create the presentation display page for the root route. + # @returns [Page] The presentation page. + def index + page(body) + end + # Add Presently's routes to Lively's standard application routes. # @parameter router [Lively::Router] The router to configure. def configure_routes(router) super - router.get("/"){render_page(DisplayView.new(controller: controller))} router.get("/presenter"){render_page(PresenterView.new(controller: controller))} router.get("/record"){render_page(RecordingView.new(controller: controller))} @@ -92,19 +99,17 @@ def configure_routes(router) end end - # Delegate requests which do not match a configured route. - # @parameter request [Protocol::HTTP::Request] The incoming request. - # @returns [Protocol::HTTP::Response] The delegate response. - def handle(request) - delegate.call(request) - end - private + # Create a Presently page with the presentation-specific stylesheets. + def page(body) + stylesheets = controller.presentation.stylesheets.map(&:url) + Page.new(title: title, body: body, stylesheets: stylesheets) + end + # Render one of Presently's live interfaces. def render_page(body) - page = Page.new(title: title, body: body) - Protocol::HTTP::Response[200, [], [page.call]] + Protocol::HTTP::Response[200, [], [page(body).call]] end # Render the narrated playback interface. diff --git a/lib/presently/export.rb b/lib/presently/export.rb index a765ba4..14dcbe1 100644 --- a/lib/presently/export.rb +++ b/lib/presently/export.rb @@ -82,6 +82,11 @@ def initialize(presentation:, page_size: PageSize::DEFAULT, notes: true, speaker # @attribute [Boolean] Whether slide timing is included. attr :timing + # @returns [Array(Stylesheet)] The ordered presentation stylesheets. + def stylesheets + @presentation.stylesheets + end + # Render a single slide to an HTML string. # @parameter slide [Slide] The slide to render. # @returns [XRB::MarkupString] diff --git a/lib/presently/export.xrb b/lib/presently/export.xrb index ab45688..887c807 100644 --- a/lib/presently/export.xrb +++ b/lib/presently/export.xrb @@ -6,6 +6,9 @@ + + +