diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 218ad1c..2dbe7a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,93 +3,153 @@ name: "Release: Create Release" on: workflow_dispatch: +permissions: + contents: read + +# Two runs at once would resolve the same version from the tags. Queue rather +# than cancel: a cancelled run can leave the bump pushed while the release is +# not. +concurrency: + group: release + cancel-in-progress: false + jobs: release: runs-on: ubuntu-latest - - permissions: - contents: write - id-token: write - pull-requests: write - - env: - BRANCH: ci/bump-version + timeout-minutes: 15 steps: + - name: Refuse to release from anything but the default branch + if: ${{ github.ref_name != github.event.repository.default_branch }} + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + REF_NAME: ${{ github.ref_name }} + shell: bash + run: | + echo "::error::Releases are cut from ${DEFAULT_BRANCH}, not ${REF_NAME}." + exit 1 + + # The App only, with no fallback token: the default token cannot pass the + # pull request rule of the default branch, and a release it creates + # triggers neither the image build nor the documentation deploy. + - name: Setup Git User + uses: mcanouil/quarto-workflows/.github/actions/setup-git-user@v2 + id: setup-git-user + with: + gh-app-id: ${{ vars.APP_ID }} + app-key: ${{ secrets.APP_KEY }} + - name: Checkout repository uses: actions/checkout@v7 with: - persist-credentials: true - fetch-tags: true + token: ${{ steps.setup-git-user.outputs.token }} + # The whole history and every tag, since the same-day suffix is + # resolved from the tags already cut. fetch-depth: 0 - - name: Create GitHub App token - uses: actions/create-github-app-token@v3 - id: app-token + # The first release of a day is the bare date; every further one carries + # the count of releases cut that day, such as `2026.10.16-2`. An empty + # Unreleased section in CHANGELOG.md stops the release here. + - name: Bump the version + id: bump + uses: mcanouil/release-actions/bump@v1 with: - client-id: ${{ vars.APP_ID }} - private-key: ${{ secrets.APP_KEY }} - - - name: Get GitHub App User ID - id: get-user-id - run: echo "user-id=$(gh api "/users/${{ steps.app-token.outputs.app-slug }}[bot]" --jq .id)" >> "$GITHUB_OUTPUT" + bump: calver + version-source: tag + targets: | + - file: CITATION.cff + pattern: '^version: {version}$' + - file: CITATION.cff + pattern: '^date-released: "{date}"$' + + # The release step refuses an existing tag or release too, but only after + # the push, which would leave the bump on the default branch. + - name: Refuse an existing tag or release env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - - - name: Git Config + GH_TOKEN: ${{ steps.setup-git-user.outputs.token }} + VERSION: ${{ steps.bump.outputs.version }} + shell: bash run: | - git config --global user.name '${{ steps.app-token.outputs.app-slug }}[bot]' - git config --global user.email '${{ steps.get-user-id.outputs.user-id }}+${{ steps.app-token.outputs.app-slug }}[bot]@users.noreply.github.com' + set -euo pipefail + STATUS=0 + git ls-remote --exit-code --tags origin "refs/tags/${VERSION}" > /dev/null || STATUS=$? + case "${STATUS}" in + 0) echo "::error::Tag ${VERSION} already exists. Delete it, or create its release by hand, then dispatch this workflow again."; exit 1 ;; + 2) ;; + *) echo "::error::Could not list the tags of origin (git exit ${STATUS})."; exit 1 ;; + esac + if OUTPUT=$(gh release view "${VERSION}" 2>&1); then + echo "::error::A release ${VERSION} already exists, perhaps as a draft. Delete it, then dispatch this workflow again." + exit 1 + fi + if ! grep -qi 'release not found' <<< "${OUTPUT}"; then + echo "::error::Could not check for release ${VERSION}: ${OUTPUT}" + exit 1 + fi - - name: Bump Version - id: bump-version + # The App is exempt from the pull request rule of the default branch, and + # [skip ci] keeps CI from running again on a tree it has checked. + - name: Push the version + id: push env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} - COMMIT: "ci: bump version for release :rocket:" + VERSION: ${{ steps.bump.outputs.version }} + FILES: ${{ steps.bump.outputs.files }} + shell: bash run: | - if git show-ref --quiet refs/heads/${BRANCH}; then - echo "Branch ${BRANCH} already exists." - git branch -D "${BRANCH}" - git push origin --delete "${BRANCH}" + set -euo pipefail + printf '%s\n' "${FILES}" | git add --pathspec-from-file=- + git commit -m "ci: release ${VERSION} [skip ci]" + if ! git push origin "HEAD:refs/heads/${GITHUB_REF_NAME}"; then + echo "::error::The App could not push the bump to ${GITHUB_REF_NAME}. If the push output says the branch moved, dispatch this workflow again; otherwise add the App to the bypass list of the branch ruleset first." + exit 1 fi + echo "sha=$(git rev-parse HEAD)" >> "${GITHUB_OUTPUT}" - echo "$(git tag -l --sort=-v:refname | head -n 5)" - - git checkout -b "${BRANCH}" - - DATE=$(date +%Y-%m-%d) - BASE_VERSION=$(date +%Y.%m.%d) - - SUFFIX=1 - VERSION="${BASE_VERSION}" - while [ -n "$(git tag -l "${VERSION}")" ]; do - echo "Tag ${VERSION} already exists, incrementing suffix..." - SUFFIX=$((SUFFIX + 1)) - VERSION="${BASE_VERSION}-${SUFFIX}" - done - echo "New version will be: ${VERSION}" - sed -i "s/^version:.*/version: ${VERSION}/" CITATION.cff - sed -i "s/^date-released:.*/date-released: \"${DATE}\"/" CITATION.cff - - git add CITATION.cff || echo "No changes to add" - git commit -m "${COMMIT}" || echo "No changes to commit" - git push --force origin ${BRANCH} || echo "No changes to push" - - sleep 5 - gh pr create --fill-first --base "main" --head "${BRANCH}" --label "Type: CI/CD :robot:" - sleep 2 - gh pr merge --auto --squash --delete-branch - - - name: Update branch + - name: Compose the release body + id: notes + uses: mcanouil/release-actions/notes@v1 + with: + notes-file: ${{ steps.bump.outputs.notes-file }} + tag: ${{ steps.bump.outputs.version }} + target: ${{ steps.push.outputs.sha }} + generate-notes: true + token: ${{ steps.setup-git-user.outputs.token }} + + # A further release of a day is titled with its count, such as + # `2026-10-16 (2)`. + - name: Resolve the release title + id: title + env: + VERSION: ${{ steps.bump.outputs.version }} + DATE: ${{ steps.bump.outputs.date }} + shell: bash run: | - git fetch --all - git checkout main - git pull origin main + set -euo pipefail + TITLE="${DATE}" + if [[ "${VERSION}" == *-* ]]; then + TITLE="${DATE} (${VERSION##*-})" + fi + echo "title=${TITLE}" >> "${GITHUB_OUTPUT}" - - name: Release extension on GitHub + # Created as the App, so that the published release starts the image + # build and the documentation deploy. + - name: Release on GitHub + uses: mcanouil/release-actions/release@v1 + with: + version: ${{ steps.bump.outputs.version }} + body-file: ${{ steps.notes.outputs.body-file }} + title: ${{ steps.title.outputs.title }} + target: ${{ steps.push.outputs.sha }} + latest: true + token: ${{ steps.setup-git-user.outputs.token }} + + # A new dispatch would stop on the emptied Unreleased section, so the + # pushed bump has to go first. + - name: Explain the recovery + if: ${{ failure() && steps.push.outputs.sha != '' }} env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ steps.bump.outputs.version }} + SHA: ${{ steps.push.outputs.sha }} + shell: bash run: | - DATE=$(grep "^date-released:" CITATION.cff | sed 's/date-released: //;s/"//g') - VERSION=$(grep "^version:" CITATION.cff | sed 's/version: //;s/"//g') - gh release create ${VERSION} --title ${DATE} --generate-notes + echo "::error::${GITHUB_REF_NAME} holds the bump ${SHA} for ${VERSION}, but the release failed. Create release ${VERSION} on ${SHA} by hand, or revert that commit with a pull request and dispatch this workflow again." diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..58fd187 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## Unreleased + +- feat: Release notes now come from this changelog. (#182) +- feat: The documentation website has icons and a social preview card. (#165) +- fix: Screen readers no longer read the navigation bar and footer icons twice. diff --git a/docs/_extensions/mcanouil/atelier/_extension.yml b/docs/_extensions/mcanouil/atelier/_extension.yml index f05145e..f684d5a 100644 --- a/docs/_extensions/mcanouil/atelier/_extension.yml +++ b/docs/_extensions/mcanouil/atelier/_extension.yml @@ -1,6 +1,6 @@ title: Atelier author: Mickaƫl Canouil -version: 0.10.5 +version: 0.10.7 quarto-required: ">=1.9.36" contributes: project: @@ -84,5 +84,5 @@ contributes: - file: html/scripts/ordinal-dates.html - file: html/scripts/a11y-fixes.html - file: html/scripts/navbar-tooltips.html -source: mcanouil/quarto-atelier@0.10.5 +source: mcanouil/quarto-atelier@0.10.7 source-type: github diff --git a/docs/_extensions/mcanouil/atelier/html/chrome.scss b/docs/_extensions/mcanouil/atelier/html/chrome.scss index fe2aa86..c1e84fc 100644 --- a/docs/_extensions/mcanouil/atelier/html/chrome.scss +++ b/docs/_extensions/mcanouil/atelier/html/chrome.scss @@ -1,3 +1,19 @@ +/*-- scss:functions --*/ + +// Mix $color towards black or white, as Quarto's `theme-contrast()` does, until +// it reads at 4.5:1 on its own soft tint: $color at $alpha over $bg. The tint +// darkens with the colour, so it is recomputed at every step. +@function atelier-contrast-on-tint($color, $bg, $alpha) { + $toward: if(tone($bg) == "light", #000, #fff); + @for $step from 0 through 100 { + $candidate: mix($toward, $color, $step * 1%); + @if quarto-contrast($candidate, mix($candidate, $bg, $alpha * 100%)) >= 4.5 { + @return $candidate; + } + } + @return $toward; +} + /*-- scss:defaults --*/ // =========================================================================== @@ -58,11 +74,10 @@ $atelier-chrome-bg: if( ) ) !default; -// Both inks are run through Quarto's own `theme-contrast()`, which mixes a -// colour towards black or white only until it clears the ratio asked of it and -// returns it untouched otherwise. In `auto` that matters for the accent: a -// brand primary is picked against the page, and the bar is a tint of the page, -// so a primary sitting just above 4.5:1 on the page can fall under it here. +// Both inks are mixed towards black or white only until they clear 4.5:1, and +// returned untouched when they already do; a ratio that cannot be met ends at +// black or white. In `auto` the bar is a tint of the page, so a brand colour +// sitting just above 4.5:1 on the page can fall under it here. // Setting either variable in a project skips the correction, since a `!default` // assignment is not evaluated at all once the variable is set. $atelier-chrome-fg: theme-contrast( @@ -79,7 +94,14 @@ $atelier-chrome-fg: theme-contrast( "AA" ) !default; -$atelier-chrome-accent: theme-contrast( +// The surface of dropdown menus, a step from the bar towards the ink. +$atelier-chrome-surface: mix($atelier-chrome-bg, $atelier-chrome-fg, 90%) !default; + +// The accent sits on the bar, and on the accent-soft pill of the active sidebar +// item and of a hovered dropdown item. The pill over the surface is the closest +// of these to the accent, so holding the accent to AA there covers the rest. +$atelier-chrome-accent-soft-alpha: 0.16 !default; +$atelier-chrome-accent: atelier-contrast-on-tint( if( $atelier-chrome == dark, $atelier-chrome-dark-accent, @@ -89,15 +111,33 @@ $atelier-chrome-accent: theme-contrast( $atelier-chrome-page-accent ) ), - $atelier-chrome-bg, - "AA" + $atelier-chrome-surface, + $atelier-chrome-accent-soft-alpha ) !default; // Everything else follows from that triple, so a project re-points three // colours rather than eight. -$atelier-chrome-surface: mix($atelier-chrome-bg, $atelier-chrome-fg, 90%) !default; -$atelier-chrome-muted: mix($atelier-chrome-fg, $atelier-chrome-bg, 65%) !default; -$atelier-chrome-accent-soft: rgba($atelier-chrome-accent, 0.16) !default; +$atelier-chrome-muted: theme-contrast( + mix($atelier-chrome-fg, $atelier-chrome-bg, 65%), + $atelier-chrome-bg, + "AA" +) !default; +$atelier-chrome-accent-soft: rgba($atelier-chrome-accent, $atelier-chrome-accent-soft-alpha) !default; + +// =========================================================================== +// PAGE VARIABLES +// =========================================================================== +// The website listing layer pins $text-muted to a fixed #6c757d ahead of this +// file, which is under 4.5:1 on most pages; derive it from the page instead, +// held to AA. A `!default` would lose to that layer, so it is handed over by +// plain assignment, like the chrome variables below; a project sets +// $atelier-text-muted instead. +$atelier-text-muted: theme-contrast( + mix($atelier-chrome-page-fg, $atelier-chrome-page-bg, 65%), + $atelier-chrome-page-bg, + "AA" +) !default; +$text-muted: $atelier-text-muted; // A hairline between two surfaces, which is what the bar edges and the section // dividers want. diff --git a/docs/_extensions/mcanouil/atelier/html/scripts/a11y-fixes.html b/docs/_extensions/mcanouil/atelier/html/scripts/a11y-fixes.html index 3b57b7b..fde1d48 100644 --- a/docs/_extensions/mcanouil/atelier/html/scripts/a11y-fixes.html +++ b/docs/_extensions/mcanouil/atelier/html/scripts/a11y-fixes.html @@ -7,6 +7,7 @@ document.addEventListener("DOMContentLoaded", () => { // Logo link: Quarto emits with an inner , // but the accessible name is lost when the image is styled away. + // Related upstream: https://github.com/quarto-dev/quarto-cli/issues/14660 const siteName = ( document.querySelector(".navbar-title")?.textContent || document.title || @@ -19,25 +20,54 @@ }); // Colour scheme toggle: Quarto adds title= but no aria-label. + // Fixed upstream in Quarto 1.11.2: https://github.com/quarto-dev/quarto-cli/issues/13463 document.querySelectorAll("a.quarto-color-scheme-toggle:not([aria-label])").forEach((a) => { a.setAttribute("aria-label", a.getAttribute("title") || "Toggle dark mode"); }); - // Search input: Quarto's inline control has no visible label. - const searchInput = document.querySelector( - ".aa-Input, input.aa-Input, input[placeholder='Search'], input[type='search']", - ); - if (searchInput && !searchInput.getAttribute("aria-label")) { - searchInput.setAttribute("aria-label", "Search the site"); - } + // Search input: Quarto labels the field through aria-labelledby, which + // names it "Submit" (textbox) or nothing (overlay), and that label wins + // over an aria-label on the input. Name the referenced label with Quarto's + // own localised search label, and again when a search host or the overlay + // renders a new one. + // Upstream: https://github.com/quarto-dev/quarto-cli/issues/15062 + const searchName = (() => { + try { + const options = document.getElementById("quarto-search-options"); + return (options && JSON.parse(options.textContent).language?.["search-label"]) || "Search"; + } catch { + return "Search"; + } + })(); + const setSearchName = (element) => { + if (element.getAttribute("aria-label") !== searchName) { + element.setAttribute("aria-label", searchName); + } + }; + const nameSearchInputs = () => { + document.querySelectorAll(".aa-Input").forEach((input) => { + const labelId = (input.getAttribute("aria-labelledby") || "").trim().split(/\s+/)[0]; + setSearchName((labelId && document.getElementById(labelId)) || input); + }); + // The overlay trigger carries autocomplete's English "Search" title. + document.querySelectorAll(".aa-DetachedSearchButton").forEach(setSearchName); + }; + nameSearchInputs(); + const searchObserver = new MutationObserver(nameSearchInputs); + searchObserver.observe(document.body, { childList: true }); + document.querySelectorAll('[id="quarto-search"]').forEach((host) => { + searchObserver.observe(host, { childList: true, subtree: true }); + }); // CSL bibliography: Quarto emits role="listitem" without a role="list" parent. + // Upstream: https://github.com/quarto-dev/quarto-cli/issues/14755 document.querySelectorAll('[role="listitem"]').forEach((el) => { el.removeAttribute("role"); }); // Disambiguate landmarks. The page title-block is also a
, which is a // duplicate banner alongside the site navbar; demote it and label the rest. + // Upstream: https://github.com/quarto-dev/quarto-cli/issues/14375 (banner), closed https://github.com/quarto-dev/quarto-cli/issues/14376 (nav labels) const siteHeader = document.getElementById("quarto-header"); if (siteHeader && !siteHeader.getAttribute("aria-label")) { siteHeader.setAttribute("aria-label", "Site banner"); @@ -57,6 +87,7 @@ // Listing category sidebar: Quarto emits
Categories
after the page //

, breaking heading order. Re-tag to

so the outline stays monotonic. + // Upstream: https://github.com/quarto-dev/quarto-cli/issues/14384 document.querySelectorAll("h5.quarto-listing-category-title").forEach((h5) => { const h2 = document.createElement("h2"); for (const attr of h5.attributes) h2.setAttribute(attr.name, attr.value); @@ -66,6 +97,7 @@ // Wrap orphan listing landmarks so the region rule passes; skip nodes already // inside a landmark, and the mobile sidebar-toggle clones (non-unique). + // Related upstream: https://github.com/quarto-dev/quarto-cli/issues/14377 const insideLandmark = (el) => el && el.closest( diff --git a/docs/_extensions/mcanouil/atelier/html/theme.scss b/docs/_extensions/mcanouil/atelier/html/theme.scss index 862b892..408793f 100644 --- a/docs/_extensions/mcanouil/atelier/html/theme.scss +++ b/docs/_extensions/mcanouil/atelier/html/theme.scss @@ -33,8 +33,12 @@ $btn-close-white-filter: none !default; color: $fg; } + // The colour above outranks Bootstrap's colour for the active item but not + // its fill, so the active item takes the hover pair too, which stays AA. .dropdown-item:hover, - .dropdown-item:focus { + .dropdown-item:focus, + .dropdown-item.active, + .dropdown-item:active { background: $accent-soft; color: $accent; } @@ -179,57 +183,211 @@ embed.embed-art:not([width]) { // body.quarto-dark / .quarto-light and swaps the whole stylesheet bundle, so an // html prefix (not the never-set data-bs-theme) raises specificity over // Quarto's rule; body-mix() derives the surface per bundle, correct in both. -html div.sourceCode, +html div.sourceCode { + @if $code-block-bg { + background-color: body-mix(95%); + } + border: 1px solid body-mix(85%); + border-radius: $border-radius-sm; +} + +// Quarto zeroes the padding of every pre, and pads pre.sourceCode only when +// $code-block-bg is set. The border is drawn either way, so always inset the +// code inside it. With $code-block-bg off, Quarto forces the page colour onto +// div.sourceCode, so blocks become outlines and a plain block drops its tint. html pre:not(.sourceCode) { - background-color: body-mix(95%); + @if $code-block-bg { + background-color: body-mix(95%); + } @else { + background-color: transparent; + } border: 1px solid body-mix(85%); border-radius: $border-radius-sm; + padding: $code-block-bg-padding; } html pre.sourceCode { background-color: transparent; border: none; + @if not $code-block-bg { + padding: $code-block-bg-padding; + } } -// Code Window extension: its style.css uses the CSS system colour `Canvas`, -// which follows the OS scheme rather than the active colour-scheme bundle. -// Re-skin the container and title bar with body-mix(); a leading `div` keeps -// these authoritative over the extension's class-only rules. -div.code-with-filename { - background-color: body-mix(95%); - border-color: body-mix(85%); +// Quarto drops the left inset of highlighted code in a callout; the frame is +// still drawn there, so restore it. +// Upstream: https://github.com/quarto-dev/quarto-cli/issues/15058 +html .callout pre.sourceCode { + padding-left: $code-block-bg-padding; } -div.code-with-filename .code-with-filename-file { - background-color: body-mix(90%); - border-bottom-color: body-mix(85%); +// Annotated code keeps Quarto's flush rows, which the rules above outrank. +html pre.code-annotation-code { + padding-top: 0; + padding-bottom: 0; } -div.code-with-filename .code-with-filename-file pre { - color: rgba($body-color, 0.6); +// Quarto paints the annotation gutter from a fixed light grey in both schemes, +// over a page-coloured backing that hides code scrolled under it; a strip on +// the code surface. Clear the grey and back the gutter with the code surface. +// Upstream: https://github.com/quarto-dev/quarto-cli/issues/15059 +html .code-annotation-gutter { + background-color: transparent; } -div.code-with-filename div.sourceCode, -div.code-with-filename pre.sourceCode { +@if $code-block-bg { + html .code-annotation-gutter-bg { + background-color: body-mix(95%); + } +} + +// A filename title bar holds its name in a plain `pre`, which the code surface +// rules above would frame and pad; Quarto's own bar and the Code Window bar +// both need it bare. +html .code-with-filename .code-with-filename-file pre { + padding: 0; + background-color: transparent; border: none; border-radius: 0; +} + +// Code Window extension: it reads its surfaces from these variables and +// derives the border, filename and chip colours from --code-window-bg. Its +// defaults sit in :where(:root), so this rule wins in each scheme bundle. +:root { + --code-window-bg: #{body-mix(95%)}; + --code-window-title-bg: #{body-mix(90%)}; +} + +// The window draws the frame and the surface, so code inside it takes the +// window colour. Quarto's own filename block, without the extension, has no +// surface of its own and gets the same colour from the variable above, while +// its title bar is a fixed grey (`#555` in dark) that the next rule replaces; +// the dark selector matches Quarto's. A plain +// `pre` needs the border reset too: `html pre:not(.sourceCode)` outranks the +// extension's own reset. The window is a `div`, or a `details` once +// collapsible, so match the class alone. With $code-block-bg off, Quarto's page +// colour on div.sourceCode is `!important`, so this needs `!important` too. +html .code-with-filename div.sourceCode { + @if $code-block-bg { + background-color: var(--code-window-bg); + } @else { + background-color: var(--code-window-bg) !important; + } +} + +html .code-with-filename .code-with-filename-file, +html .quarto-dark .code-with-filename .code-with-filename-file { + background-color: var(--code-window-title-bg); +} + +// The window surface also backs the annotation gutter of code inside it. +html .code-with-filename .code-annotation-gutter-bg { + background-color: var(--code-window-bg); +} + +// Quarto greys every `details > summary`, and a collapsible window's title bar +// is one; the filename mixes from that grey and drops below 4.5:1. +html .code-with-filename > summary { + color: inherit; +} + +html .code-with-filename > pre:not(.sourceCode) { background-color: transparent; + border: none; + border-radius: 0; } // Inline code: Bootstrap/Pandoc leave a near-white patch on dark surfaces. +// The patch is darker than the page, so a primary that passes on the page can +// fail on it; hold the ink to AA on the patch. code:not(.sourceCode):not(pre code) { - color: $primary; + color: theme-contrast($primary, body-mix(90%), "AA"); background-color: body-mix(90%); padding: 0.1em 0.35em; border-radius: 0.3em; } +// Quarto fades a callout title with opacity, which also fades the inline code +// in it below AA. Fade the title text by its colour instead, so code keeps its +// own ink. +html .callout.callout-style-default > .callout-header { + opacity: 1; + color: rgba($body-color, 0.85); +} + +html .callout.callout-style-simple > .callout-header { + opacity: 1; + color: rgba($body-color, 0.75); +} + +// A callout with no type gets a $text-muted title band, a mid grey that title +// text cannot clear AA on. Tint it from the page as the typed ones are tinted +// from their colour. +div.callout.callout-style-default:where( + :not(.callout-note, .callout-tip, .callout-warning, .callout-caution, .callout-important) + ) + > .callout-header { + background-color: mix($body-bg, $text-muted, 70%); +} + // Quarto bakes a fixed grey into the copy button, which reads poorly on the // body-mix() code surface; use the palette accent in both schemes. .code-copy-button { color: $primary; } +// Quarto moves the copy button clear of the annotation gutter with +// `.code-annotated .code-copy-button`, but the button is a sibling of the +// annotated pre, so that rule never matches and the button covers the +// markers. Apply Quarto's declarations from the scaffold instead. The gutter +// is 20px + 0.5em at the code size; 0.5em at the button's size clears it while +// code is no larger than body text, plus the button's usual 5px margin. +// Upstream: https://github.com/quarto-dev/quarto-cli/issues/13444 +html .code-copy-outer-scaffold:has(pre.code-annotated) > .code-copy-button { + margin-right: calc(20px + 0.5em + 5px); + margin-top: 0; + padding-top: 3px; + padding-bottom: 0; +} + +// Quarto spaces the first and last child of a callout body for a paragraph. +// With code there, that child is the copy scaffold, or the cell around it, +// and the code block's own margin adds to that spacing. +// In every style, drop the block's margin on that side so code starts and ends +// where text would. In the default style, the copy button, placed from a bare +// scaffold's padded top, also sits above the frame: move it down by that +// padding. In a cell, Quarto pads both the cell and the scaffold inside it at +// the bottom; keep one. An untitled callout nests its content one level +// deeper, in .callout-body-container. A code window at the edge keeps the +// spacing the Code Window extension gives it. +// Upstream: https://github.com/quarto-dev/quarto-cli/issues/13446 +html .callout.callout-style-default :is(.callout-body, .callout-body-container) + > .code-copy-outer-scaffold:first-child + > .code-copy-button { + top: 0.5rem; +} + +html .callout :is(.callout-body, .callout-body-container) { + > .code-copy-outer-scaffold:first-child, + > .cell:first-child > .code-copy-outer-scaffold:first-child { + > div.sourceCode { + margin-top: 0; + } + } + + > .code-copy-outer-scaffold:last-child, + > .cell:last-child > .code-copy-outer-scaffold:last-child { + > div.sourceCode { + margin-bottom: 0; + } + } + + > .cell:last-child > .code-copy-outer-scaffold:last-child { + padding-bottom: 0; + } +} + // =========================================================================== // TABSETS // =========================================================================== @@ -727,6 +885,16 @@ body .navbar .gitlink-widget-dropdown { text-decoration: underline; } +// Quarto gives footer text $text-muted, which is held to AA on the page rather +// than on the bar; use the chrome muted ink instead. +.nav-footer-contents { + color: var(--atelier-navbar-muted); +} + +.sidebar .sidebar-footer { + color: var(--atelier-sidebar-muted); +} + // =========================================================================== // ANNOUNCEMENT BANNER // =========================================================================== @@ -775,6 +943,7 @@ body .navbar .gitlink-widget-dropdown { // icon is no more readable there than a black one is on a modal. The icon is // masked from the text colour around it instead, so it follows the surface it // sits on, whichever bundle is active. +// Upstream: https://github.com/quarto-dev/quarto-cli/issues/15060 .btn-close, .btn-close:hover { color: inherit;