From 1f8ef5a47c4f37c6c987a64a589f50fd1d4b5a0b Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Fri, 2 Oct 2026 08:46:58 +0200 Subject: [PATCH 1/8] Workflow changes --- .github/workflows/release.yml | 25 ++++++-- .github/workflows/slides.yml | 4 +- .github/workflows/version.yml | 1 - docs/README.md | 3 +- docs/deployment.md | 23 +++++-- docs/development.md | 60 +++---------------- docs/releases.md | 109 ++++++++++++++++++++++++++++++++++ docs/thumbnail-generation.md | 15 ++--- pnpm-lock.yaml | 6 -- pnpm-workspace.yaml | 2 - 10 files changed, 168 insertions(+), 80 deletions(-) create mode 100644 docs/releases.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02b1102..7bb10a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,15 @@ name: Release notes on: push: tags: ['@allmaps/slides@*'] + workflow_dispatch: + inputs: + tag: + description: Existing release tag (for example @allmaps/slides@0.1.0-beta.1) + required: true + type: string + +concurrency: + group: release-notes-${{ inputs.tag || github.ref_name }} permissions: contents: write @@ -10,17 +19,25 @@ permissions: jobs: notes: runs-on: ubuntu-24.04 + env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} steps: - uses: actions/checkout@v6 + with: + ref: ${{ inputs.tag || github.ref }} - uses: actions/setup-node@v6 with: node-version: 24 - name: Extract notes for the tagged version - run: node scripts/release-notes.mjs > "$RUNNER_TEMP/slides-release-notes.md" + run: GITHUB_REF_NAME="$RELEASE_TAG" node scripts/release-notes.mjs > "$RUNNER_TEMP/slides-release-notes.md" - name: Create GitHub release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - args=(--verify-tag --title "$GITHUB_REF_NAME" --notes-file "$RUNNER_TEMP/slides-release-notes.md") - if [[ "$GITHUB_REF_NAME" == *-beta.* ]]; then args+=(--prerelease); fi - gh release create "$GITHUB_REF_NAME" "${args[@]}" + if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then + echo "Release $RELEASE_TAG already exists." + exit 0 + fi + args=(--verify-tag --title "$RELEASE_TAG" --notes-file "$RUNNER_TEMP/slides-release-notes.md") + if [[ "$RELEASE_TAG" == *-beta.* ]]; then args+=(--prerelease); fi + gh release create "$RELEASE_TAG" "${args[@]}" diff --git a/.github/workflows/slides.yml b/.github/workflows/slides.yml index 913386f..6d5439e 100644 --- a/.github/workflows/slides.yml +++ b/.github/workflows/slides.yml @@ -20,8 +20,8 @@ jobs: contents: read steps: - uses: actions/checkout@v6 - with: - submodules: recursive + - name: Check out the integration presentation + run: git submodule update --init content/gravity-at-sea - uses: pnpm/action-setup@v4 with: version: 10.22.0 diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 7ec889f..c62b9cd 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -25,7 +25,6 @@ jobs: with: fetch-depth: 0 persist-credentials: false - submodules: recursive - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: diff --git a/docs/README.md b/docs/README.md index 4afe69a..1702ebe 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,8 @@ small English example and [annotated configuration references](https://github.co | Reference | What it covers | | --- | --- | -| [Development and releases](development.md) | Source setup, bundling, package checks and publication. | +| [Development](development.md) | Source setup, bundling and package checks. | +| [Releases](releases.md) | Versions, npm publication, GitHub releases and content upgrades. | | [Architecture](architecture.md) | Workspace boundaries, data flow, caches and watching. | | [Slides API](api.md) | Public JavaScript exports and a build example. | | [Interface behavior](interface.md) | Navigation, mobile layouts and viewer integration. | diff --git a/docs/deployment.md b/docs/deployment.md index c03038a..4a83056 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -39,12 +39,25 @@ see [licensing and source distribution](licensing.md#distributing-a-site). ## Workflows and headers -The existing content repositories provide source-checkout workflow examples: +The content repositories install an exact `@allmaps/slides` version from their +own `package.json` and `pnpm-lock.yaml`, using `pnpm install --frozen-lockfile`. +They do not clone the Slides source. Examples: [Gravity at Sea](https://github.com/tu-delft-heritage/gravity-expeditions-app/blob/main/.github/workflows/deploy-pages.yml), -[Reuzenarbeid](https://github.com/tu-delft-heritage/reuzenarbeid/blob/main/.github/workflows/deploy-pages.yml) -and [Kattenburg Atlas](https://github.com/amsterdamtimemachine/kattenburg-atlas/blob/main/.github/workflows/deploy-pages.yml). -They install renderer system dependencies and run the build under Xvfb. -The [template](https://github.com/allmaps/slides-template) does not configure deployment. +[Reuzenarbeid](https://github.com/tu-delft-heritage/reuzenarbeid/blob/main/.github/workflows/deploy-pages.yml), +[Kattenburg Atlas](https://github.com/amsterdamtimemachine/kattenburg-atlas/blob/main/.github/workflows/deploy-pages.yml) +and the [template](https://github.com/allmaps/slides-template/blob/main/.github/workflows/deploy-pages.yml). + +Select **GitHub Actions** as the content repository's Pages source. The workflow +uses Pages' configured URL and base path, including a custom domain when configured. +It installs the native renderer's Ubuntu libraries inline and builds under Xvfb. +IIIF, annotation and thumbnail caches have separate reset options for manual runs. +Private content repositories can use the public npm package without a Slides +checkout token; Pages availability depends on the repository's GitHub plan. + +To upgrade, run `pnpm add -D -E @allmaps/slides@` in the content repository, +validate/build, then commit its manifest and lockfile. Pushing to `main` redeploys. +The `SLIDES_REF` variable is no longer used. See [software releases](releases.md) +for how npm publishing and GitHub releases work. Kattenburg also has a Docker build that serves the exported site with Nginx; see its [deployment guide](https://github.com/amsterdamtimemachine/kattenburg-atlas/blob/main/docs/deployment.md). diff --git a/docs/development.md b/docs/development.md index f50b480..eea5494 100644 --- a/docs/development.md +++ b/docs/development.md @@ -84,58 +84,14 @@ needs [system dependencies](static-render.md#linux-setup). ## Releases -`packages/slides/package.json` owns the public version, starting at -`0.1.0-beta.1`. The bundled app and helper packages share that release. Their -internal manifest versions are not separate public releases. - -For a user-visible change to the CLI, app or a bundled helper, run -`pnpm changeset`, select `@allmaps/slides`, and describe what changed. Commit the -generated Markdown file with the change. Content-only changes belong in their -content repository's history and do not need a Slides changeset. - -The version workflow opens a release PR after changesets reach `main`. It updates -the version, changelog and prerelease state. Enable GitHub Actions' permission to -create pull requests in the repository settings. To prepare the same changes -locally, run `pnpm release:version` and commit the result. During beta, Changesets -advances `0.1.0-beta.1` to `0.1.0-beta.2`, and so on. A minor or major changeset can -also change the target regular version. - -Keep the content submodules initialized when refreshing the workspace lockfile -(`git submodule update --init --recursive` in a clean release checkout). The -version workflow does this too, so it retains their dependency entries. - -For each release: - -1. Review the version PR and run the [checks below](#checks). The release PR uses - `GITHUB_TOKEN`, so run the Slides CI workflow manually on its branch if GitHub - does not trigger checks automatically. Merge the reviewed version changes. -2. Check out the resulting clean, pushed commit and run `pnpm install --frozen-lockfile` - followed by `pnpm release:check`. Local content changes are excluded from this - check. Confirm that the software commit is accessible on GitHub before publishing. -3. With npm access to `@allmaps/slides`, run `pnpm release:publish`. This rebuilds - and publishes the package, explicitly selects the `beta` npm tag for a beta version, and - creates a local Git tag such as `@allmaps/slides@0.1.0-beta.1`. -4. Run `git push --follow-tags`. The release workflow creates a GitHub prerelease - with the corresponding changelog section. GitHub supplies its usual source - downloads; no separate source archive is uploaded. - -Use this publish command rather than `changeset publish`: Changesets can choose -`latest` for packages that have not had a stable release, leaving `beta` behind. - -The initial `0.1.0-beta.1` version and changelog are already prepared; no version -bump is needed for its first publication. Packing and testing never publish. -An npm package's first publication may also receive the `latest` tag, so check -the registry tags after that first release. The documented install uses `@beta`. - -When ready for a regular release, run `pnpm changeset pre exit` followed by -`pnpm release:version`, review and commit the changes, then follow the same -release steps. With the current target, this produces `0.1.0` on npm's `latest` -tag. Keep Changesets' generated `.changeset/pre/` records in Git until it removes -them during that transition. - -The package's [README](../packages/slides/README.md) is included at the archive root -and becomes its npm landing page. Keep its documentation links absolute so they -work on npm. Detailed guides remain in this repository's `docs/` directory. +Only `@allmaps/slides` is published; the bundled app and helpers share its version. +Add a changeset for changes to the CLI, app or helpers with `pnpm changeset`. +See [publish versions and GitHub releases](releases.md) for the complete process, +workflow responsibilities, npm authentication and recovery steps. + +Content repositories have their own pinned package dependency and lockfile. +They are not workspace packages, so preparing a software release does not need +content submodules. Initialize a submodule only when developing that presentation. ## Software identity in credits diff --git a/docs/releases.md b/docs/releases.md new file mode 100644 index 0000000..1eaec14 --- /dev/null +++ b/docs/releases.md @@ -0,0 +1,109 @@ +# Publish versions and GitHub releases + +Slides has three independent outputs: the npm package, a GitHub release with +notes, and each content repository's deployed website. Publishing one does not +automatically publish the others. + +Only `@allmaps/slides` goes to npm. Its version is in +[`packages/slides/package.json`](../packages/slides/package.json); the app and +bundled helpers share that version. The first published version is `0.1.0-beta.1`. +Use Node.js 24 and pnpm 10.22.0. Run software release commands in the Slides root. + +## Publish the next version + +1. Make and test the software change. Run `pnpm changeset`, select + `@allmaps/slides`, choose the change type and describe the user-visible change. + Commit the generated file with the code and merge it into `main`. +2. **Prepare release** opens or updates a version PR. It increments the version, + writes the changelog and updates the lockfile. During beta, a patch advances + `0.1.0-beta.1` to `0.1.0-beta.2`. Review and merge this PR. It does not publish. +3. Check out the resulting commit and make sure it has been pushed to GitHub. + With a clean software working tree, run: + + ```sh + pnpm install --frozen-lockfile + pnpm release:check + npm login --auth-type=web + pnpm release:publish + ``` + + Complete npm's browser/passkey authentication. Email login verification alone + does not replace a configured publishing second factor. See + [npm's 2FA setup](https://docs.npmjs.com/configuring-two-factor-authentication/). + The command rebuilds and publishes the package to `beta`, then creates a local + annotated tag such as `@allmaps/slides@0.1.0-beta.2`. For a regular version it + selects `latest`. No workflow currently publishes to npm. +4. Push that tag (replace the version with the one just published): + + ```sh + git push origin '@allmaps/slides@0.1.0-beta.2' + ``` + + **Release notes** creates the matching GitHub prerelease from the changelog. + GitHub provides source ZIP/tar downloads. This does not upload to npm again. +5. Upgrade each content repository that should use the new software: + + ```sh + pnpm add -D -E @allmaps/slides@0.1.0-beta.2 + pnpm validate + pnpm build + ``` + + Commit `package.json` and `pnpm-lock.yaml` in that content repository and push. + Its deployment workflow installs the locked version and rebuilds the website. + A Slides release does not silently upgrade existing presentations. + +[Software checks](development.md#checks) cover the package and application. +Content changes belong in the content repository and need no Slides changeset. +Local content edits do not block `release:check`. + +## What the actions do + +| Workflow | Trigger | Result | +| --- | --- | --- | +| Prepare release (`version.yml`) | Changesets on `main`, or manual run on `main` | Opens/updates the version and changelog PR. No publication. | +| Release notes (`release.yml`) | Push of `@allmaps/slides@*`, or manual run with an existing tag | Creates the GitHub release; beta versions are prereleases. | +| Slides architecture (`slides.yml`) | Relevant pull requests, or manual run | Tests the CLI, app, renderer, development server and installed package. | +| Static renderer (`static-render.yml`) | Relevant pull requests, or manual run | Tests the Linux renderer container. | +| Deploy to GitHub Pages (content repos) | Push to `main`, or manual run | Installs the pinned npm package, builds content and deploys the site. | +| Build and Publish Docker Image (Kattenburg) | Push to `main`, version tag, or manual run | Builds the site and publishes an Nginx image to GHCR. | + +GitHub Actions must be allowed to create pull requests under repository +**Settings → Actions → General**. PRs created with `GITHUB_TOKEN` may not trigger +other workflows automatically; manually run Slides architecture and Static +renderer on the version PR branch when needed. Content repositories must select +**GitHub Actions** as their Pages source under **Settings → Pages**. + +Version preparation installs only the software workspace. It does not clone +content repositories or require a token with access to private content. The +integration workflow initializes only its public Gravity at Sea presentation. + +## Recover an interrupted release + +- If npm publishing fails, no tag is created. Resolve the error and retry the same + version after checking `npm view @allmaps/slides versions --json`. +- If npm succeeded but there is no GitHub release, check `git tag --list + '@allmaps/slides@*'` and push the existing tag. Do not republish or move the tag. +- If npm succeeded but local tagging failed, check that you are on the exact + published source commit, then run `pnpm exec changeset git-tag` and push the tag. +- If the tag is already on GitHub but the release action failed, rerun the job, + or manually run Release notes with that tag. The workflow leaves an existing + release in place, so retries are safe. + +The first npm publication also received `latest`; install an explicit version +or use `@beta` when selecting the beta channel. A version already accepted by +npm cannot be overwritten. `pnpm release:publish` does not push Git commits/tags. +Do not use `changeset publish` here: it can select `latest` for a package whose +first stable release has not yet been published. + +## Finish beta + +Run `pnpm changeset pre exit`, then `pnpm release:version`. Review, commit and +merge the changes and follow the same publish/tag steps. With the current target, +this produces `0.1.0` on `latest`. Keep Changesets' `.changeset/pre/` records until +Changesets removes them. To prepare a version PR's changes locally instead of +using the action, run `pnpm release:version` and commit its output. + +Future npm automation can use [trusted publishing](https://docs.npmjs.com/trusted-publishers/) +with a dedicated GitHub workflow. It needs a matching trusted-publisher setup in +npm; the release-notes workflow and its GitHub token alone do not grant npm access. diff --git a/docs/thumbnail-generation.md b/docs/thumbnail-generation.md index 4768861..6dfd657 100644 --- a/docs/thumbnail-generation.md +++ b/docs/thumbnail-generation.md @@ -198,12 +198,12 @@ newly generated files are saved even if a later build step fails. Only complete source bodies and completed render jobs are cached. Manual workflow inputs can independently skip restoring each cache. -The Pages workflow sets up Node 24 and pnpm, calls the renderer package's -shared Ubuntu dependency setup script, and runs `slides build` under Xvfb. +The Pages workflow sets up Node 24 and pnpm, installs Ubuntu renderer libraries +inline and the pinned npm dependencies, and runs `slides build` under Xvfb. It uploads `dist/site` directly. No Docker image is built or run for Pages. Container deployment has a separate `docker-publish.yml` workflow. Kattenburg's -single multi-stage Dockerfile obtains the Slides source, installs dependencies, +single multi-stage Dockerfile installs the pinned Slides npm package, generates all derivatives and thumbnails, prerenders the site and produces an Nginx serving image. Only public site files enter that final image. Its Nginx configuration supports clean URLs for prerendered chapter HTML files. @@ -212,12 +212,13 @@ The container build uses three BuildKit cache mounts and explicit Actions cache import/export; ordinary image-layer caching alone does not persist cache mounts on hosted runners. A daily `CACHE_EPOCH` build argument lets unchanged content revalidate remote inputs. The renderer cache is namespaced by the -framework lockfile. See [Kattenburg's deployment guide](https://github.com/amsterdamtimemachine/kattenburg-atlas/blob/main/docs/deployment.md) -for build arguments and local source overrides. +content lockfile. See [Kattenburg's deployment guide](https://github.com/amsterdamtimemachine/kattenburg-atlas/blob/main/docs/deployment.md) +for build arguments and package upgrades. The optional renderer/build-tool image remains useful for standalone batches. -It shares `install-system-deps.sh` with the Pages and web-image builds. The -monorepo's `static-render.yml` workflow tests that tool image and native pixels. +The software workspace uses `install-system-deps.sh` for its own native CI setup; +consumer workflows install the equivalent libraries inline. The monorepo's +`static-render.yml` workflow tests the tool image and native pixels. ## Validation and limits diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6dc9a6..2992e15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,12 +85,6 @@ importers: specifier: ^7.3.1 version: 7.3.1(@types/node@25.9.1)(jiti@2.6.1)(lightningcss@1.31.1)(yaml@2.9.0) - content/gravity-at-sea: {} - - content/kattenburg-atlas: {} - - content/reuzenarbeid: {} - packages/iiif: dependencies: '@iiif/builder': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7818476..a0d0c98 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,5 @@ packages: - apps/* - - content/* - - '!content/tests' - packages/* allowBuilds: From 2e85311fa8d5d15cef4600a6e06d416a16550e47 Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Fri, 2 Oct 2026 11:32:20 +0200 Subject: [PATCH 2/8] Run tests on any PR --- .github/workflows/slides.yml | 11 +---------- .github/workflows/static-render.yml | 9 +-------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/.github/workflows/slides.yml b/.github/workflows/slides.yml index 6d5439e..7eceaa8 100644 --- a/.github/workflows/slides.yml +++ b/.github/workflows/slides.yml @@ -1,16 +1,7 @@ name: Slides architecture on: + # Required status checks must run on every pull request, including docs-only changes. pull_request: - paths: - - "packages/**" - - "package.json" - - ".changeset/**" - - "apps/slides/**" - - "scripts/**" - - "content/**" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - - ".github/workflows/slides.yml" workflow_dispatch: jobs: integration: diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml index 1249637..d32f768 100644 --- a/.github/workflows/static-render.yml +++ b/.github/workflows/static-render.yml @@ -1,14 +1,7 @@ name: Static renderer on: + # Required status checks must run on every pull request, including docs-only changes. pull_request: - paths: - - "packages/**" - - "apps/slides/**" - - "scripts/**" - - ".dockerignore" - - "pnpm-lock.yaml" - - "pnpm-workspace.yaml" - - ".github/workflows/static-render.yml" workflow_dispatch: jobs: container: From cdb23737b6696e190d4ad1d27d2d5a8180d80d25 Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Fri, 2 Oct 2026 11:32:55 +0200 Subject: [PATCH 3/8] Updated submodules --- content/gravity-at-sea | 2 +- content/kattenburg-atlas | 2 +- content/reuzenarbeid | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/content/gravity-at-sea b/content/gravity-at-sea index dd89674..8b7624c 160000 --- a/content/gravity-at-sea +++ b/content/gravity-at-sea @@ -1 +1 @@ -Subproject commit dd8967428ab67fa3590802dc848097fb7fef30f8 +Subproject commit 8b7624c2eac5e32d93320b2b374f2a314a8d3d98 diff --git a/content/kattenburg-atlas b/content/kattenburg-atlas index 6743e27..89f9d48 160000 --- a/content/kattenburg-atlas +++ b/content/kattenburg-atlas @@ -1 +1 @@ -Subproject commit 6743e277c6f521aec3ad64278036256558cdad0e +Subproject commit 89f9d48c2092a52d05c4aefa0dece27a8caa1747 diff --git a/content/reuzenarbeid b/content/reuzenarbeid index 5ac2cec..e700525 160000 --- a/content/reuzenarbeid +++ b/content/reuzenarbeid @@ -1 +1 @@ -Subproject commit 5ac2cec19bb684fb9083c2e6e78627b8070900ab +Subproject commit e700525bbd6644b657144901990bc8a56cd9e175 From ebeeb53f7c0b2cab87ae3d53ceb38b6bff29a289 Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Fri, 2 Oct 2026 11:42:26 +0200 Subject: [PATCH 4/8] Skip tests for non-code changes --- .github/test-paths.yml | 12 ++++++++++++ .github/workflows/slides.yml | 23 ++++++++++++++++++++++- .github/workflows/static-render.yml | 17 ++++++++++++++++- docs/development.md | 9 +++++++++ docs/releases.md | 10 ++++++---- 5 files changed, 65 insertions(+), 6 deletions(-) create mode 100644 .github/test-paths.yml diff --git a/.github/test-paths.yml b/.github/test-paths.yml new file mode 100644 index 0000000..6fe06e9 --- /dev/null +++ b/.github/test-paths.yml @@ -0,0 +1,12 @@ +# Used with dorny/paths-filter's `every` predicate in both test workflows. +# Run tests unless every changed file is documentation. Keep content Markdown, +# licenses, workflow files and unknown paths covered by default. +tests: + - '**' + - '!docs/**' + - '!README.md' + - '!packages/*/README.md' + - '!apps/*/README.md' + - '!packages/*/CHANGELOG.md' + - '!apps/*/CHANGELOG.md' + - '!.changeset/*.md' diff --git a/.github/workflows/slides.yml b/.github/workflows/slides.yml index 7eceaa8..9e60050 100644 --- a/.github/workflows/slides.yml +++ b/.github/workflows/slides.yml @@ -1,6 +1,6 @@ name: Slides architecture on: - # Required status checks must run on every pull request, including docs-only changes. + # Always report the required check; skip expensive steps for documentation-only PRs. pull_request: workflow_dispatch: jobs: @@ -9,28 +9,49 @@ jobs: timeout-minutes: 20 permissions: contents: read + pull-requests: read steps: - uses: actions/checkout@v6 + - name: Detect changes that need testing + id: changes + # Manual runs and PRs beyond the API's 3,000-file limit run all tests. + if: github.event_name == 'pull_request' && github.event.pull_request.changed_files <= 3000 + uses: dorny/paths-filter@v4 + with: + predicate-quantifier: every + filters: .github/test-paths.yml + - name: Documentation-only changes + if: steps.changes.outputs.tests == 'false' + run: echo "Documentation-only changes; integration tests were not needed." >> "$GITHUB_STEP_SUMMARY" - name: Check out the integration presentation + if: steps.changes.outputs.tests != 'false' run: git submodule update --init content/gravity-at-sea - uses: pnpm/action-setup@v4 + if: steps.changes.outputs.tests != 'false' with: version: 10.22.0 - uses: actions/setup-node@v6 + if: steps.changes.outputs.tests != 'false' with: node-version: 24 cache: pnpm - name: Set up native renderer + if: steps.changes.outputs.tests != 'false' run: sudo sh packages/static-render/scripts/install-system-deps.sh - run: pnpm install --frozen-lockfile + if: steps.changes.outputs.tests != 'false' - name: Unit and component checks + if: steps.changes.outputs.tests != 'false' run: | pnpm -r test pnpm --filter @allmaps/slides --filter @allmaps/iiif --filter @allmaps/static-render check pnpm exec slides check ./content/gravity-at-sea - name: Native pixel alignment + if: steps.changes.outputs.tests != 'false' run: xvfb-run -a pnpm --filter @allmaps/static-render test:native - name: Independent development sites + if: steps.changes.outputs.tests != 'false' run: pnpm --filter @allmaps/slides test:dev - name: Fresh installed consumer + if: steps.changes.outputs.tests != 'false' run: xvfb-run -a pnpm --filter @allmaps/slides test:package diff --git a/.github/workflows/static-render.yml b/.github/workflows/static-render.yml index d32f768..2d98c3d 100644 --- a/.github/workflows/static-render.yml +++ b/.github/workflows/static-render.yml @@ -1,6 +1,6 @@ name: Static renderer on: - # Required status checks must run on every pull request, including docs-only changes. + # Always report the required check; skip expensive steps for documentation-only PRs. pull_request: workflow_dispatch: jobs: @@ -8,10 +8,24 @@ jobs: runs-on: ubuntu-24.04 permissions: contents: read + pull-requests: read steps: - uses: actions/checkout@v6 + - name: Detect changes that need testing + id: changes + # Manual runs and PRs beyond the API's 3,000-file limit run all tests. + if: github.event_name == 'pull_request' && github.event.pull_request.changed_files <= 3000 + uses: dorny/paths-filter@v4 + with: + predicate-quantifier: every + filters: .github/test-paths.yml + - name: Documentation-only changes + if: steps.changes.outputs.tests == 'false' + run: echo "Documentation-only changes; container tests were not needed." >> "$GITHUB_STEP_SUMMARY" - uses: docker/setup-buildx-action@v3 + if: steps.changes.outputs.tests != 'false' - name: Build image and run native smoke test + if: steps.changes.outputs.tests != 'false' uses: docker/build-push-action@v6 with: context: . @@ -23,5 +37,6 @@ jobs: cache-from: type=gha,scope=slides-renderer cache-to: type=gha,mode=max,scope=slides-renderer - name: Check container entrypoint + if: steps.changes.outputs.tests != 'false' timeout-minutes: 1 run: docker run --rm slides-renderer:check node packages/static-render/bin/render.mjs --help diff --git a/docs/development.md b/docs/development.md index eea5494..da80e4c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -118,6 +118,15 @@ and [licensing](licensing.md#distributing-a-site). ## Checks +Every pull request reports the required `integration` and `container` checks. +Documentation-only changes pass after a quick file check, without installing +dependencies or building the container. The shared rules in +[`.github/test-paths.yml`](../.github/test-paths.yml) exclude guides, software +READMEs, changelogs and changeset notes. All other paths run the full checks, +including configuration, licenses, workflows and content submodules. Manual +workflow runs always run the full checks; a failed change-detection step fails +the required check. + ```sh pnpm -r test pnpm --filter @allmaps/slides check diff --git a/docs/releases.md b/docs/releases.md index 1eaec14..8b0ab9d 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -63,15 +63,17 @@ Local content edits do not block `release:check`. | --- | --- | --- | | Prepare release (`version.yml`) | Changesets on `main`, or manual run on `main` | Opens/updates the version and changelog PR. No publication. | | Release notes (`release.yml`) | Push of `@allmaps/slides@*`, or manual run with an existing tag | Creates the GitHub release; beta versions are prereleases. | -| Slides architecture (`slides.yml`) | Relevant pull requests, or manual run | Tests the CLI, app, renderer, development server and installed package. | -| Static renderer (`static-render.yml`) | Relevant pull requests, or manual run | Tests the Linux renderer container. | +| Slides architecture (`slides.yml`) | Every pull request, or manual run | Tests the CLI, app, renderer, development server and installed package; skips tests for documentation-only PRs. | +| Static renderer (`static-render.yml`) | Every pull request, or manual run | Tests the Linux renderer container; skips tests for documentation-only PRs. | | Deploy to GitHub Pages (content repos) | Push to `main`, or manual run | Installs the pinned npm package, builds content and deploys the site. | | Build and Publish Docker Image (Kattenburg) | Push to `main`, version tag, or manual run | Builds the site and publishes an Nginx image to GHCR. | GitHub Actions must be allowed to create pull requests under repository **Settings → Actions → General**. PRs created with `GITHUB_TOKEN` may not trigger -other workflows automatically; manually run Slides architecture and Static -renderer on the version PR branch when needed. Content repositories must select +other workflows automatically. If the version PR has no checks, close and reopen +it from your own account to trigger the pull-request workflows. Manual workflow +runs are useful for diagnostics but do not satisfy required PR checks. +Content repositories must select **GitHub Actions** as their Pages source under **Settings → Pages**. Version preparation installs only the software workspace. It does not clone From 961cc39db23d15d2716d0ce1712e995fc9ee633d Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Fri, 2 Oct 2026 12:07:32 +0200 Subject: [PATCH 5/8] Fixes for tests --- packages/slides/tests/package-smoke.mjs | 6 ++++-- packages/static-render/Dockerfile | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/slides/tests/package-smoke.mjs b/packages/slides/tests/package-smoke.mjs index 6d55227..113769c 100644 --- a/packages/slides/tests/package-smoke.mjs +++ b/packages/slides/tests/package-smoke.mjs @@ -4,6 +4,7 @@ import { mkdtemp, mkdir, readFile, writeFile, readdir, rm, realpath, stat } from import { tmpdir } from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { stripVTControlCharacters } from 'node:util'; import sharp from 'sharp'; const repository = fileURLToPath(new URL('../../../', import.meta.url)); const root = await mkdtemp(path.join(tmpdir(), 'slides-package-consumer-')); @@ -12,7 +13,7 @@ const pnpm = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm'; const run = (args, cwd = root, env = {}) => execFileSync(pnpm, args, { cwd, stdio: 'inherit', env: { ...process.env, ...env }, timeout: 240_000 }); async function checkDevServer(installed) { const child = spawn(process.execPath, [path.join(installed, 'bin/slides.js'), 'dev', '.', '--host', '127.0.0.1', '--port', '0'], - { cwd: root, stdio: ['ignore', 'pipe', 'pipe'] }); + { cwd: root, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, FORCE_COLOR: '1' } }); const closed = new Promise(resolve => child.once('close', resolve)); let log = ''; child.stdout.on('data', data => log += data); @@ -20,7 +21,8 @@ async function checkDevServer(installed) { try { const deadline = Date.now() + 60_000; while (Date.now() < deadline && child.exitCode === null) { - const origin = log.match(/http:\/\/127\.0\.0\.1:\d+/)?.[0]; + // CI colors the port separately; exercise that output even in local runs. + const origin = stripVTControlCharacters(log).match(/http:\/\/127\.0\.0\.1:\d+/)?.[0]; if (origin) { // Vite may still be compiling the app after it prints the server URL. const response = await fetch(`${origin}/story/`, { signal: AbortSignal.timeout(Math.max(1, deadline - Date.now())) }); diff --git a/packages/static-render/Dockerfile b/packages/static-render/Dockerfile index 954476c..ac0c9f4 100644 --- a/packages/static-render/Dockerfile +++ b/packages/static-render/Dockerfile @@ -24,7 +24,12 @@ CMD ["node", "/workspace/packages/static-render/bin/render.mjs", "--help"] # Optional Slides build image: content, caches and output are mounted at run time. FROM renderer AS slides-build +# Build-info and release tests create Git fixtures and read the release tooling. +RUN apt-get update && apt-get install -y --no-install-recommends git \ + && rm -rf /var/lib/apt/lists/* COPY apps ./apps +COPY scripts ./scripts +COPY .changeset ./.changeset RUN mkdir -p content && pnpm install --frozen-lockfile \ && pnpm --filter @allmaps/slides --filter @allmaps/static-render --filter @allmaps/iiif check \ && pnpm --filter @allmaps/slides --filter @allmaps/slides-app --filter @allmaps/svelte-canvas-panel test From 0d5cc6af0d6e8617ea9ca8ef2b20207e22857e27 Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Fri, 2 Oct 2026 22:12:31 +0200 Subject: [PATCH 6/8] Docs and credits changes --- README.md | 22 +++++++++++++---- .../slides/src/lib/components/MadeWith.svelte | 24 +++++++++++++++---- .../src/lib/components/SoftwareCredits.svelte | 8 ++++--- docs/development.md | 4 +++- packages/slides/README.md | 4 ++-- packages/slides/package.json | 2 +- 6 files changed, 47 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index d93a07b..aa0907c 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,18 @@ website you can host yourself. Principles of the project: -- Ready for academic environments. This project has been tested in the classroom, and was developed in collaboration with groups of students from various disciplines, with the aim to make them familiar with digital humanities workflows and open formats. -- Flexibility between self-contained and API-driven. Allmaps annotations, IIIF resources, map styles and GeoJSON data, additional imagery, and even map tiles can be called from remote APIs or integrated in the repository directly. -- Reusable, openly licensed software. The CLI and libraries use MIT; the app uses GPL-3.0-or-later with a [content permission](docs/licensing.md) that lets authors choose licenses such as CC BY for their own material. +- Developed for academic use. Tested in the classroom and developed with students from various disciplines, Allmaps Slides helps students become familiar with digital humanities workflows and open data formats. +- Shared software, independent content. A centrally maintained codebase lets presentations benefit from shared improvements without each project maintaining its own software. Authors manage their content in separate repositories and choose when to update. The [license structure](docs/licensing.md) supports this independence, allowing authors to choose their own content license. +- Combine local and remote resources. Annotations, IIIF resources, map styles, GeoJSON, images and map tiles can be loaded from remote services or included in the content repository. Authors choose which resources to keep locally and which services to depend on. + +View in action: + +- [Gravity at Sea](https://tu-delft-heritage.github.io/gravity-expeditions-app) +- [Kattenburg Atlas](https://kattenburg.amsterdamtimemachine.nl/) (Dutch only) +- [Reuzenarbeid](https://tu-delft-heritage.github.io/reuzenarbeid/) (Dutch only) +- [From Image to Map](http://pages.allmaps.org/slides-template) (GitHub template) + +_Allmaps Slides is currently in beta. Try it out and share your feedback, ideas or bug reports through [GitHub issues](https://github.com/allmaps/slides/issues)._ ## Create a slideshow @@ -29,8 +38,9 @@ git submodule update --init content/slides-template pnpm exec slides dev ./content/slides-template ``` -Open the URL printed by the server. The template uses a plain background and -remote images, so no basemap API key is needed. +Open the URL printed by the server. The template uses remote images and a +Protomaps basemap. Follow its [API key setup](content/slides-template/README.md#preview) +when copying or forking it. - [Development and releases](docs/development.md): source setup, checks and npm bundles. - [Architecture](docs/architecture.md): application, package and content boundaries. @@ -44,3 +54,5 @@ site after its content submodule has been initialized. - [Reuzenarbeid](https://tu-delft-heritage.github.io/reuzenarbeid/) was originally a Jekyll site and the first narrative map made with Allmaps. It was inspired by Bert Spaan's [The Changing Shoreline of New York City](https://github.com/nypl-spacetime/the-changing-shoreline-of-nyc), which was in turn inspired by [Travel the path of the solar eclipse](https://www.washingtonpost.com/graphics/national/mapping-the-2017-eclipse/). - [City Atlas](https://cityatlas.theberlage.nl/), one of three atlases made with the postmaster's programme of the [The Berlage Center for Advanced Studies in Architecture and Urban Design](https://theberlage.nl/) at TU Delft, in collaboration with Allmaps. - Existing templates for storytelling applications using MapLibre such as [Interactive Storytelling with MapLibre](https://github.com/digidem/maplibre-storymap/). + +_The development of Allmaps Slides was financially supported by the [Samenwerkende Maritieme Fondsen](https://www.samenwerkendemaritiemefondsen.nl/) for the publication of [Kattenburg Atlas](https://kattenburg.amsterdamtimemachine.nl/)._ diff --git a/apps/slides/src/lib/components/MadeWith.svelte b/apps/slides/src/lib/components/MadeWith.svelte index 318eea7..26ba521 100644 --- a/apps/slides/src/lib/components/MadeWith.svelte +++ b/apps/slides/src/lib/components/MadeWith.svelte @@ -1,11 +1,13 @@ @@ -13,7 +15,11 @@

{label ?? t("madeWith")} -

- +

{buildInfo.version}

{#if buildInfo.sourceState === "modified"}

{t("softwareModifiedBuild")}

@@ -32,8 +32,10 @@
diff --git a/docs/development.md b/docs/development.md index da80e4c..dd70798 100644 --- a/docs/development.md +++ b/docs/development.md @@ -13,7 +13,9 @@ git submodule update --init content/slides-template pnpm exec slides dev ./content/slides-template ``` -The template uses remote images and an empty basemap, so it needs no provider key. +The template uses remote images and a Protomaps basemap. Its +[README](../content/slides-template/README.md#preview) explains how to replace +the demo API key when copying or forking it. To work on another bundled content repository, initialize its submodule and pass that directory instead. `git submodule update --init --recursive` initializes all content repositories in a fresh checkout. diff --git a/packages/slides/README.md b/packages/slides/README.md index d37a9e5..92080b0 100644 --- a/packages/slides/README.md +++ b/packages/slides/README.md @@ -1,6 +1,6 @@ # @allmaps/slides -Create interactive map stories from Markdown, images and georeferenced maps. +Create interactive narrative maps from Markdown, images and georeferenced maps. The `slides` CLI previews your story locally and builds a static website. It includes the application, image tools and map thumbnail renderer. @@ -22,7 +22,7 @@ For a source checkout or a local package archive, see Create `slides.config.yml`: ```yaml -title: My first map story +title: My first narrative map slideshows: - id: main path: chapters diff --git a/packages/slides/package.json b/packages/slides/package.json index ecaba6b..5d5153b 100644 --- a/packages/slides/package.json +++ b/packages/slides/package.json @@ -90,7 +90,7 @@ "engines": { "node": ">=24" }, - "description": "Build map stories from Markdown and local assets", + "description": "Build narrative maps from Markdown and local assets", "publishConfig": { "access": "public", "exports": { From ef675048f9182329d9cece53b5d5b491657b354b Mon Sep 17 00:00:00 2001 From: Jules Schoonman Date: Sat, 3 Oct 2026 15:20:35 +0200 Subject: [PATCH 7/8] Add project setup, map overlays, and automated releases --- .changeset/content-independent-development.md | 7 + .changeset/fresh-narrative-map.md | 5 + .changeset/global-overlay-layers.md | 11 + .changeset/optional-basemaps.md | 5 + .changeset/quiet-dev-startup.md | 5 + .changeset/remove-image-map-mode.md | 15 + .changeset/shared-camera-fit.md | 11 + .changeset/space-map-comparison.md | 15 + .github/workflows/release.yml | 86 +- .github/workflows/slides.yml | 4 +- README.md | 7 +- apps/slides/package.json | 2 - apps/slides/src/lib/components/Map.svelte | 164 +- .../lib/components/PanelOverlayToggle.svelte | 28 - .../src/lib/components/Slideshow.svelte | 84 +- .../src/lib/components/SlideshowLayers.svelte | 24 +- .../src/lib/components/SlideshowPanel.svelte | 26 +- apps/slides/src/lib/shared/content-schema.ts | 1 - .../src/lib/shared/interface-settings.ts | 4 - apps/slides/src/lib/shared/keyboard.ts | 39 +- apps/slides/src/lib/shared/map/comparison.ts | 14 + apps/slides/src/lib/shared/map/focus.ts | 20 + apps/slides/src/lib/shared/map/image.ts | 1 - .../src/lib/shared/map/layer-transitions.ts | 69 + apps/slides/src/lib/shared/panel-scroll.ts | 38 + apps/slides/src/lib/shared/utils.ts | 75 - apps/slides/svelte.config.js | 8 - apps/slides/tests/interface.test.mjs | 53 +- apps/slides/tests/layer-transitions.test.mjs | 133 ++ apps/slides/tests/map-comparison.test.mjs | 37 + apps/slides/tests/map-focus.test.mjs | 66 + apps/slides/tests/panel-scroll.test.mjs | 105 ++ content/slides-template | 2 +- docs/architecture-migration.md | 3 +- docs/authoring.md | 31 +- docs/commands.md | 51 +- docs/configuration.md | 127 +- docs/development.md | 26 +- docs/interface.md | 17 +- docs/releases.md | 178 +- docs/thumbnail-generation.md | 12 +- package.json | 12 +- packages/slides/README.md | 62 +- packages/slides/package.json | 6 - packages/slides/src/build/index.ts | 11 +- packages/slides/src/build/layers.ts | 24 +- packages/slides/src/build/prepare.ts | 23 +- packages/slides/src/build/runner.ts | 18 +- packages/slides/src/build/styles.ts | 4 +- packages/slides/src/cli/commands/init.ts | 183 ++ packages/slides/src/cli/main.js | 10 + packages/slides/src/model/basemap.ts | 14 +- packages/slides/src/model/content-schema.ts | 40 +- packages/slides/src/model/geojson.ts | 8 +- packages/slides/src/model/map/annotations.ts | 13 +- packages/slides/src/model/map/camera.ts | 34 +- packages/slides/src/model/map/image.ts | 97 - packages/slides/src/model/map/layers.ts | 56 +- packages/slides/src/model/project.ts | 9 +- packages/slides/src/model/settings.ts | 6 +- packages/slides/src/model/types.ts | 20 +- packages/slides/tests/basemap.test.mjs | 50 + packages/slides/tests/cli-config.test.mjs | 20 + packages/slides/tests/dev-smoke.mjs | 1 + packages/slides/tests/init.test.mjs | 192 ++ packages/slides/tests/model-camera.test.mjs | 91 +- packages/slides/tests/model-layers.test.mjs | 131 ++ packages/slides/tests/model-model.test.mjs | 27 + packages/slides/tests/package-smoke.mjs | 12 + .../slides/tests/padding-previews.test.mjs | 56 + packages/slides/tests/release.test.mjs | 109 ++ packages/slides/tests/render-geojson.test.mjs | 44 +- packages/slides/tests/social-images.test.mjs | 9 +- pnpm-lock.yaml | 1672 +---------------- scripts/check-release.mjs | 43 +- scripts/prepare-npm-release.mjs | 78 + scripts/publish-slides.mjs | 5 +- 77 files changed, 2459 insertions(+), 2340 deletions(-) create mode 100644 .changeset/content-independent-development.md create mode 100644 .changeset/fresh-narrative-map.md create mode 100644 .changeset/global-overlay-layers.md create mode 100644 .changeset/optional-basemaps.md create mode 100644 .changeset/quiet-dev-startup.md create mode 100644 .changeset/remove-image-map-mode.md create mode 100644 .changeset/shared-camera-fit.md create mode 100644 .changeset/space-map-comparison.md delete mode 100644 apps/slides/src/lib/components/PanelOverlayToggle.svelte delete mode 100644 apps/slides/src/lib/shared/content-schema.ts create mode 100644 apps/slides/src/lib/shared/map/comparison.ts create mode 100644 apps/slides/src/lib/shared/map/focus.ts delete mode 100644 apps/slides/src/lib/shared/map/image.ts create mode 100644 apps/slides/src/lib/shared/map/layer-transitions.ts create mode 100644 apps/slides/src/lib/shared/panel-scroll.ts delete mode 100644 apps/slides/src/lib/shared/utils.ts create mode 100644 apps/slides/tests/layer-transitions.test.mjs create mode 100644 apps/slides/tests/map-comparison.test.mjs create mode 100644 apps/slides/tests/map-focus.test.mjs create mode 100644 apps/slides/tests/panel-scroll.test.mjs create mode 100644 packages/slides/src/cli/commands/init.ts delete mode 100644 packages/slides/src/model/map/image.ts create mode 100644 packages/slides/tests/basemap.test.mjs create mode 100644 packages/slides/tests/init.test.mjs create mode 100644 packages/slides/tests/model-layers.test.mjs create mode 100644 packages/slides/tests/padding-previews.test.mjs create mode 100644 packages/slides/tests/release.test.mjs create mode 100644 scripts/prepare-npm-release.mjs diff --git a/.changeset/content-independent-development.md b/.changeset/content-independent-development.md new file mode 100644 index 0000000..33dd429 --- /dev/null +++ b/.changeset/content-independent-development.md @@ -0,0 +1,7 @@ +--- +"@allmaps/slides": patch +--- + +Remove unused application files: the old panel toggle component, geometry helpers and content-schema re-export. Drop unused app dependencies on `@sveltejs/adapter-auto` and `@turf/turf`. + +Root development commands now forward to the CLI without selecting a content repository. Pass a directory, for example `pnpm dev ./content/slides-template` or `pnpm build ../my-story`. Update the documentation and use the public Slides template for the integration workflow's application check. diff --git a/.changeset/fresh-narrative-map.md b/.changeset/fresh-narrative-map.md new file mode 100644 index 0000000..5099205 --- /dev/null +++ b/.changeset/fresh-narrative-map.md @@ -0,0 +1,5 @@ +--- +"@allmaps/slides": minor +--- + +Add `slides init [directory]` (also available as `slides create`) to create a minimal English project with configuration, an example slide, package scripts and setup instructions. Interactive runs ask for a title and an optional Protomaps key, both editable later. Skipping the key writes an empty `protomaps.key` and uses no basemap. Use `--title`, `--protomaps-key` and `--yes` for automation; non-interactive runs do not prompt. The starter pins the running Slides version and refuses to overwrite existing files. Cancelling setup leaves no partial project. diff --git a/.changeset/global-overlay-layers.md b/.changeset/global-overlay-layers.md new file mode 100644 index 0000000..d7f1e1e --- /dev/null +++ b/.changeset/global-overlay-layers.md @@ -0,0 +1,11 @@ +--- +"@allmaps/slides": minor +--- + +Add top-level `layers` configuration for shared visibility, opacity and transition defaults. Entries with `layer` customize generated GeoJSON layers; full MapLibre definitions with `id`, `type`, `source`, `layout` and `paint` add custom layers, including text labels from GeoJSON properties. + +Resolve each slide and start screen against these defaults in both the live map and thumbnails. Slide changes no longer leak opacity, visibility or transitions into other slides, regardless of navigation order. Custom layers draw above generated layers in authored order; fill, line and point layers follow normal MapLibre drawing order. Text overlays also receive a glyph endpoint when no basemap is configured. + +Use native MapLibre opacity transitions for user GeoJSON layers when slide visibility changes (300 ms by default). Preserve authored opacity and delay hiding until fade-out completes. Whole-layer opacity lets lines and fills fade while retaining feature styling. Feature-dependent circle and symbol opacity may switch abruptly; use constant opacity for smooth fades. Remove custom animation and opacity-expression rewriting. Initial rendering, reduced-motion preferences and entering full-map mode bypass transitions. No configuration changes are needed. + +Fix the duplicate internal `user-` prefix. Use `-line`, `-fill`, `-point-circle`, `-point-symbol`, or a custom layer's `id` in authored overrides. Remove the extra `user-` from any overrides that worked around the old bug. diff --git a/.changeset/optional-basemaps.md b/.changeset/optional-basemaps.md new file mode 100644 index 0000000..0533151 --- /dev/null +++ b/.changeset/optional-basemaps.md @@ -0,0 +1,5 @@ +--- +"@allmaps/slides": patch +--- + +Show no basemap when no custom map style or Protomaps API key is supplied, in both the application and generated previews. Projects using Protomaps can continue supplying a key in configuration or through `PUBLIC_PROTOMAPS_KEY`. Newly initialized projects no longer need an empty `map.styles` block. diff --git a/.changeset/quiet-dev-startup.md b/.changeset/quiet-dev-startup.md new file mode 100644 index 0000000..81350ff --- /dev/null +++ b/.changeset/quiet-dev-startup.md @@ -0,0 +1,5 @@ +--- +"@allmaps/slides": patch +--- + +Generate SvelteKit's base TypeScript configuration before Vite starts. Fresh development, build and preview runners no longer warn that the extended tsconfig file cannot be found. diff --git a/.changeset/remove-image-map-mode.md b/.changeset/remove-image-map-mode.md new file mode 100644 index 0000000..378a1b5 --- /dev/null +++ b/.changeset/remove-image-map-mode.md @@ -0,0 +1,15 @@ +--- +"@allmaps/slides": minor +--- + +Remove `warpedMaps[].type: Image` and its image-only `region` and `wiggle` options. Warped maps now always load georeference annotations. Remove the generated georeferencing helper, the `model/map/image` export, its Turf dependency, image-specific layer controls and the unused `imageLayer` label. Images no longer trigger automatic basemap hiding or immediate camera transitions. + +Move non-georeferenced images from `warpedMaps` into zoomable IIIF figures in the slide's Markdown: + +```html +
+
Image caption
+
+``` + +Use `data-region="x,y,width,height"` on the figure to keep a crop and `data-rotation` for rotation. Set slide-level `hideBasemap: true` when needed. Zoomable text images and their IIIF services are unchanged. Remove the obsolete options and label from the docs and template references. diff --git a/.changeset/shared-camera-fit.md b/.changeset/shared-camera-fit.md new file mode 100644 index 0000000..265a27a --- /dev/null +++ b/.changeset/shared-camera-fit.md @@ -0,0 +1,11 @@ +--- +"@allmaps/slides": minor +--- + +Use a single slide/start-screen `fit` option for automatic map fitting in the live viewer and generated previews. It uses the same sizing logic as Allmaps' `getMapCenterZoomBearing`: `contain` (the default) shows all map bounds, `cover` fills the available viewport with cropping, and `equal` gives the map bounds the same area as the available viewport. Maps marked `useZoom` still determine resource-scale zoom, and an explicit `location.zoom` takes priority over both. Per-map layer thumbnails and the temporary "Show full map" view continue to show the entire map. + +Make slide/start-screen `padding` a uniform inner margin in pixels, separate from the space reserved by the interface. `fit: cover` with `padding: 0` fills the available layout area without an extra margin. Negative values enlarge the fitting area for extra cropping without moving the layout center. The reading panel's reserved space and camera offset remain controlled by the app. Explicit padding applies to both the live view and generated slide/sharing images. Omitting it preserves the existing margins: 25 pixels live, 20 in slide previews and 32 in sharing images. Per-map layer thumbnails retain their own margin, and the temporary "Show full map" view always uses the normal 25-pixel margin. + +Remove the unused chapter-level `caption`, `freeze` and `contain` declarations from the schema and public types. They had no effect in the bundled app. Per-map `warpedMaps[].caption` remains supported. Custom metadata is still allowed, so old unused keys do not prevent existing slides from loading. + +Remove the unused interface labels `chapterCountSingular`, `chapterCountPlural` and `noCredits`, including the chapter-count keys in `interface.startScreen`. Keep `startButton` and `madeWith` as supported legacy fallbacks, with `interface.text` taking precedence. Remove the obsolete fields, labels and `readMore` example from the documentation and template references. diff --git a/.changeset/space-map-comparison.md b/.changeset/space-map-comparison.md new file mode 100644 index 0000000..7c38efb --- /dev/null +++ b/.changeset/space-map-comparison.md @@ -0,0 +1,15 @@ +--- +"@allmaps/slides": patch +--- + +Replace the backtick shortcut with Space in the central keyboard handler. Hold Space to temporarily hide warped maps for comparison with the basemap, then release it to restore their configured opacity and visibility. Prevent scrolling during the hold, preserve typing and focused controls, and restore the maps when the window loses focus. + +Keep shortcuts working after clicking the map or navigating through chapter links. Reserve keys only for controls that use them, and let image dialogs handle Escape before the slideshow's panel overlays. + +Move focus to the reading panel when scrolling its text, so Space does not remain assigned to a previously clicked map or navigation button. Give the panel a keyboard focus target, preserve text-entry focus, and keep handled shortcuts from interrupting smooth chapter navigation. + +Move focus to the map canvas on wheel and trackpad zoom gestures, so Space is not left assigned to a previous control. Keep native focus handling for clicks and remove the map canvas's browser focus outline. + +Explicitly repaint after changing comparison opacity. Allmaps can omit its change event when combining layer and map-specific opacity, leaving a settled map's old frame on screen until another interaction or tile update. Space now redraws immediately after zooming has finished, without relying on focus changes or ongoing rendering. + +Also request a repaint after applying per-map options, so the layers-panel hide/show button and clickable layer row update a settled map immediately. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bb10a0..fcb4dc2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,30 +1,102 @@ -name: Release notes +name: Publish release on: push: + branches: [main] tags: ['@allmaps/slides@*'] + paths: ['packages/slides/package.json'] workflow_dispatch: inputs: tag: - description: Existing release tag (for example @allmaps/slides@0.1.0-beta.1) - required: true + description: 'Optional existing tag: repair GitHub release only. Leave empty to publish the current version on main.' + required: false type: string concurrency: - group: release-notes-${{ inputs.tag || github.ref_name }} + group: publish-slides + cancel-in-progress: false permissions: - contents: write + contents: read jobs: + publish: + if: github.ref == 'refs/heads/main' && inputs.tag == '' + runs-on: ubuntu-24.04 + environment: npm + timeout-minutes: 20 + permissions: + contents: write + id-token: write + outputs: + release: ${{ steps.plan.outputs.release }} + tag: ${{ steps.plan.outputs.tag }} + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 24 + package-manager-cache: false + - name: Check version and previous publication + id: plan + env: + RELEASE_BEFORE: ${{ github.event.before }} + run: node scripts/prepare-npm-release.mjs + - uses: pnpm/action-setup@v4 + if: steps.plan.outputs.release == 'true' && steps.plan.outputs.published == 'false' + - name: Install npm with trusted publishing support + if: steps.plan.outputs.release == 'true' && steps.plan.outputs.published == 'false' + run: npm install --global npm@11 + - name: Install packaging dependencies + if: steps.plan.outputs.release == 'true' && steps.plan.outputs.published == 'false' + # Packaging does not run the native renderer or need content repositories. + run: pnpm install --frozen-lockfile --ignore-scripts + - name: Build and pack + if: steps.plan.outputs.release == 'true' && steps.plan.outputs.published == 'false' + # pnpm applies publishConfig.exports and removes workspace dependencies. + run: pnpm --filter @allmaps/slides pack --out "$RUNNER_TEMP/slides.tgz" + - name: Publish to npm + if: steps.plan.outputs.release == 'true' && steps.plan.outputs.published == 'false' + env: + NPM_TAG: ${{ steps.plan.outputs.npmTag }} + run: npm publish "$RUNNER_TEMP/slides.tgz" --access public --tag "$NPM_TAG" --provenance + - name: Create and push the release tag + if: steps.plan.outputs.release == 'true' + env: + RELEASE_TAG: ${{ steps.plan.outputs.tag }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + if ! git show-ref --verify --quiet "refs/tags/$RELEASE_TAG"; then + git tag -a "$RELEASE_TAG" "$GITHUB_SHA" -m "$RELEASE_TAG" + fi + gh auth setup-git + git push origin "refs/tags/$RELEASE_TAG" + notes: + needs: publish + # Tags pushed by GITHUB_TOKEN do not start another workflow; create the + # release here too. User-pushed tags and manual repairs keep working. + if: >- + always() && !cancelled() && + ((needs.publish.result == 'success' && needs.publish.outputs.release == 'true') || + startsWith(github.ref, 'refs/tags/@allmaps/slides@') || + (github.event_name == 'workflow_dispatch' && inputs.tag != '')) runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: write env: - RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + RELEASE_TAG: ${{ inputs.tag || needs.publish.outputs.tag || github.ref_name }} steps: - uses: actions/checkout@v6 with: - ref: ${{ inputs.tag || github.ref }} + ref: ${{ env.RELEASE_TAG }} + persist-credentials: false - uses: actions/setup-node@v6 with: node-version: 24 diff --git a/.github/workflows/slides.yml b/.github/workflows/slides.yml index 9e60050..64f69ae 100644 --- a/.github/workflows/slides.yml +++ b/.github/workflows/slides.yml @@ -25,7 +25,7 @@ jobs: run: echo "Documentation-only changes; integration tests were not needed." >> "$GITHUB_STEP_SUMMARY" - name: Check out the integration presentation if: steps.changes.outputs.tests != 'false' - run: git submodule update --init content/gravity-at-sea + run: git submodule update --init content/slides-template - uses: pnpm/action-setup@v4 if: steps.changes.outputs.tests != 'false' with: @@ -45,7 +45,7 @@ jobs: run: | pnpm -r test pnpm --filter @allmaps/slides --filter @allmaps/iiif --filter @allmaps/static-render check - pnpm exec slides check ./content/gravity-at-sea + pnpm check ./content/slides-template - name: Native pixel alignment if: steps.changes.outputs.tests != 'false' run: xvfb-run -a pnpm --filter @allmaps/static-render test:native diff --git a/README.md b/README.md index aa0907c..0e8624f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ _Allmaps Slides is currently in beta. Try it out and share your feedback, ideas ## Create a slideshow -Start with the [package quick start](packages/slides/README.md) or the +Create a minimal project with `slides init` using the +[package quick start](packages/slides/README.md), or copy the [GitHub template repository](https://github.com/allmaps/slides-template), which includes a small example and annotated configuration files. @@ -46,8 +47,8 @@ when copying or forking it. - [Architecture](docs/architecture.md): application, package and content boundaries. - [Content examples](content): independent repositories included as Git submodules. -`pnpm bundle` builds the npm package. `pnpm build` builds the Gravity at Sea -site after its content submodule has been initialized. +`pnpm bundle` builds the npm package. Root shortcuts take a content directory: +for example, `pnpm build ./content/slides-template` builds the template site. ## Inspiration and previous versions diff --git a/apps/slides/package.json b/apps/slides/package.json index ab2f93b..a900c01 100644 --- a/apps/slides/package.json +++ b/apps/slides/package.json @@ -14,7 +14,6 @@ }, "devDependencies": { "@allmaps/annotation": "1.0.0-beta.38", - "@sveltejs/adapter-auto": "^7.0.0", "@sveltejs/kit": "^2.50.2", "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.2.0", @@ -34,7 +33,6 @@ "@allmaps/stdlib": "1.0.0-beta.42", "@lucide/svelte": "^1.34.0", "@sveltejs/adapter-static": "^3.0.10", - "@turf/turf": "^7.3.5", "maplibre-gl": "^6.11.2", "pmtiles": "^4.4.1" } diff --git a/apps/slides/src/lib/components/Map.svelte b/apps/slides/src/lib/components/Map.svelte index a6d406b..c266c45 100644 --- a/apps/slides/src/lib/components/Map.svelte +++ b/apps/slides/src/lib/components/Map.svelte @@ -26,18 +26,21 @@ import { getAnnotationsFromChapters, getUniqueAnnotations, - hidesBasemap, getWarpedMapOptions, } from "$lib/shared/map/annotations"; import { resolveChapterCamera, + getCameraPadding, getCameraLayoutOptions, type CameraLayoutOptions, } from "$lib/shared/map/camera"; import { constrainSlideshowCamera } from "$lib/shared/map/constraints"; import { withBaseUrl } from "$lib/shared/paths"; - import { createFauxGeoreferencedMap } from "$lib/shared/map/image"; - import { prepareUserLayers, getUserLayerChange } from "$lib/shared/map/layers"; + import { applyUserLayerChanges, getUserLayerGlyphs } from "$lib/shared/map/layers"; + import { createLayerTransitions } from "$lib/shared/map/layer-transitions"; + import { focusMapOnWheel } from "$lib/shared/map/focus"; + import { setComparisonOpacity } from "$lib/shared/map/comparison"; + import { LAYER_TYPES } from "$lib/shared/settings"; import { slidesConfig } from "$lib/shared/app-config"; import { getBasemapLayerVisibility as resolveLayerVisibility, @@ -82,12 +85,13 @@ slideshowMapConfig?: MapConfig; highlight?: string; hiddenWarpedMapUrls?: string[]; + temporarilyHideMaps?: boolean; focusedWarpedMapUrl?: string; showLabels?: boolean; anticipate?: boolean; layoutRevision?: number; resetSignal?: number; - padding?: number | PaddingOptions; + layoutPadding?: number | PaddingOptions; controlsVisible?: boolean; onBasemapAttribution?: (attributions: string[]) => void; debug?: boolean; @@ -105,12 +109,13 @@ slideshowMapConfig, highlight, hiddenWarpedMapUrls = [], + temporarilyHideMaps = false, focusedWarpedMapUrl, showLabels, anticipate, layoutRevision = 0, resetSignal = 0, - padding, + layoutPadding = 0, controlsVisible = true, onBasemapAttribution, debug = dev, @@ -124,25 +129,15 @@ ); let currentWarpedMaps = $derived(currentChapter?.warpedMaps); let currentLayers = $derived(currentChapter?.layers); - let currentImageSlide = $derived( - currentWarpedMaps?.some((warpedMaps) => warpedMaps.type === "Image") || - false, - ); const focusedMapUrl = $derived(currentWarpedMaps?.some(({ url }) => url === focusedWarpedMapUrl) ? focusedWarpedMapUrl : undefined); - let currentHideBasemap = $derived(!!focusedMapUrl || hidesBasemap(currentChapter ?? {})); - let currentPadding = $derived.by(() => { - if (typeof padding === "number" || padding === undefined) { - return padding ?? DEFAULT_PADDING; - } - - return { - top: padding.top ?? DEFAULT_PADDING, - right: padding.right ?? DEFAULT_PADDING, - bottom: padding.bottom ?? DEFAULT_PADDING, - left: padding.left ?? DEFAULT_PADDING, - }; - }); + let currentHideBasemap = $derived(!!focusedMapUrl || !!currentChapter?.hideBasemap); + // Full-map mode restores the normal margin so the whole map stays visible. + // Its native MapLibre fitting path also requires non-negative padding. + const currentPadding = $derived(getCameraPadding( + layoutPadding, + focusedMapUrl ? DEFAULT_PADDING : currentChapter?.padding ?? DEFAULT_PADDING, + )); let sprite = $derived(currentChapter?.sprite); const theme = $derived((isDarkMode ? "dark" : "light") as ThemeMode); @@ -190,6 +185,7 @@ ); let map: maplibregl.Map; + let userLayerTransitions: ReturnType; let container: HTMLElement; let mapLoaded = $state(false); let currentBearing = $state(0); @@ -375,8 +371,11 @@ }; const setStyleAssets = (basemapStyle: ResolvedBasemapStyle) => { - if (basemapStyle.glyphs) { - map.setGlyphs(basemapStyle.glyphs); + const glyphs = basemapStyle.glyphs ?? getUserLayerGlyphs( + Array.isArray(layers) ? layers : layers ? [layers] : [], + ); + if (glyphs) { + map.setGlyphs(glyphs); } if (typeof basemapStyle.sprite === "string") { @@ -609,7 +608,7 @@ } const initialCameraUpdate = start; - if (currentImageSlide || initialCameraUpdate) { + if (initialCameraUpdate) { flyToOptions.duration = 0; } else if ( (!useCurrentLocation || !currentLocation.duration) && @@ -665,42 +664,25 @@ console.log("Loading warped map...", annotation); } - if (annotation.type === "Image") { - // Create a 'fake' annotation for the image, in order to add it to the map - const georeferencedMap = await createFauxGeoreferencedMap(url, { - region: annotation.region, - wiggle: annotation.wiggle, - }); + const snapshotUrl = annotationUrls[url]; + const georeferenceAnnotation = await fetch(snapshotUrl ? withBaseUrl(snapshotUrl) : url).then((response) => + response.json(), + ); - if (destroyed) return; + if (destroyed) return; - const options = { - ...getWarpedMapOptions(annotation, theme), - visible: false, - }; - const id = warpedMapLayer.addGeoreferencedMap(georeferencedMap, options); - rememberMapIdsForAnnotation(url, [id]); - } else { - const snapshotUrl = annotationUrls[url]; - const georeferenceAnnotation = await fetch(snapshotUrl ? withBaseUrl(snapshotUrl) : url).then((response) => - response.json(), - ); - - if (destroyed) return; - - const options = { - ...getWarpedMapOptions(annotation, theme), - visible: false, - }; - const results = warpedMapLayer.addGeoreferenceAnnotation(georeferenceAnnotation, options); + const options = { + ...getWarpedMapOptions(annotation, theme), + visible: false, + }; + const results = warpedMapLayer.addGeoreferenceAnnotation(georeferenceAnnotation, options); - const mapIds = results.flatMap((result) => result.ok ? [result.mapId] : []); - const errors = results.flatMap((result) => result.ok ? [] : [result.error]); - if (errors.length) { - console.error("Failed to add georeferenced map for", url, errors); - } - rememberMapIdsForAnnotation(url, mapIds); + const mapIds = results.flatMap((result) => result.ok ? [result.mapId] : []); + const errors = results.flatMap((result) => result.ok ? [] : [result.error]); + if (errors.length) { + console.error("Failed to add georeferenced map for", url, errors); } + rememberMapIdsForAnnotation(url, mapIds); } catch (error) { if (!destroyed) { console.error("Failed to load georeferenced map for", url, error); @@ -851,6 +833,9 @@ .filter((option) => option !== "visible" && option !== "opacity"), }, ); + // Layer-panel visibility/opacity updates must redraw even when Allmaps + // emits no change event and MapLibre has stopped rendering after zooming. + map.triggerRepaint(); }); } @@ -907,17 +892,6 @@ }); }; - function toggleVisibility(event: KeyboardEvent) { - if (event.repeat) return; - if (mapLoaded && event.code === "Backquote") { - const opacity = warpedMapLayer.getOpacity(); - warpedMapLayer.setLayerOptions( - { opacity: opacity === 0 ? 1 : 0 }, - { animate: ANIMATE_WARPED_MAP_OPACITY }, - ); - } - } - const resetNorth = () => { if (!mapLoaded) return; @@ -978,36 +952,35 @@ } const layerList = Array.isArray(layers) ? layers : [layers]; - prepareUserLayers(layerList) - .forEach((layer) => { - const vectorTypes = ["symbol", "circle", "line", "raster", "fill"]; - const moveToFront = vectorTypes.includes(layer.type); - map.addLayer(layer, moveToFront ? undefined : "warped-map-layer"); - }); + for (const layer of layerList) map.addLayer(layer); } function setUserLayerState() { if (!mapLoaded || !layers) return; const layerList = Array.isArray(layers) ? layers : [layers]; - for (const layer of prepareUserLayers(layerList)) { + for (const layer of applyUserLayerChanges(layerList, currentLayers)) { if (!map.getLayer(layer.id)) continue; - const change = currentLayers?.find((change) => `user-${change.layer}` === layer.id); const source = "source" in layer ? sources?.[layer.source] : undefined; const visibility = focusedMapUrl && source?.type === "geojson" ? "none" - : change?.visibility ?? layer.layout?.visibility ?? "visible"; - // Full-map mode temporarily hides content overlays, including layers - // with no per-slide overrides. Restore the slide's visibility on exit. - map.setLayoutProperty(layer.id, "visibility", visibility); - - if (!change) continue; - const { paint, duration } = getUserLayerChange(change, layer.type); - for (const [property, value] of Object.entries(paint)) { - const options = duration ? { duration } : {}; - if (duration) map.setPaintProperty(layer.id, `${property}-transition` as keyof maplibregl.AllPaintProperties, options); - map.setPaintProperty(layer.id, property as keyof maplibregl.AllPaintProperties, value); + : layer.layout?.visibility ?? "visible"; + if (source?.type === "geojson") { + // Full-map mode and reduced motion bypass native opacity transitions. + userLayerTransitions.set(layer, visibility !== "none", + !!focusedMapUrl || window.matchMedia("(prefers-reduced-motion: reduce)").matches); + continue; } + const paint = layer.paint as Partial | undefined; + for (const property of LAYER_TYPES[layer.type as keyof typeof LAYER_TYPES] ?? []) { + // Resolve each slide from global defaults, including expressions and + // transitions. Omitting an override must not retain the previous slide. + const transition = `${property}-transition` as keyof maplibregl.AllPaintProperties; + map.setPaintProperty(layer.id, transition, paint?.[transition]); + const opacityProperty = property as keyof maplibregl.AllPaintProperties; + map.setPaintProperty(layer.id, opacityProperty, paint?.[opacityProperty]); + } + map.setLayoutProperty(layer.id, "visibility", visibility); } } @@ -1054,6 +1027,10 @@ }; }); $effect(applyWarpedMapState); + $effect(() => { + if (!mapLoaded) return; + setComparisonOpacity(map, warpedMapLayer, temporarilyHideMaps); + }); $effect(setChapterCamera); $effect(setUserLayerState); $effect(() => { @@ -1111,6 +1088,9 @@ currentBearing = map.getBearing(); }; + userLayerTransitions = createLayerTransitions(map); + const removeMapFocusListeners = focusMapOnWheel(map.getCanvas()); + map.on("move", updateBearing); // TileJSON may supply attribution after its style source was installed. map.on("sourcedata", (event) => { @@ -1164,6 +1144,8 @@ return () => { destroyed = true; + removeMapFocusListeners(); + userLayerTransitions.destroy(); if (mapLoaded) { warpedMapLayer.clear(); } @@ -1172,10 +1154,8 @@ }); - -
-
+