Skip to content
Open
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
26 changes: 17 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Links that open a hover preview carry a small stacked-squares glyph after the label. `.hover-link`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added the ([#176](…)) ([#239](…)). link group here and on the Changed entry, per CONTRIBUTING. Consider also dropping the last two sentences (the word joiner and the data URI): they describe how the stylesheet is built rather than what a reader of the release notes sees, and both live in the CSS comment already. The Changed entry's "narrowing the divergence recorded under 3.0.0" is history rather than the change; "where upstream underlines always" already says it.

is what myst-to-react hangs on links to other lectures and to Wikipedia (which
render a card with a thumbnail and summary) and on cross-references (which render
an excerpt of the target); with the resting underline gone, nothing otherwise
distinguished them from a plain link. The icon takes the size, gap and opacity
upstream gives external links through `.link-icon`. Footnote markers and citations
take no glyph — they lose their resting underline with everything else, and render
as bare markers. It is an `::after` whose content is a word joiner, so it cannot be
stranded on a line of its own the way an inline-block box is, and the glyph is a
data URI, so it survives `myst build --html` ([#176](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/176)) ([#239](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/239)).

### 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
Expand All @@ -39,15 +51,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
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)).
- Content links no longer underline at rest. `.link` and `.hover-link` render
undecorated and draw a solid underline in `currentColor` on hover — and now on
keyboard focus too — where upstream underlines always, dotted for `.hover-link`.
This is the Sphinx lecture builds' behaviour, narrowing the divergence recorded
under 3.0.0, which matched their colours only. `:visited` is still left alone ([#176](https://github.com/QuantEcon/quantecon-theme.mystmd/issues/176)) ([#239](https://github.com/QuantEcon/quantecon-theme.mystmd/pull/239)).

### Fixed
- The site footer's text and links clear WCAG AA in both modes once its 70%
Expand Down
3 changes: 0 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,6 @@ problem. The existing divergences are the model:
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;
- 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
Expand Down
26 changes: 20 additions & 6 deletions docs/typography.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,23 @@ inside an exercise renders at the same 16px as the exercise.

## Links

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rewritten on the rebase to describe what the sheet now does, with the contrast residue and the token-recolouring caveat stated in prose as the description asked. Please read it as yours: if the decision on the residue changes, this section and the CONTRIBUTING list are the two places that must move with it.


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).
Content links are QuantEcon blue (`#0072bc`, `#004979` on hover) with no
underline at rest, on plain links and cross-references alike; a solid underline
in the link's own colour appears on hover and on keyboard focus. This is the
Sphinx sites' behaviour. Visited links are not coloured. In dark mode links are
white, weight 600; the dark palette is an open design question (#237).

Links that open a hover preview -- cross-references, links to other lectures
that carry a description or thumbnail, Wikipedia and GitHub links -- carry a
small stacked-squares glyph after the label, at the size, gap and opacity
upstream gives the external-link arrow. Footnote markers and citations open a
preview too but take no glyph. The glyph is a pre-coloured image rather than
an icon in `currentColor`, so a site that re-points `--qe-link-color` recolours
the text and not the glyph.

The glyph is the non-colour cue on the links that have one. A plain internal
link with no preview has none at rest: `#0072bc` is 2.02:1 against the body
text, short of the 3:1 WCAG 1.4.1 asks before colour alone may mark a link,
and no link colour clears both that and 4.5:1 on white. That residue is the
cost of matching the lecture builds; closing it means a non-colour resting
cue, not a different blue.
151 changes: 124 additions & 27 deletions styles/quantecon.css
Original file line number Diff line number Diff line change
Expand Up @@ -382,38 +382,34 @@
CONTENT LINKS

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, 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.
links) and `.hover-link` (cross-references, citations, footnotes, and any
link that opens a hover preview). Upstream defines both in
`@myst-theme/styles` as blue-700/blue-400, underlined at rest, with
`.hover-link`'s underline dotted. These rules re-colour them to QuantEcon
blue (the `--qe-link-*` tokens) and take the resting underline away, so
content links read as the lecture builds' do: no underline until hover or
keyboard focus, then a solid one in the link's own colour.

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.
- `.link` here (0,1,0) ties upstream `.link` -- wins on order. It also
ties `.hover-link, .hover-text { text-decoration-style: dotted }`,
which is why `solid` is restated below rather than assumed.
- `.link:hover` (0,2,0) ties upstream's compiled `hover:` utility. It has
to carry `text-decoration-line` itself: this file's own hover
underline (`.article :where(a):hover`, also (0,2,0)) sits in
`@layer base`, and the `none` below is in `@layer components`, which
wins whatever the specificity -- so without it no state would
underline. `:focus-visible` has no base-layer counterpart at all.
- `.dark :is(...)` (0,2,0) ties upstream's `dark:` -> `.link:is(.dark *)`.
- `.dark :is(...):hover` (0,3,0) ties `.link:hover:is(.dark *)`.

The dark rules stay colour-only: nothing under `.dark` declares
`text-decoration-line`, so the none/underline pair set here carries through
both themes unchanged.

`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. Thickness and offset stay upstream's
Expand All @@ -427,15 +423,25 @@
.link,
.hover-link {
color: var(--qe-link-color, #0072bc);
text-decoration-line: underline;
text-decoration-style: solid;
text-decoration-color: currentColor;
text-decoration-line: none;
/* Restated rather than inherited: upstream's dotted rule for `.hover-link`
is (0,1,0) too, and would otherwise pick the style of the underline the
hover rule below draws. */
text-decoration-style: solid;
}

/* Keyboard focus carries the same cue as the pointer. Without it a keyboard
reader has colour alone to go on, and #0072bc against the #44403c prose is
2.02:1 -- short of the 3:1 WCAG 1.4.1 asks of a colour-only distinction
(the underline on hover/focus is the other half of technique G183). */
.link:hover,
.hover-link:hover {
.hover-link:hover,
.link:focus-visible,
.hover-link:focus-visible {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This never matches on a footnote marker. The focusable element there is the inner HashLink anchor, not the <sup class="hover-link">, and that anchor carries a no-underline utility with only hover:underline (myst-to-react/dist/footnotes.js and hashLink.js). So a keyboard reader tabbing to [1] gets no underline and no colour change, which is exactly the colour-only state the comment above says this rule exists to avoid. Pointer hover still works because sup:hover propagates. Adding .hover-link:focus-within to this selector list (or .hover-link:has(:focus-visible), which the sheet already requires support for) covers it; the glyph rule keys on a.hover-link and is unaffected.

color: var(--qe-link-hover-color, #004979);
text-decoration-color: currentColor;
text-decoration-line: underline;
}

/* Dark mode needs its own rules despite the tokens flipping under `.dark`:
Expand All @@ -448,6 +454,97 @@
}
}

/* ---------------------------------------------------------------------------
HOVER-PREVIEW AFFORDANCE

`.hover-link` is what myst-to-react hangs on every link that opens a hover
popover: links to other lectures and to Wikipedia (a card with a thumbnail
and summary), cross-references (an excerpt of the target), citations and
footnotes. With the resting underline gone these are indistinguishable from
a plain `.link`, so they carry a small stacked-squares glyph after the label
-- the affordance upstream already gives external links through
`.link-icon`, at its size, gap and opacity.

Footnote markers are a `<sup class="hover-link">` rather than an anchor, so
keying on `a.hover-link` passes over them; citations are opted out below.
The `:has()` guard mirrors upstream's own, keeping the icon off links that
wrap an image instead of text.

Two constraints decide how it is built, and they pull against each other.

`content` is U+2060 WORD JOINER and the glyph is painted into
`padding-inline-start`, rather than the obvious inline-block box. A box is
an atomic inline, which UAX #14 treats as a break opportunity, and the icon
then strands itself on a line of its own whenever the label's last word
lands near the column edge -- 31 of 61 label lengths in a 300px column, in
both Chromium and WebKit. A word joiner forbids the break on both sides, so
the icon always travels with the last word. Upstream's `.link-icon` has the
fault this avoids: its `whitespace-nowrap` wrapper does not prevent it.

The glyph is then a `background-image` in three pre-coloured copies, not
one `currentColor` fill behind a `mask-image`. The mask is the tidier
construction -- one image, colour tracked for free -- but WebKit does not
render a mask on a non-replaced *inline* box: it paints the whole padding
box and knocks the glyph out of it, a solid rectangle where the icon should
be. (Chromium renders it correctly; WebKit renders the same mask correctly
on an `inline-block`, which is the display this cannot use.) Neither
`mask-mode: alpha`, `-webkit-mask-source-type`, `mask-clip` nor a
white-stroked source changes it. A `background-image` renders correctly on
an inline box in both engines, at the cost of baking the colour in -- hence
the three copies below, one per state the link can be in.

A data URI also has no path to rewrite, so the icon survives
`myst build --html`: mystmd's static rewriter handles `.html`, `.js` and
`.json` and never rewrites `url()` inside a stylesheet. */
@layer components {
a.hover-link:not(:has(img, figure, picture, video))::after {
Comment thread
mmcky marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things the description's link-kind table understates, both worth checking on a real lecture build rather than the fixture. .hover-link — lecture is not one kind: InternalLink adds the class only when the target page has a description or thumbnail (links/index.js, skipPreview), so in a list of twelve lecture links the glyph appears on the three whose targets happen to carry a thumbnail and not on the other nine, and adding a thumbnail to a lecture puts a glyph on links in pages nobody edited. That is the same per-target coin flip #238 cited when it replaced the dotted underline. And crossReference.js hangs the class on every {eq}, {numref} and {ref}, so an equation-heavy lecture renders a glyph after every (3), while the [Sar87] citations in the same paragraph, which also open a popover from a bracketed marker, are opted out below. Not asking for a specific change here; asking that the call on both be made with a real page in front of you.

/* heroicons 24/outline `Square2Stack`, stroked in the three link colours:
the `--qe-link-*` tokens' light values and the white both of them take
under `.dark`. Declared here rather than with the other design tokens
so the whole affordance stays in one place. The names say what the
glyph is for, not what it depicts, so swapping it is three URLs. */
--qe-preview-icon-rest: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%230072bc'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6'/%3E%3C/svg%3E");
--qe-preview-icon-active: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23004979'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6'/%3E%3C/svg%3E");
--qe-preview-icon-dark: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke-width='1.5' stroke='%23ffffff'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' d='M16.5 8.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v8.25A2.25 2.25 0 0 0 6 16.5h2.25m8.25-8.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-7.5A2.25 2.25 0 0 1 8.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 0 0-2.25 2.25v6'/%3E%3C/svg%3E");
content: '\2060';
display: inline;
/* Upstream's geometry: a 0.85em glyph behind a 0.125rem gap. `em` for the
glyph so it tracks the content size, `rem` for the gap because that is
what `.link-icon`'s `ml-0.5` resolves to. */
padding-inline-start: calc(0.85em + 0.125rem);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The description lists print and forced-colors as out of scope; the print half is one line and worth taking now. background-image is dropped under the default print-color-adjust: economy but this padding is not, so a printed or PDF-exported lecture gets a 0.85em blank tail after every cross-reference, wiki and GitHub link, with no underline left to mark them either. @media print { a.hover-link::after { content: none; } } removes both the gap and the missing glyph. Forced-colors can stay deferred, but note it in the comment: the baked strokes ignore the system LinkText colour that .link-icon next to it follows.

opacity: 0.8;
background-image: var(--qe-preview-icon-rest);
background-repeat: no-repeat;
/* The glyph sits at the far end of the padding, leaving the gap next to
the label. Mirrored for right-to-left in styles/rtl.css. */
background-position: right center;
background-size: 0.85em 0.85em;
}

/* (0,2,1), which outranks the (0,1,2) base rule above. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The arithmetic here and in the two comments below is off, and one conclusion depends on it. :not(:has(img, figure, picture, video)) contributes (0,0,1), the specificity of its most specific argument, so the base rule is (0,1,3), this one is (0,2,2), and .dark a.hover-link::after is (0,2,2). The ordering between those three holds either way. What does not hold is the citation opt-out: cite a.hover-link::after is also (0,1,3), so it ties the base rule and wins on source order only, where this comment says it outranks. Anyone who later groups the opt-outs above the base rule silently gives every <cite><a class="hover-link"> the glyph back. Correcting the numbers and saying "ties, wins on order" at the cite rule is enough; the replacement test does not cover citations (the fixture has none), which is another reason the comment needs to be right.

a.hover-link:hover::after,
a.hover-link:focus-visible::after {
background-image: var(--qe-preview-icon-active);
}

/* Both dark states are white, because `--qe-link-color` and
`--qe-link-hover-color` both are. The first selector ties the hover rule
above at (0,2,1) and wins on order; the other two settle it outright.
None of them declares `content`, so a link the base rule passed over --
one wrapping an image -- still generates no pseudo-element here. */
.dark a.hover-link::after,
.dark a.hover-link:hover::after,
.dark a.hover-link:focus-visible::after {
background-image: var(--qe-preview-icon-dark);
}

/* Citations render `<cite><a class="hover-link">`, where the label is
already a bracketed marker and the popover is the bibliography entry. */
cite a.hover-link::after {
content: none;
}
}

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

Expand Down
11 changes: 11 additions & 0 deletions styles/rtl.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
* KaTeX lays out LTR.
* 4. The theme's own parenthesised list markers (styles/lists.css) hang in
* the gutter with a physical `left` and translate; hung from the right.
* 5. The hover-preview icon (styles/quantecon.css) places its glyph with a
* physical `background-position`, which has no logical form; mirrored
* here.
*
* Unlayered on purpose: every rule targets a class that Tailwind only sees
* in node_modules or generates at build time, and `@layer` would purge them;
Expand Down Expand Up @@ -108,3 +111,11 @@
right: -0.4em;
transform: translateX(100%);
}

/* 5. The hover-preview icon (styles/quantecon.css) is painted into the
pseudo-element's inline-start padding, which flips on its own; only the
glyph's physical `background-position` needs mirroring, so it keeps to the
far end of the padding and the gap stays against the label. */
[dir='rtl'] a.hover-link::after {
background-position: left center;
}
Loading
Loading