From dca9cc06c14de3896aed6b2fcabbad49363a5d12 Mon Sep 17 00:00:00 2001 From: "mcanouil-dev[bot]" <211049963+mcanouil-dev[bot]@users.noreply.github.com> Date: Sat, 1 Aug 2026 13:10:18 +0000 Subject: [PATCH] chore(deps): update 2 Quarto extensions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - mcanouil/atelier: 0.6.0 → 0.9.0 - mcanouil/gitlink: 1.8.0 → 1.9.0 --- .../mcanouil/atelier/_extension.yml | 86 +-- .../mcanouil/atelier/html/chrome.scss | 134 +++++ .../mcanouil/atelier/html/theme.scss | 494 +++++++++++++++--- .../mcanouil/atelier/social-metadata.lua | 31 +- .../mcanouil/gitlink/_extension.yml | 7 +- .../mcanouil/gitlink/_modules/widget.lua | 2 +- docs/_extensions/mcanouil/gitlink/_schema.yml | 4 +- docs/_extensions/mcanouil/gitlink/widget.css | 103 +++- docs/_extensions/mcanouil/gitlink/widget.js | 106 ++-- 9 files changed, 783 insertions(+), 184 deletions(-) create mode 100644 docs/_extensions/mcanouil/atelier/html/chrome.scss diff --git a/docs/_extensions/mcanouil/atelier/_extension.yml b/docs/_extensions/mcanouil/atelier/_extension.yml index ea87ad8..f0cc22a 100644 --- a/docs/_extensions/mcanouil/atelier/_extension.yml +++ b/docs/_extensions/mcanouil/atelier/_extension.yml @@ -1,25 +1,66 @@ title: Atelier author: Mickaël Canouil -version: 0.6.0 -quarto-required: '>=1.9.36' -source: mcanouil/quarto-atelier@0.6.0 -source-type: github +version: 0.9.0 +quarto-required: ">=1.9.36" contributes: + project: + project: + type: website + output-dir: _site + website: + repo-actions: [edit, issue] + repo-link-target: _blank + repo-link-rel: noopener noreferrer + # `locale` matches `lang: en-GB` below; override the two together. + # Naming `twitter-card` at all is what switches Quarto's Twitter + # provider on, after which it inherits title, description, image, and + # image alt from each page. + open-graph: + locale: en_GB + twitter-card: + card-style: summary_large_image + page-navigation: true + back-to-top-navigation: true + llms-txt: true + search: + location: navbar + type: overlay + # `background` and `foreground` are left to the theme on both bars: the + # chrome palette in `html/chrome.scss` sets `$navbar-bg`, `$navbar-fg`, + # `$footer-bg`, and `$footer-fg` from a layer Quarto evaluates first, so + # naming them here would be a configuration with no effect. + navbar: + search: true + page-footer: + border: true + left: | + Powered by [Quarto](https://quarto.org){target="_blank" rel="noopener noreferrer"}. + center: | + © []{#current-year} [Mickaël CANOUIL](https://mickael.canouil.fr){target="_blank" rel="noopener noreferrer"}. + format: atelier-html formats: common: lang: en-GB - date-format: dddd[, the] Do [of] MMMM, YYYY + date-format: "dddd[, the] Do [of] MMMM, YYYY" code-copy: true code-overflow: wrap code-link: false html: respect-user-color-scheme: true + # Quarto builds the canonical link from `website.site-url`, giving a + # directory index the URL of its directory. Set `canonical-url: false` + # on a page served from more than one URL, such as `404.qmd`. canonical-url: true + # `html/chrome.scss` comes before `brand` on purpose: Quarto evaluates + # user layer defaults in reverse list order, so a file placed first is + # evaluated last, after the brand palette it derives the chrome from. theme: light: + - html/chrome.scss - brand - html/theme.scss dark: + - html/chrome.scss - brand - html/theme.scss syntax-highlighting: @@ -43,37 +84,4 @@ contributes: - file: html/scripts/ordinal-dates.html - file: html/scripts/a11y-fixes.html - file: html/scripts/navbar-tooltips.html - project: - project: - type: website - output-dir: _site - website: - repo-actions: - - edit - - issue - repo-link-target: _blank - repo-link-rel: noopener noreferrer - open-graph: - locale: en_GB - twitter-card: - card-style: summary_large_image - page-navigation: true - back-to-top-navigation: true - llms-txt: true - search: - location: navbar - type: overlay - navbar: - background: dark - foreground: light - search: true - page-footer: - border: true - background: dark - foreground: light - left: | - Powered by [Quarto](https://quarto.org){target="_blank" rel="noopener noreferrer"}. - center: > - © []{#current-year} [Mickaël CANOUIL](https://mickael.canouil.fr){target="_blank" rel="noopener - noreferrer"}. - format: atelier-html +source: mcanouil/quarto-atelier@0.9.0 diff --git a/docs/_extensions/mcanouil/atelier/html/chrome.scss b/docs/_extensions/mcanouil/atelier/html/chrome.scss new file mode 100644 index 0000000..fe2aa86 --- /dev/null +++ b/docs/_extensions/mcanouil/atelier/html/chrome.scss @@ -0,0 +1,134 @@ +/*-- scss:defaults --*/ + +// =========================================================================== +// CHROME PALETTE +// =========================================================================== +// This file is listed before `brand` in the format's theme lists, which is +// what lets it read the brand palette. Quarto emits user layer defaults in +// reverse list order, so a file placed before brand has its defaults evaluated +// after brand's, where $body-bg, $body-color, and $primary are all resolved, +// and still ahead of every Quarto and Bootstrap `!default`. That is the only +// point in the cascade where the chrome can be derived from the page and still +// be handed to Quarto's own $navbar-*, $sidebar-*, and $footer-* variables. +// +// The chrome is the navbar, the page footer, and the sidebar: the bars that +// frame the page rather than carry it. + +// How the chrome follows the colour scheme. +// +// auto Derived from the brand palette, so each bundle gets its own chrome +// and the bars follow the colour-scheme toggle. +// light Pinned to the light palette below in both schemes. +// dark Pinned to the dark palette below in both schemes. +$atelier-chrome: auto !default; + +@if not index((auto, light, dark), $atelier-chrome) { + @error "$atelier-chrome must be auto, light, or dark, but was #{$atelier-chrome}."; +} + +// The pinned palettes, each a background, a foreground, and an accent that +// every other chrome colour is derived from. Only the palette matching +// $atelier-chrome is used, so pinning light leaves the dark values inert. +$atelier-chrome-dark-bg: #1b242e !default; +$atelier-chrome-dark-fg: #e8edf2 !default; +$atelier-chrome-dark-accent: #7fa8c4 !default; + +$atelier-chrome-light-bg: #eef1f5 !default; +$atelier-chrome-light-fg: #1b242e !default; +$atelier-chrome-light-accent: #1f5f80 !default; + +// A project can drop `brand` from its theme list, in which case the page +// variables are only defined in the Bootstrap layer that follows this one; +// fall back to the Bootstrap defaults so `auto` still resolves. +$atelier-chrome-page-bg: if(variable-exists(body-bg), $body-bg, #fff); +$atelier-chrome-page-fg: if(variable-exists(body-color), $body-color, #212529); +$atelier-chrome-page-accent: if(variable-exists(primary), $primary, #0d6efd); + +// In `auto` the bar is a light tint of the page rather than the page colour +// itself, so it still reads as a bar without a hard colour break, and the +// mix resolves per bundle: a paler paper in the light one, a lifted slate in +// the dark one. +$atelier-chrome-bg: if( + $atelier-chrome == dark, + $atelier-chrome-dark-bg, + if( + $atelier-chrome == light, + $atelier-chrome-light-bg, + mix($atelier-chrome-page-bg, $atelier-chrome-page-fg, 94%) + ) +) !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. +// 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( + if( + $atelier-chrome == dark, + $atelier-chrome-dark-fg, + if( + $atelier-chrome == light, + $atelier-chrome-light-fg, + $atelier-chrome-page-fg + ) + ), + $atelier-chrome-bg, + "AA" +) !default; + +$atelier-chrome-accent: theme-contrast( + if( + $atelier-chrome == dark, + $atelier-chrome-dark-accent, + if( + $atelier-chrome == light, + $atelier-chrome-light-accent, + $atelier-chrome-page-accent + ) + ), + $atelier-chrome-bg, + "AA" +) !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; + +// A hairline between two surfaces, which is what the bar edges and the section +// dividers want. +$atelier-chrome-border: rgba($atelier-chrome-fg, 0.2) !default; + +// The boundary of a boxed control, which WCAG 1.4.11 asks 3:1 of. The weight +// has to hold on a light bar as well as a dark one: at 0.4 it is 3.3:1 on the +// dark palette but only 2.4:1 on the light one. +$atelier-chrome-control-border: rgba($atelier-chrome-fg, 0.55) !default; + +// =========================================================================== +// QUARTO CHROME VARIABLES +// =========================================================================== +// Handing the palette to Quarto's own variables is what makes the parts this +// theme cannot reach from CSS come out right: the collapsed navbar's toggler +// icon and the reader-mode toggle are SVGs with the foreground baked into the +// markup, and the sidebar link, hover, and disabled colours are contrasted +// against $sidebar-bg. These are plain assignments rather than defaults +// because Quarto's own layer, which they have to beat, uses `!default`. +$navbar-bg: $atelier-chrome-bg; +$navbar-fg: $atelier-chrome-fg; +$navbar-hl: $atelier-chrome-accent; + +// Quarto fades a hovered navbar link to `rgba($navbar-hl, 0.8)`, which mixes +// the accent back into the bar and costs it about a third of its contrast; +// hover at full strength instead, which is also what the sidebar does. +$navbar-hover-color: $atelier-chrome-accent; + +$sidebar-bg: $atelier-chrome-bg; +$sidebar-fg: $atelier-chrome-fg; + +$footer-bg: $atelier-chrome-bg; +$footer-fg: $atelier-chrome-fg; +$footer-border-color: $atelier-chrome-border; diff --git a/docs/_extensions/mcanouil/atelier/html/theme.scss b/docs/_extensions/mcanouil/atelier/html/theme.scss index aaa8f6a..98e9c7e 100644 --- a/docs/_extensions/mcanouil/atelier/html/theme.scss +++ b/docs/_extensions/mcanouil/atelier/html/theme.scss @@ -14,22 +14,62 @@ $enable-smooth-scroll: true !default; @return mix($body-bg, $body-color, $weight); } +/*-- scss:mixins --*/ + +// Dropdown menus render on both bars, each from its own tokens. +@mixin atelier-dropdown($surface, $border, $fg, $accent, $accent-soft) { + .dropdown-menu { + background: $surface; + border: 1px solid $border; + border-radius: $border-radius-sm; + } + + .dropdown-item { + color: $fg; + } + + .dropdown-item:hover, + .dropdown-item:focus { + background: $accent-soft; + color: $accent; + } +} + /*-- scss:rules --*/ // =========================================================================== -// NAVBAR TOKENS (dark-pinned in both schemes) +// CHROME TOKENS // =========================================================================== -// The navbar and footer are `background: dark` in both schemes, so their -// surfaces must not follow the page scheme. These fixed tokens default to a -// neutral slate; override them in a project stylesheet to match a site palette. +// The chrome palette is resolved in `html/chrome.scss`, which follows the +// colour scheme or pins to one of two fixed palettes depending on +// `$atelier-chrome`. These tokens republish it as custom properties, which is +// what the rules below read and what a project overrides for a single part of +// a bar; re-point the Sass variables instead when the change should also reach +// the colours Quarto bakes into its own markup. :root { - --atelier-navbar-bg: #1b242e; - --atelier-navbar-surface: #223041; - --atelier-navbar-fg: #e8edf2; - --atelier-navbar-muted: #9db0c0; - --atelier-navbar-accent: #7fa8c4; - --atelier-navbar-accent-soft: rgba(127, 168, 196, 0.16); - --atelier-navbar-border: rgba(232, 237, 242, 0.14); + --atelier-navbar-bg: #{$atelier-chrome-bg}; + --atelier-navbar-surface: #{$atelier-chrome-surface}; + --atelier-navbar-fg: #{$atelier-chrome-fg}; + --atelier-navbar-muted: #{$atelier-chrome-muted}; + --atelier-navbar-accent: #{$atelier-chrome-accent}; + --atelier-navbar-accent-soft: #{$atelier-chrome-accent-soft}; + --atelier-navbar-border: #{$atelier-chrome-border}; + + // A boxed control (a search field, a widget pill) needs a boundary that can + // be seen, where the hairline above only has to separate two surfaces. + --atelier-navbar-control-border: #{$atelier-chrome-control-border}; + + // A docked sidebar is the third bar, so its tokens default to the navbar + // ones: overriding the navbar palette carries the sidebar with it, and a + // project that wants the two to differ re-points these alone. + --atelier-sidebar-bg: var(--atelier-navbar-bg); + --atelier-sidebar-surface: var(--atelier-navbar-surface); + --atelier-sidebar-fg: var(--atelier-navbar-fg); + --atelier-sidebar-muted: var(--atelier-navbar-muted); + --atelier-sidebar-accent: var(--atelier-navbar-accent); + --atelier-sidebar-accent-soft: var(--atelier-navbar-accent-soft); + --atelier-sidebar-border: var(--atelier-navbar-border); + --atelier-sidebar-control-border: var(--atelier-navbar-control-border); } // =========================================================================== @@ -181,20 +221,231 @@ html .panel-tabset > .tab-content { } // =========================================================================== -// SIDEBAR + TOC +// TOC // =========================================================================== -// Quarto marks the current page and section with .active but leaves them in -// the sidebar body colour; surface them with the palette accent, and give the -// margin TOC's active border the same accent instead of the baked grey. -#quarto-sidebar .sidebar-item-container .active, -#quarto-sidebar .sidebar-item-container .sidebar-link:hover, -.sidebar-navigation .sidebar-item .active, +// Quarto marks the current section with .active but leaves it in the body +// colour, and gives its border the baked grey; the margin TOC sits on the page +// surface, so both take the palette accent. #TOC .active { color: $primary; + border-left-color: $primary; } -#TOC .active { - border-left-color: $primary; +// =========================================================================== +// SIDEBAR +// =========================================================================== +// `html/chrome.scss` hands the palette to `$sidebar-bg` and `$sidebar-fg`, so +// the column and everything Quarto contrasts against it already match the +// other two bars. What is left here is the treatment Quarto has no variable +// for, plus the handful of colours it bakes past those two. +// +// Everything below is scoped to `body.docked`, the class Quarto adds when the +// sidebar style is `docked`, which is also what raises these rules over the +// Quarto rules they replace; only the sidebar edge, which Quarto draws with +// `!important`, needs one of its own. A floating sidebar has no surface of its +// own, and takes the chrome through Quarto's own rules alone. +body.docked { + .sidebar.sidebar-navigation { + background-color: var(--atelier-sidebar-bg); + } + + // `sidebar.border` defaults to true for a docked sidebar, and Quarto draws + // that edge from the baked $table-border-color with `!important`. + .sidebar.sidebar-navigation:not(.rollup) { + border-right: 1px solid var(--atelier-sidebar-border) !important; + } + + #quarto-sidebar, + .sidebar-item-container { + color: var(--atelier-sidebar-fg); + } + + .sidebar-item-container:hover, + .sidebar-item-container:focus-within, + .sidebar-item-container .active, + .sidebar-item-container .sidebar-link > code { + color: var(--atelier-sidebar-accent); + } + + .sidebar-item-container.disabled { + color: var(--atelier-sidebar-muted); + } + + // The current page reads as a pill rather than as a colour change alone, + // which matches the navbar dropdown treatment. + .sidebar-item-container .sidebar-link.active { + background: var(--atelier-sidebar-accent-soft); + border-radius: $border-radius-sm; + padding: 0.15rem 0.4rem; + margin: -0.15rem -0.4rem; + font-weight: 600; + } + + // A sidebar entry with text and no href or contents is a group label; Quarto + // renders it as a bare span, in the same colour as a link. + .sidebar-item > .menu-text { + color: var(--atelier-sidebar-muted); + } + + // Bootstrap draws an
from currentColor at 25% opacity, which on the dark + // column is all but invisible; use the border token at full strength. + .sidebar-navigation .sidebar-divider { + border-top-color: var(--atelier-sidebar-border); + opacity: 1; + } + + .sidebar-item-toggle { + color: var(--atelier-sidebar-muted); + } + + .sidebar-item-toggle:hover { + color: var(--atelier-sidebar-accent); + } + + // The header (logo and title) only renders when the project has no navbar. + .sidebar-title { + color: var(--atelier-sidebar-fg); + } + + .sidebar-title > a:hover, + .sidebar-title > a:focus-visible { + color: var(--atelier-sidebar-accent); + } + + // Quarto renders the sidebar tools inside `.sidebar-title` whenever the + // sidebar has a title and the project has no navbar, as an inline-flex + // pulled up 6px, which lands the colour-scheme toggle on the title's line. + // A block-level flex row breaks it onto its own line, which the document + // order then places above the search field. Nothing here is forced: the + // gitlink extension stacks the same container into a column from an id + // selector when its widget is one of the tools. + .sidebar-title .sidebar-tools-main { + display: flex; + justify-content: flex-start; + margin-top: 0.5rem; + } + + // `sidebar.align` reaches the header as a text class, which a flex row does + // not follow. Bootstrap has no `text-left`, so the rule above covers the + // left-aligned case on its own. + .sidebar-header.text-center .sidebar-title .sidebar-tools-main { + justify-content: center; + } + + .sidebar-header.text-end .sidebar-title .sidebar-tools-main { + justify-content: flex-end; + } + + // Quarto dims the sidebar tools to 0.7, which on the dark column drops the + // colour-scheme toggle and the repo links below the contrast floor. + .sidebar-navigation .quarto-navigation-tool { + color: var(--atelier-sidebar-fg); + opacity: 1; + } + + .sidebar-navigation .quarto-navigation-tool:hover, + .sidebar-navigation .quarto-navigation-tool:focus-visible { + color: var(--atelier-sidebar-accent); + } + + // A sidebar tool with a `menu` opens a Bootstrap dropdown on the dark column. + .sidebar-navigation { + @include atelier-dropdown( + var(--atelier-sidebar-surface), + var(--atelier-sidebar-border), + var(--atelier-sidebar-fg), + var(--atelier-sidebar-accent), + var(--atelier-sidebar-accent-soft) + ); + } + + // Inline sidebar search (`website.sidebar.search: true` with a textbox + // search). Quarto colours the input from $body-color through a four-class + // rule, which matches the sidebar surface in one of the two schemes and hides + // the text and the caret; the magnifier and the spinner come from + // $input-color at half opacity through another. Both chains have to be named + // here to be outranked. + .sidebar-search .aa-Form { + background-color: var(--atelier-sidebar-surface); + border-color: var(--atelier-sidebar-control-border); + color: var(--atelier-sidebar-fg); + } + + .sidebar-search .aa-Form .aa-InputWrapper .aa-Input { + color: var(--atelier-sidebar-fg); + caret-color: var(--atelier-sidebar-fg); + } + + .sidebar-search .aa-Form .aa-InputWrapper .aa-Input::placeholder { + color: var(--atelier-sidebar-muted); + opacity: 1; + } + + .sidebar-search .aa-Form .aa-InputWrapperPrefix svg { + color: var(--atelier-sidebar-muted); + opacity: 1; + } + + // Overlay search reaches the sidebar as a button rather than as a field. + // Quarto inks that button only inside `.sidebar-tools-*`, from a $sidebar-fg + // contrasted against the surface it painted rather than against ours, and + // through a selector carrying the search id; match the id to outrank it. + .sidebar-navigation #quarto-search .aa-DetachedSearchButton svg { + color: var(--atelier-sidebar-fg); + } + + .sidebar-navigation #quarto-search .aa-DetachedSearchButton:hover svg { + color: var(--atelier-sidebar-accent); + } + + // The mobile bar carrying the sidebar toggle, the page title, and the search + // button. Quarto paints it with $sidebar-bg, the surface this file replaces. + .quarto-secondary-nav { + background: var(--atelier-sidebar-bg); + border-bottom: 1px solid var(--atelier-sidebar-border); + } + + .quarto-secondary-nav .quarto-btn-toggle, + .quarto-secondary-nav .quarto-search-button, + .quarto-secondary-nav-title, + .quarto-secondary-nav .quarto-page-breadcrumbs, + .quarto-secondary-nav .quarto-page-breadcrumbs a { + color: var(--atelier-sidebar-fg); + } + + .quarto-secondary-nav .quarto-btn-toggle:hover, + .quarto-secondary-nav .quarto-search-button:hover, + .quarto-secondary-nav .quarto-page-breadcrumbs a:hover { + color: var(--atelier-sidebar-accent); + } + + .quarto-secondary-nav .breadcrumb-item::before { + color: var(--atelier-sidebar-muted); + } + + // The page focus ring is $primary, which on the dark column is far too dim; + // keep its geometry and swap the colour for the sidebar accent. + :is(#quarto-sidebar, .quarto-secondary-nav) :is(a, button):focus-visible { + outline-color: var(--atelier-sidebar-accent); + } + + // The page scrollbar rules are unscoped, so without these the dark column + // would carry a page-coloured bar. + #quarto-sidebar { + scrollbar-color: var(--atelier-sidebar-muted) var(--atelier-sidebar-surface); + + &::-webkit-scrollbar-track { + background: var(--atelier-sidebar-surface); + } + + &::-webkit-scrollbar-thumb { + background: var(--atelier-sidebar-muted); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--atelier-sidebar-accent); + } + } } // Quarto's floating back-to-top pill has no edge on tinted surfaces; give it @@ -204,67 +455,150 @@ html .panel-tabset > .tab-content { } // =========================================================================== -// NAVBAR (dark-pinned) +// NAVBAR // =========================================================================== -// Uses the fixed --atelier-navbar-* tokens so the bar, dropdowns, and widgets -// stay dark in both schemes; !important beats Quarto's `.navbar a` colour. +// The bar itself, its links, and its tools are painted by Quarto from the +// `$navbar-*` variables `html/chrome.scss` sets. Only the bottom edge, which +// Quarto does not draw at all, and the dropdown menus are left. .navbar { - background: var(--atelier-navbar-bg) !important; border-bottom: 1px solid var(--atelier-navbar-border); -} - -.navbar-brand, -.navbar .nav-link { - color: var(--atelier-navbar-fg) !important; -} - -.navbar .nav-link:hover, -.navbar .nav-link.active { - color: var(--atelier-navbar-accent) !important; -} - -.navbar .dropdown-menu { - background: var(--atelier-navbar-surface); - border: 1px solid var(--atelier-navbar-border); - border-radius: $border-radius-sm; -} - -.navbar .dropdown-item { - color: var(--atelier-navbar-fg); -} -.navbar .dropdown-item:hover, -.navbar .dropdown-item:focus, -.navbar .dropdown-item:focus-visible { - background: var(--atelier-navbar-accent-soft); - color: var(--atelier-navbar-accent); + @include atelier-dropdown( + var(--atelier-navbar-surface), + var(--atelier-navbar-border), + var(--atelier-navbar-fg), + var(--atelier-navbar-accent), + var(--atelier-navbar-accent-soft) + ); } -// The search button and colour-scheme toggle pill styling ships with the -// gitlink widget (widget.css and widget-navbar-tools.css via -// `widget.style-navbar-tools`); bridge the fixed navbar tokens onto the +// The pill visuals on the search button and the colour-scheme toggle, the +// border and its hover fill, ship with the gitlink widget +// (widget-navbar-tools.css via `widget.style-navbar-tools`); their geometry +// comes from the control tokens below. Bridge the navbar tokens onto the // widget's own custom properties so the pill, trigger, and dropdown match the -// dark-pinned navbar. Inert when the gitlink extension is not installed. +// bar they sit on. Inert when the gitlink extension is not installed. .navbar { - --gitlink-widget-border: var(--atelier-navbar-border); + --gitlink-widget-border: var(--atelier-navbar-control-border); --gitlink-widget-accent: var(--atelier-navbar-accent); --gitlink-widget-accent-soft: var(--atelier-navbar-accent-soft); --gitlink-widget-menu-bg: var(--atelier-navbar-surface); --gitlink-widget-menu-fg: var(--atelier-navbar-fg); } +// The same widget can render in the sidebar tools, where it takes the sidebar +// tokens instead. +body.docked .sidebar-navigation { + --gitlink-widget-border: var(--atelier-sidebar-control-border); + --gitlink-widget-accent: var(--atelier-sidebar-accent); + --gitlink-widget-accent-soft: var(--atelier-sidebar-accent-soft); + --gitlink-widget-menu-bg: var(--atelier-sidebar-surface); + --gitlink-widget-menu-fg: var(--atelier-sidebar-fg); +} + // Structure ships with the extension's widget.css; match the radius of the // navbar dropdown menus above. body .navbar .gitlink-widget-dropdown { border-radius: $border-radius-sm; } +// =========================================================================== +// NAVBAR CONTROL SIZING +// =========================================================================== +// The search button, the colour-scheme toggle, and a `website.navbar.tools` +// icon are three Quarto components with three box models: a 40px detached +// button carrying a 26px glyph, an inline `` with no height of +// its own, and a `bi` icon at the link font size. Pin all three to one control +// box so they read as one set on the bar. +// +// The defaults are the numbers the gitlink widget applies (widget.css), so the +// bar looks the same with and without that extension and neither stylesheet has +// to win the cascade for the geometry to hold. +:root { + --atelier-navbar-control-size: 2rem; + --atelier-navbar-control-icon-size: 1rem; + --atelier-navbar-control-gap: 0.4rem; + + // The magnifier is the one glyph on the bar that does not fill its own box: + // Quarto's `aa-SubmitIcon` draws in 20 units of a 24-unit viewBox, where the + // colour-scheme glyph and the `bi` tool icons paint edge to edge. Scaling its + // box by 24/20 is what puts the ink at the icon size rather than the box. + --atelier-navbar-search-icon-size: calc(var(--atelier-navbar-control-icon-size) * 1.2); +} + +.navbar { + // Quarto hands the search the whole free space with `margin-left: auto` below + // the expand breakpoint and 0.25rem above it; the control gap replaces both, + // so the search keeps the same distance from the tools at every width. Scoped + // to `.type-overlay`, the search this project type configures: a `textbox` + // search in the navbar is a field rather than a control box, and Quarto's own + // margins are what place it. + #quarto-search.type-overlay { + display: inline-flex; + align-items: center; + margin-left: var(--atelier-navbar-control-gap); + } + + .quarto-navbar-tools { + display: inline-flex; + align-items: center; + gap: var(--atelier-navbar-control-gap); + } + + // Quarto pins the overlay autocomplete to a 40px box, wider than every other + // navbar control; let the button size it instead. + #quarto-search.type-overlay .aa-Autocomplete { + width: auto; + } + + // `min-width` rather than `width`, so a tool with a `menu`, which keeps its + // dropdown caret in the navbar, can grow past the square rather than clip it. + #quarto-search .aa-DetachedSearchButton, + .quarto-navbar-tools .quarto-navigation-tool { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: var(--atelier-navbar-control-size); + height: var(--atelier-navbar-control-size); + // !important is required: Bootstrap's .px-1 utility on the toggle is itself + // !important. The inline padding only shows on a control wide enough to + // exceed the square, which is the caret case above. + padding: 0 0.25rem !important; + } + + // Quarto leaves the glyph in a block wrapper whose height is a line box, so it + // is taller than the icon and splits the leftover unevenly around it, dropping + // the glyph off the button's centre; a flex wrapper hugs the icon instead. + #quarto-search .aa-DetachedSearchButtonIcon { + display: flex; + align-items: center; + } + + // Quarto draws the overlay glyph at 26px, against the 1rem of the toggle glyph + // and the tool icons; `.aa-Autocomplete` is in the selector to match the + // specificity of that rule, which this then wins on order. `display: block` + // drops the inline baseline gap under the icon. + #quarto-search.type-overlay .aa-Autocomplete .aa-DetachedSearchButtonIcon svg { + display: block; + width: var(--atelier-navbar-search-icon-size); + height: var(--atelier-navbar-search-icon-size); + } + + // Quarto gives the search button no hover colour at all, where it takes the + // colour-scheme toggle to the accent; match it. + #quarto-search .aa-DetachedSearchButton:hover .aa-DetachedSearchButtonIcon svg, + #quarto-search .aa-DetachedSearchButton:focus-visible .aa-DetachedSearchButtonIcon svg { + color: var(--atelier-navbar-accent); + } +} + // =========================================================================== // COLLAPSED NAVBAR (mobile) // =========================================================================== -// Below the expand breakpoint, keep the collapsed menu a wrapping row and stop +// Below the expand breakpoint, keep the collapsed menu a wrapping row, stop // Bootstrap's navbar-nav-scroll (max-height + overflow-y) from clipping an -// absolutely-positioned dropdown; re-anchor navbar dropdowns to absolute. +// absolutely-positioned dropdown by re-anchoring navbar dropdowns to absolute, +// and regroup the bar as toggler, brand, then tools. @media (max-width: 991.98px) { .navbar .navbar-collapse .navbar-nav { flex-direction: row; @@ -285,22 +619,27 @@ body .navbar .gitlink-widget-dropdown { top: 100%; left: 0; } + + // Quarto orders the collapsed bar as toggler, brand, tools, search and pins + // the brand's inline margins to 0 !important; with the search margin cut back + // to the control gap above, the free space has nowhere to go and spreads the + // four controls across the bar. Give the brand the auto margins instead: the + // free space lands either side of it, leaving the toggler left, the brand + // centred, and the toggle and the search grouped right. + // !important is required to beat Quarto's own !important margins. + .navbar.navbar-expand-lg .navbar-container > .navbar-brand-container { + margin-inline: auto !important; + } } // =========================================================================== -// FOOTER (dark-pinned) +// FOOTER // =========================================================================== -// `page-footer.background: dark` paints the footer with Bootstrap's $dark, -// which need not match the navbar tokens; repaint it so both dark-pinned bars -// share one surface, and recolour links, which otherwise keep the page link -// colour on the dark surface. -.nav-footer { - background: var(--atelier-navbar-bg) !important; - border-top: 1px solid var(--atelier-navbar-border); -} - +// Quarto paints the surface, the text, the links, and the top edge from the +// chrome palette, the edge because `html/chrome.scss` sets +// `$footer-border-color` alongside the rest. Only the link treatment is left: +// Quarto underlines nothing and gives no hover colour of its own. .nav-footer a { - color: var(--atelier-navbar-fg); text-decoration: none; } @@ -386,8 +725,8 @@ body .navbar .gitlink-widget-dropdown { // TOOLTIPS // =========================================================================== // Bootstrap tooltips plus the tippy theme used by navbar-tooltips.html; both -// pinned to the navbar tokens so they match the dark-pinned navbar in either -// scheme. +// take the navbar tokens, since every control they are bound to sits on the +// navbar. .tooltip { --bs-tooltip-bg: var(--atelier-navbar-bg); --bs-tooltip-color: var(--atelier-navbar-fg); @@ -420,8 +759,9 @@ body .navbar .gitlink-widget-dropdown { // THEME TOGGLE (sun in light scheme, moon in dark scheme) // =========================================================================== // Quarto renders an empty ; the glyph is a mask-image so it -// follows currentColor and picks up the navbar-tool colour. The pill container -// itself ships with the gitlink widget; this only draws the glyph. +// follows currentColor and picks up the navbar-tool colour. The box around it +// comes from the control tokens above, and its pill border, when there is one, +// from the gitlink widget; this only draws the glyph. $atelier-icon-sun: url('data:image/svg+xml,'); $atelier-icon-moon: url('data:image/svg+xml,'); @@ -463,14 +803,6 @@ $atelier-icon-moon: url('data:image/svg+xml, local function theme_colour_tags(config) local tags = {} - local colours = config and config['theme-color'] - if not colours then - return tags - end for _, scheme in ipairs(THEME_COLOUR_SCHEMES) do - local colour = colours[scheme] and str.stringify(colours[scheme]) + local colour = theme_colour(config, scheme) if not str.is_empty(colour) then table.insert( tags, diff --git a/docs/_extensions/mcanouil/gitlink/_extension.yml b/docs/_extensions/mcanouil/gitlink/_extension.yml index d5c2069..eef6217 100644 --- a/docs/_extensions/mcanouil/gitlink/_extension.yml +++ b/docs/_extensions/mcanouil/gitlink/_extension.yml @@ -1,9 +1,8 @@ title: gitlink author: Mickaël Canouil -version: 1.8.0 -quarto-required: '>=1.8.21' -source: mcanouil/quarto-gitlink@1.8.0 -source-type: registry +version: 1.9.0 +quarto-required: ">=1.8.21" contributes: filters: - gitlink.lua +source: mcanouil/quarto-gitlink@1.9.0 diff --git a/docs/_extensions/mcanouil/gitlink/_modules/widget.lua b/docs/_extensions/mcanouil/gitlink/_modules/widget.lua index bcc44c9..1212673 100644 --- a/docs/_extensions/mcanouil/gitlink/_modules/widget.lua +++ b/docs/_extensions/mcanouil/gitlink/_modules/widget.lua @@ -23,7 +23,7 @@ local log = load_sibling('logging.lua') local html_mod = load_sibling('html.lua') --- @type string Version used for the injected HTML dependency -local WIDGET_DEPENDENCY_VERSION = '1.7.0' +local WIDGET_DEPENDENCY_VERSION = '1.9.0' --- @type table|nil Octicon SVG bodies by name (loaded once per render) local octicons_cache = nil diff --git a/docs/_extensions/mcanouil/gitlink/_schema.yml b/docs/_extensions/mcanouil/gitlink/_schema.yml index 71a2fe4..cbbd4c5 100644 --- a/docs/_extensions/mcanouil/gitlink/_schema.yml +++ b/docs/_extensions/mcanouil/gitlink/_schema.yml @@ -81,12 +81,12 @@ options: type: string widget: type: object - description: "Repository navbar widget showing live star/fork counts and a dropdown of repository links, replacing a navbar item with href '#gitlink-widget'. HTML websites only." + description: "Repository widget showing live star/fork counts and a dropdown of repository links, replacing every navbar item, sidebar tool, or sidebar contents item with href '#gitlink-widget'. HTML websites only." properties: enabled: type: boolean default: false - description: "Whether to inject the navbar widget. Independent of the top-level 'enabled' option, so a site can run widget-only." + description: "Whether to inject the widget. Independent of the top-level 'enabled' option, so a site can run widget-only." links: type: object description: "Toggles for the platform-derived default menu entries." diff --git a/docs/_extensions/mcanouil/gitlink/widget.css b/docs/_extensions/mcanouil/gitlink/widget.css index 473549e..83ee75f 100644 --- a/docs/_extensions/mcanouil/gitlink/widget.css +++ b/docs/_extensions/mcanouil/gitlink/widget.css @@ -1,9 +1,9 @@ -/* Gitlink repository navbar widget. +/* Gitlink repository navigation widget (navbar or sidebar). Structural styling only; theming happens through the `--gitlink-widget-*` custom properties, which fall back to Bootstrap tokens so unthemed Quarto sites work out of the box. */ -#gitlink-widget { +.gitlink-widget { position: relative; display: inline-flex; align-items: center; @@ -123,9 +123,10 @@ transform: rotate(180deg); } -/* Menu rules are scoped under `.navbar` so they outrank Quarto's `.navbar a` - colour, which would otherwise force the navbar foreground onto the items. */ -.navbar .gitlink-widget-dropdown { +/* Menu rules are scoped under `.gitlink-widget` so they outrank Quarto's + `.navbar a` and `.sidebar-navigation a` colours, which would otherwise + force the surrounding navigation foreground onto the items. */ +.gitlink-widget .gitlink-widget-dropdown { position: absolute; top: calc(100% + 6px); right: 0; @@ -142,13 +143,13 @@ z-index: 1000; } -.navbar .gitlink-widget-open .gitlink-widget-dropdown { +.gitlink-widget.gitlink-widget-open .gitlink-widget-dropdown { opacity: 1; visibility: visible; transform: translateY(0); } -.navbar .gitlink-widget-item { +.gitlink-widget .gitlink-widget-item { display: flex; align-items: center; gap: 10px; @@ -161,32 +162,106 @@ transition: background 150ms ease, color 150ms ease; } -.navbar .gitlink-widget-item svg, -.navbar .gitlink-widget-item .bi { +.gitlink-widget .gitlink-widget-item svg, +.gitlink-widget .gitlink-widget-item .bi { flex-shrink: 0; } /* Labels may carry inline markup (e.g. an iconify icon from a shortcode in the entry's `text`); keep any such icon aligned with the text. */ -.navbar .gitlink-widget-item .gitlink-widget-item-label { +.gitlink-widget .gitlink-widget-item .gitlink-widget-item-label { display: inline-flex; align-items: center; gap: 6px; } -.navbar .gitlink-widget-item:hover, -.navbar .gitlink-widget-item:focus-visible { +.gitlink-widget .gitlink-widget-item:hover, +.gitlink-widget .gitlink-widget-item:focus-visible { background: var(--gitlink-widget-accent-soft, color-mix(in srgb, var(--gitlink-widget-accent, var(--bs-link-color, #0d6efd)) 15%, transparent)); color: var(--gitlink-widget-accent, var(--bs-link-color, #0d6efd)); text-decoration: none; } -.navbar .gitlink-widget-divider { +.gitlink-widget .gitlink-widget-divider { height: 1px; margin: 0.35rem 0; background: var(--gitlink-widget-border, var(--bs-border-color-translucent, rgba(128, 128, 128, 0.3))); } +/* Sidebar navigation item (`sidebar.contents`). Quarto's sidebar is an + `overflow-auto` scroll container, which would clip an absolutely positioned + menu, so the menu expands inline below the trigger, like a sidebar section. + `display` is not animatable, hence no transition. */ +.gitlink-widget-sidebar { + display: flex; + flex-direction: column; + align-items: stretch; + width: 100%; +} + +.gitlink-widget-sidebar .gitlink-widget-trigger { + width: 100%; + justify-content: space-between; +} + +.gitlink-widget-sidebar .gitlink-widget-dropdown { + position: static; + display: none; + min-width: 0; + width: 100%; + margin-top: 0.35rem; + box-shadow: none; + transform: none; + transition: none; +} + +.gitlink-widget-sidebar.gitlink-widget-open .gitlink-widget-dropdown { + display: block; +} + +/* A tools row holding the widget stacks vertically: the widget trigger is a + counted pill, not an icon, so it takes its own line above the remaining + tools (colour-scheme toggle, reader toggle, overlay search). The marker + class is set by widget.js on whichever tools container the placeholder was + in; the id keeps this ahead of Quarto's own `.sidebar-tools-main` rather + than relying on stylesheet order. */ +#quarto-sidebar .gitlink-widget-tools { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + width: 100%; +} + +/* Quarto pulls the tools row up under the sidebar title (`margin-top: -6px`), + which crowds the widget pill; give it room instead. */ +.sidebar-title .sidebar-tools-main.gitlink-widget-tools { + margin-top: 0.5rem; +} + +/* The widget sits below the icon tools (colour-scheme and reader toggles) and + above the search control, whether search is the sidebar field below the row + or an overlay button inside it. */ +.gitlink-widget-tools .gitlink-widget { + order: 1; +} + +.gitlink-widget-tools #quarto-search { + order: 2; +} + +/* A tool sits in a strip of icon-sized controls, where an inline menu would + be laid out as another flex item beside them, so it keeps the overlay menu. + The menu is anchored to the trigger's left edge and takes its content width + (no 14rem floor) so it stays inside the sidebar, which clips overflow + rather than scrolling sideways. */ +.gitlink-widget-tools .gitlink-widget-dropdown { + left: 0; + right: auto; + min-width: 0; + width: max-content; +} + /* The collapsed navbar-nav is a Bootstrap `.navbar-nav-scroll` region (max-height + overflow-y: auto) that clips the absolute dropdown, showing a scrollbar instead of the menu. Neutralise it so the menu overlays on top, @@ -197,7 +272,7 @@ overflow: visible; } - .navbar .gitlink-widget-dropdown { + .navbar .gitlink-widget .gitlink-widget-dropdown { left: 0; right: auto; } diff --git a/docs/_extensions/mcanouil/gitlink/widget.js b/docs/_extensions/mcanouil/gitlink/widget.js index c20dde5..cb9dd90 100644 --- a/docs/_extensions/mcanouil/gitlink/widget.js +++ b/docs/_extensions/mcanouil/gitlink/widget.js @@ -1,13 +1,14 @@ /** - * Gitlink - Repository Navbar Widget - * Replaces the `#gitlink-widget` navbar placeholder with a button showing live - * star and fork counts and a dropdown menu of repository links. Configuration - * is injected by the gitlink Lua filter as a JSON script element; counts come - * from the platform REST API, cached in localStorage for four hours so the - * rate limit is not hit on every page view. A stale cache or `?` covers a - * failed request. Octicon path data for the icons the menu uses arrives in - * the configuration (`icons`); other icon names render as Bootstrap Icons, - * which Quarto bundles with every HTML page. + * Gitlink - Repository Navigation Widget + * Replaces every `#gitlink-widget` placeholder (navbar item, sidebar tool, or + * sidebar contents item) with a button showing live star and fork counts and a + * dropdown menu of repository links. Configuration is injected by the gitlink + * Lua filter as a JSON script element; counts come from the platform REST API, + * cached in localStorage for four hours so the rate limit is not hit on every + * page view, and shared by every widget on the page. A stale cache or `?` + * covers a failed request. Octicon path data for the icons the menu uses + * arrives in the configuration (`icons`); other icon names render as Bootstrap + * Icons, which Quarto bundles with every HTML page. * * @license MIT * @copyright 2026 Mickaël Canouil @@ -164,12 +165,12 @@ // Disclosure pattern: a native button toggling a plain list of links, per // the ARIA Authoring Practices for navigation link collections. No menu // roles, so Tab moves through the links naturally. - const buildTrigger = (config) => { + const buildTrigger = (config, menuId) => { const trigger = document.createElement("button"); trigger.type = "button"; trigger.className = "gitlink-widget-trigger"; trigger.setAttribute("aria-expanded", "false"); - trigger.setAttribute("aria-controls", "gitlink-widget-menu"); + trigger.setAttribute("aria-controls", menuId); trigger.setAttribute("aria-label", config.menuLabel); const platformIcon = buildIcon(config.icon, 20, config.icons); @@ -192,9 +193,9 @@ return trigger; }; - const buildDropdown = (config) => { + const buildDropdown = (config, menuId) => { const dropdown = document.createElement("div"); - dropdown.id = "gitlink-widget-menu"; + dropdown.id = menuId; dropdown.className = "gitlink-widget-dropdown"; dropdown.setAttribute("aria-hidden", "true"); @@ -227,15 +228,17 @@ return dropdown; }; - const buildWidget = (config) => { - const widget = document.createElement("li"); - widget.className = "nav-item"; + // The first widget on a page keeps the historical `gitlink-widget` id; any + // further placeholder gets a suffixed one so ids stay unique. + const buildWidget = (config, index) => { + const rootId = index === 0 ? "gitlink-widget" : "gitlink-widget-" + index; + const menuId = rootId + "-menu"; const root = document.createElement("div"); - root.id = "gitlink-widget"; - root.appendChild(buildTrigger(config)); - root.appendChild(buildDropdown(config)); - widget.appendChild(root); - return widget; + root.id = rootId; + root.className = "gitlink-widget"; + root.appendChild(buildTrigger(config, menuId)); + root.appendChild(buildDropdown(config, menuId)); + return root; }; const showStats = (widget, stats, config) => { @@ -259,17 +262,19 @@ } }; - const loadStats = (widget, config) => { - if (!config.api) return; + // The in-flight request, shared by every widget on the page (they all read + // the same configuration) so several placeholders make one API call instead + // of racing each other before the cache is written. + let pendingStats = null; + const fetchStats = (config) => { // Read + parse the cache once; branch on freshness so the stale-fallback // path does not re-read and re-parse the same entry. const cached = readCache(config.cacheKey); if (cached && Date.now() - cached.timestamp <= CACHE_DURATION) { - showStats(widget, cached, config); - return; + return Promise.resolve(cached); } - const stale = cached; + if (pendingStats) return pendingStats; const options = { headers: config.api.headers || {} }; // Bound the request so a slow API falls back to the stale cache instead @@ -278,7 +283,7 @@ options.signal = AbortSignal.timeout(8000); } - fetch(config.api.endpoint, options) + const request = fetch(config.api.endpoint, options) .then((response) => (response.ok ? response.json() : Promise.reject(response.status))) .then((data) => { const stats = { @@ -287,9 +292,16 @@ timestamp: Date.now(), }; writeCache(config.cacheKey, stats); - showStats(widget, stats, config); + return stats; }) - .catch(() => showStats(widget, stale || { stars: "?", forks: "?" }, config)); + .catch(() => cached || { stars: "?", forks: "?" }); + pendingStats = request; + return request; + }; + + const loadStats = (widget, config) => { + if (!config.api) return; + fetchStats(config).then((stats) => showStats(widget, stats, config)); }; const setupDropdown = (widget) => { @@ -324,6 +336,31 @@ }); }; + // Replace one placeholder anchor with a widget. Only the anchor goes, so + // whatever Quarto wrapped it in (`li.nav-item`, `li.sidebar-item > + // div.sidebar-item-container`, `div.sidebar-tools-main`) is preserved, and + // the widget inherits the spacing and colours of its neighbours. + const mountWidget = (anchor, index, config) => { + const root = buildWidget(config, index); + + // The tools row is a horizontal strip of icon-sized controls; flag it so + // the CSS can stack the widget above the search and colour-scheme + // controls instead of squeezing the counts in beside them. + const tools = anchor.closest(".sidebar-tools-main, .sidebar-tools-collapse"); + if (tools) { + tools.classList.add("gitlink-widget-tools"); + } else if (anchor.closest("#quarto-sidebar")) { + // A sidebar navigation item is as wide as the sidebar and sits in its + // scroll container, so its menu expands inline rather than overlaying. + root.classList.add("gitlink-widget-sidebar"); + } + + anchor.replaceWith(root); + + loadStats(root, config); + setupDropdown(root); + }; + document.addEventListener("DOMContentLoaded", function () { const config = readConfig(); if (!config) return; @@ -331,14 +368,7 @@ // Quarto rewrites the placeholder href to a relative path // (`./#gitlink-widget` at the root, `../#gitlink-widget` deeper), so // match the fragment suffix rather than an exact href. - const anchor = document.querySelector('a[href$="#gitlink-widget"]'); - if (!anchor) return; - const slot = anchor.closest("li"); - if (!slot) return; - - const widget = buildWidget(config); - slot.replaceWith(widget); - loadStats(widget, config); - setupDropdown(widget); + const anchors = document.querySelectorAll('a[href$="#gitlink-widget"]'); + anchors.forEach((anchor, index) => mountWidget(anchor, index, config)); }); })();