Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed
- Code blocks render their source at 16px with a 20px line, up from the 13px
/ 17px they inherited, against the 18px prose. The 13px was JupyterLab's
default, not a theme decision: an element rule in `thebe-core.css` applied it
to every `<pre>`, outranking the `text-sm` on the block's wrapper. The value
now goes on the same JupyterLab variables, scoped to the content, so the
source block, the output Jupyter renders itself under live compute and the
spacing thebe derives from them all follow it, and code does not change size
when a reader switches live compute on. Stored cell outputs take the same
16px / 20px, from 14px / 20px. The frame is unchanged: executable cells keep
their left rule, plain fences and `{code-block}` directives keep upstream's
fill and shadow ([#177](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/177)) ([#184](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/184)).
- Admonition, exercise, proof and dropdown bodies render at 16px, a step below
the 18px prose, as the Sphinx lecture sites' `.admonition { font-size: .9rem
}` does; until now nothing sized a callout and every family inherited the
full 18px. Root-relative, so a note nested in an exercise stays at 16px
rather than stepping down twice, and flat at every width where the Sphinx
rule falls to 14.4px below 992px. Callout titles take the container's size;
their weight is unchanged ([#178](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/178)) ([#185](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/185)).
- Content links keep their resting underline, now one solid line on plain
links and cross-references alike. Upstream dotted the cross-reference
underline to mark a hover-preview card, and which entries in a list were
dotted followed which target pages carried a thumbnail image. The underline
stays at rest where the Sphinx sites show it on hover only, because colour
cannot mark a link on these grounds: 2.02:1 against the body text in light
mode and 1.49:1 in dark, against the 3:1 WCAG 1.4.1 asks of a colour-only
cue. `:visited` is still unstyled; its dark value waits on the dark palette
decision ([#176](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/176)).

### Fixed
- The site footer's text and links clear WCAG AA in both modes once its 70%
opacity is composited. Light-mode text at `#444` landed at 4.17:1 on white
and links at `#0072bc` at 3.01:1; dark-mode text at `#a6a6a6` landed at
3.92:1 on the `#222` panel. They are now `#3b3b3b` (4.54:1), `#00436f`
(4.55:1) and `#b5b5b5` (4.55:1); the fade, the badge, the 5px rule and the
dark link colour are unchanged. The default footer's "A theme by QuantEcon"
link puts link text on every page that declares no footer part, so the link
figure applies to every site ([#201](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/201)).

## [3.0.0] - 2026-09-12

### Added
Expand Down
15 changes: 11 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,20 @@ problem. The existing divergences are the model:
- the dark footer link is blue-300, because the inherited `#0072bc` composites
to 2.2:1 at the footer's opacity;
- content is a flat 18px off a `rem` root, rather than the Sphinx builds'
px root, which is a WCAG 1.4.4 problem.
px root, which is a WCAG 1.4.4 problem;
- content links keep a resting underline, where the lecture builds underline
on hover only, because the link colour is 2.02:1 (light) and 1.49:1 (dark)
against the body text and colour alone cannot mark a link (WCAG 1.4.1);
- the footer's text, and its light-mode link, are darker than the lecture
builds' (`#3b3b3b` and `#00436f`, `#b5b5b5` in dark mode), because the
inherited values composite to 4.17:1, 3.01:1 and 3.92:1 at the footer's
70% opacity.

Changing an inherited value for any *other* reason is a two-theme decision, not
a fix this repo makes alone — it would reintroduce a difference against the live
sites. Open an issue instead (see #172 for the code palette, #201 for the
footer). When you compute a contrast ratio, composite any `opacity` on the
element first: the declared colour is not what the reader sees.
sites. Open an issue instead (see #172 for the code palette). When you compute
a contrast ratio, composite any `opacity` on the element first: the declared
colour is not what the reader sees.

## CI: visual tests and PR previews

Expand Down
2 changes: 1 addition & 1 deletion app/components/SiteFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function SiteFooter({ content, className }: { content?: GenericParent; cl
return (
<div
className={classNames(
'qe-site-footer col-screen text-qetext-light text-opacity-80 dark:text-qetext-dark-muted subgrid-gap',
'qe-site-footer col-screen subgrid-gap',
grid
)}
>
Expand Down
3 changes: 3 additions & 0 deletions docs/code-highlighting.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Code highlighting

The size and frame of code blocks are described under
[typography](typography.md#code-blocks); this page covers the token colours.

Code blocks use QuantEcon's own token palette, the Sphinx lecture sites'
default `qetheme_code_style`, in light and dark mode. MyST tokenises with
highlight.js rather than Pygments, so the palette is mapped scope by scope onto
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Every theme option lives under `site.options` and is listed in
| [launch](launch.md) | notebook launch buttons (Colab) and the notebook repo conventions |
| [notebooks](notebooks.md) | notebook output rendering, live compute, collapsible stderr |
| [git-metadata](git-metadata.md) | "Last changed" and the inline changelog |
| [typography](typography.md) | content size, code blocks, callouts and links |
| [code-highlighting](code-highlighting.md) | the code token palette |
| [text-color-schemes](text-color-schemes.md) | emphasis, strong and definition colours |
| [dark-mode](dark-mode.md) | the dark theme and its tokens |
Expand Down
5 changes: 5 additions & 0 deletions docs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ licensed under a Creative Commons Attribution-ShareAlike 4.0 International.",
and "A theme by QuantEcon". This is the Sphinx sites' footer block, which every
site carried without configuring anything.

The footer renders at 14.4px, faded to 70% as the Sphinx footer is. Its text
and link colours are darker than the Sphinx values so that what the reader
sees, the colour composited at that opacity, clears WCAG AA on the white page
and on the `#222` dark panel.

`site.parts.footer` names a Markdown file that **replaces** the default
outright — licence notice and theme credit included. Set it only to state
different terms:
Expand Down
40 changes: 40 additions & 0 deletions docs/typography.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Content typography

Content renders at a flat 18px, set on the article rather than on the root:
the root stays at the browser's 16px, so a reader's own font-size preference
still applies and the toolbar's font-size control scales everything with it.
There is no breakpoint step, so nothing changes size under the reader as the
window narrows. The values below are measurements of the Sphinx lecture sites,
rounded to the 16px root where the Sphinx build sizes off its 18px one.

| Surface | Size | Notes |
| --- | --- | --- |
| prose | 18px | flat at every width |
| inline literals | 16px | `#af5f5f`; see [text colour schemes](text-color-schemes.md) |
| code blocks | 16px / 20px line | source and stored outputs alike; unchanged when live compute starts |
| callout bodies and titles | 16px | admonitions, exercises, proofs, dropdowns; nested callouts do not step down again |
| footer | 14.4px | faded to 70%; see [layout](layout.md#footer) |

## Code blocks

Source sits at 16px with a 20px line against the 18px prose, the Sphinx sites'
0.9 ratio. The frame is upstream's: an executable cell carries a grey frame and
a blue left rule, a plain fence or `{code-block}` directive a light fill and a
shadow, so a reader can tell a run cell from a shown snippet. The token palette
is described under [code highlighting](code-highlighting.md).

## Callouts

Admonition, exercise, proof and dropdown bodies step down to 16px, so an aside
reads as subordinate to the prose it interrupts; their titles take the same
size and keep upstream's medium weight. The step is off the root, so a note
inside an exercise renders at the same 16px as the exercise.

## Links

Content links are QuantEcon blue (`#0072bc`, `#004979` on hover) with a solid
underline at rest, on plain links and cross-references alike. The Sphinx sites
underline on hover only; this theme keeps the underline because the link
colour is too close to the body text (2.02:1 in light mode, 1.49:1 in dark) for
colour alone to mark a link. Visited links are not coloured. In dark mode links
are white, weight 600; the dark palette is an open design question (#237).
147 changes: 127 additions & 20 deletions styles/quantecon.css
Original file line number Diff line number Diff line change
Expand Up @@ -384,30 +384,51 @@
myst-to-react colours content anchors through two classes: `.link` (plain
links) and `.hover-link` (cross-references, citations, footnotes). Upstream
defines both in `@myst-theme/styles` as blue-700/blue-400 with blue
decoration colours. These rules re-colour the classes to QuantEcon blue
(the `--qe-link-*` tokens) -- colour only. The underline itself
(always-on, dotted for `.hover-link`) is left as upstream ships it.
decoration colours, underlined at rest, and dots the `.hover-link`
underline. These rules re-colour the classes to QuantEcon blue (the
`--qe-link-*` tokens) and draw one solid resting underline on both.

The underline stays at rest, where the lecture builds show it on hover
only, because colour alone cannot mark a link here. A link colour has to
clear 4.5:1 against the ground to read as text and 3:1 against the body
text to be findable without another cue (WCAG 1.4.1, technique G183); on
white against #44403c, and on #222 against #d6d3d1, no colour does both,
and the ones that ship are 2.02:1 and 1.49:1 against the body text. The
underline is the non-colour cue, so it cannot be hover-only.

Solid on `.hover-link` as well. Upstream's dots mark a link that opens a
hover-preview card, and myst-to-react decides that per target page by
whether it has a description or a thumbnail. The lecture pages carry no
descriptions, so which entries in one list were dotted followed which
lectures happened to have a thumbnail image -- a difference the reader
cannot account for, and one that flips when a thumbnail is added.

Specificity is matched to the upstream rules this overrides, with source
order breaking each tie (this file is @import-ed after `@myst-theme/styles`,
see styles/app.css):

- `.link` here (0,1,0) ties upstream `.link` -- wins on order.
- `.hover-link` here (0,1,0) ties upstream's dotted `.hover-link` -- wins
on order.
- `.link:hover` (0,2,0) ties upstream's compiled `hover:` utility.
- `.dark :is(...)` (0,2,0) ties upstream's `dark:` -> `.link:is(.dark *)`.
- `.dark :is(...):hover` (0,3,0) ties `.link:hover:is(.dark *)`.

`text-decoration-color: currentColor` replaces upstream's separate blue
decoration utilities in one declaration per rule -- the underline always
draws in the link's own colour.
draws in the link's own colour. Thickness and offset stay upstream's
(0.1em and 0.15em).

Two deliberate divergences from the lecture builds, so they are not "fixed"
later: they underline on hover only, where these underline always, and they
colour `:visited` links #004979, which these leave alone. */
One deliberate divergence from the lecture builds remains, so it is not
"fixed" later: they colour `:visited` links #004979, which these leave
alone. A visited colour is a light and a dark value together, and the dark
half waits on the dark palette decision (#237). */
@layer components {
.link,
.hover-link {
color: var(--qe-link-color, #0072bc);
text-decoration-line: underline;
text-decoration-style: solid;
text-decoration-color: currentColor;
}

Expand All @@ -427,6 +448,78 @@
}
}

/* ---------------------------------------------------------------------------
CODE BLOCKS

Source at 1rem (16px) with a 1.25 line-height (20px), against the 18px
prose. The lecture builds render code at .9rem of their 18px root, 16.2px,
with `line-height: 125%`; 1rem is the call the inline literals above make
for the same Sphinx quantity, so the whole code column sits on one size.
Upstream leaves the `<pre>` at 13px / 17px, and that is JupyterLab's
default rather than a myst-theme decision: `thebe-core.css` (loaded after
the Tailwind bundle, see app/root.tsx) declares `--jp-code-font-size: 13px`
on `:root` and applies it through an element rule, `tt, code, kbd, samp,
pre`, at (0,0,1) -- on the `<pre>` itself, so it outranks anything the
`text-sm` on the `.myst-code` wrapper could pass down by inheritance.

The value goes on the variables, not on a `pre` rule, and on `.article`
rather than `:root`: a custom property set on a descendant beats the one
thebe sets on `:root` whatever the stylesheet order, and every consumer of
the variable inside the content follows it -- the source block, the output
Jupyter renders itself once a reader starts live compute (the same
variable sizes it), and the two `calc()`s thebe derives from it -- so code
does not change size when the live-compute toggle is switched on.

The stored outputs are the one surface the variable does not reach:
`@myst-theme/jupyter` renders them in a `div` carrying `font-mono text-sm`
(14px / 20px), so they take their own rule, at (0,2,0) to beat the
utilities. The families are left as they come -- thebe's stack on the
source, Tailwind's on the outputs. */
@layer components {
.article {
--jp-code-font-size: 1rem; /* 16px */
--jp-code-line-height: 1.25; /* 20px */
}

.article .myst-jp-safe-output-text {
font-size: 1rem; /* 16px */
line-height: 1.25; /* 20px */
}
}

/* ---------------------------------------------------------------------------
CALLOUTS

Admonition, exercise, proof and dropdown bodies at 1rem (16px), a step
below the 18px prose they interrupt. The lecture builds size every
`.admonition` at .9rem of their 18px root, 16.2px, so an aside reads as
subordinate to the copy around it by size; without this rule nothing here
sizes a callout at all, and every family inherits `.article`'s 18px.

Root-relative on purpose, like the inline literals: `em` off `.article`
would compound, and a `{note}` inside an `{exercise}` -- common in the
lectures -- would land at 0.9 x 0.9 x 18px. Flat, per the UNITS note: the
Sphinx rule falls to 14.4px below 992px, on exercise and solution text,
and this does not.

The headers take the container's size, as the Sphinx titles do (its
stylesheet sets no size on `.admonition-title`). Exercise and proof
headers already inherit -- myst-to-react gives them `text-md`, a class
Tailwind does not define -- but the admonition and dropdown headers carry
`text-lg`, a root-relative 18px, so they need `1em` here, at (0,2,0) to
beat the utility. Weight (upstream's `font-medium`) is left as it is.
Margin and sidebar asides (`.myst-aside`) are not callouts and stay at
upstream's `text-sm`. */
@layer components {
.article :is(.myst-admonition, .myst-exercise, .myst-proof, .myst-dropdown) {
font-size: 1rem; /* 16px */
}

.article :is(.myst-admonition-header, .myst-dropdown-header) {
font-size: 1em; /* the container's 16px, not `text-lg`'s 18px */
}
}

/* ---------------------------------------------------------------------------
SITE FOOTER

Expand All @@ -436,10 +529,12 @@
page chrome.

Selectors are `.article .qe-site-footer`, (0,2,0), on purpose. At (0,1,0) a
bare `.qe-site-footer` loses to the utilities already on the element
(`text-qetext-light`, `text-opacity-80`, and `.link` on the badge anchor),
because utilities are emitted after components. The extra `.article` wins on
specificity instead of relying on source order.
bare `.qe-site-footer` would lose to utilities of the same weight emitted
later -- the grid utilities on the element, and `.link` on the badge anchor
of a site's own footer.md -- because utilities are emitted after
components. The extra `.article` wins on specificity instead of relying on
source order. The element carries no colour utility: both modes' colours
are set here, so the composited contrast noted on each is the whole story.
--------------------------------------------------------------------------- */
@layer components {
.article .qe-site-footer {
Expand All @@ -449,10 +544,11 @@
root. */
font-size: 0.9rem; /* 14.4px */
line-height: 1.5; /* 21.6px */
/* This overrides the element's `text-qetext-light text-opacity-80`
utilities outright, so that 80% no longer applies; the `opacity: 0.7`
below is what fades the footer. */
color: #444;
/* #3b3b3b, not the lecture builds' #444: what the reader sees is the
colour composited at the 70% opacity below, and #444 lands at #7c7c7c
on white, 4.17:1 -- under the 4.5:1 floor for 14.4px text. #3b3b3b
composites to #767676, 4.54:1. */
color: #3b3b3b;
/* The 36px gap above the rule is padding, not margin: `styles/app.css` sets
`margin-top: 0 !important` on every `.simple-center-grid > *`, and this
footer is a direct child of that grid. */
Expand All @@ -462,19 +558,30 @@
extend the document. */
padding-bottom: 2.5em; /* 36px */
/* 0.7 is the lecture footer's own opacity, on the whole element, so the
rule, badge and text fade together: the colours above are declared at
full strength and render softer here (the #0072bc rule samples as about
rgb(75 155 207) on white). Declaring the faded colours directly would
not reproduce it -- the badge image fades too. */
rule, badge and text fade together: the colours here are declared at
full strength and render softer (the #0072bc rule samples as about
rgb(77 156 208) on white). Declaring the faded colours directly would
not reproduce it -- the badge fades too. */
opacity: 0.7;
}

/* Dark mode. #a6a6a6, the toolbar's muted text, composites to #7e7e7e on
the #222 content panel at the footer's opacity, 3.92:1; #b5b5b5 lands at
#898989, 4.55:1. */
.dark .article .qe-site-footer {
color: #b5b5b5;
}

.article .qe-site-footer p {
margin: 1em 0; /* 14.4px */
}

.article .qe-site-footer a {
color: #0072bc; /* rgb(0 114 188), the same blue as the rule above */
/* #0072bc's hue and saturation, darkened until the composited link clears
AA: at the footer's opacity #0072bc itself lands at #4d9cd0 on white,
3.01:1, and #00436f at #4d7b9a, 4.55:1. The 5px rule above keeps
#0072bc; a non-text rule has no 4.5:1 floor. */
color: #00436f;
text-decoration: none;
}

Expand Down
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/history-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/lists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/rtl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/desktop-chrome-linux/sidebar-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/features.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/history-open.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/intro.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/lists.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/notebook.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/visual/__snapshots__/mobile-chrome-linux/rtl.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions tests/visual/fixture/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,16 @@ A tip admonition with a nested list:
- two
:::

::::{exercise}
:label: fixture-exercise
An exercise, with a note inside it. Callouts are sized off the root, so the
nested one keeps the outer one's size instead of stepping down again.

:::{note}
A note nested in the exercise.
:::
::::

### A note in an admonition subsection

A level-three heading, so the "On this page" panel has a nested entry (#182).
Expand Down
Loading
Loading