Skip to content

Non-executable code blocks get no frame, and every code block renders 13px against 18px prose #184

Description

@mmcky

From DrDrij's review of the v2.5.0 preview (item 7, "some code blocks lack the blue left border and do not stand out"). Two separate defects sit behind that one sentence, and the missing border is the smaller of them. Measured with getComputedStyle on the reviewed deployment (https://6a9b93b9108dd4beb6bebadc--epic-agnesi-957267.netlify.app/python-by-example/, Chromium 1440×900) against the same lecture on https://python-programming.quantecon.org/python_by_example.html: every code block in this theme sets its <pre> at 13px / 17px line-height, sitting against 18px / 28px prose, where the Sphinx build sets code at 16.2px / 20.25px against 18px / 27px prose. A grey card around 13px text will not answer "does not stand out" on its own; the size is most of the complaint.

Both halves are inherited untouched from upstream — neither is anything this theme chose. grep -rn --exclude-dir=node_modules 'myst-code\|border-left\|border-l-' over styles/ and app/ at v2.5.0 returns nothing at all, and the compiled Tailwind bundle carries no .myst-code rule; the only code-block fill anywhere in it is .dark .hljs{background:#1e1e1e!important;color:#dcdcdc}, from @myst-theme/styles/code-highlight-dark.css:37.

What is on the page today

Every monospace size in both builds, measured:

Element This theme Sphinx build Ratio to its own 18px prose
body prose p 18px / 28px 18px / 27px 1.00 / 1.00
inline literal (code in a paragraph) 16px 16.2px 0.89 / 0.90
code block source (.myst-code pre, div.highlight pre) 13px / 17px 16.2px / 20.25px 0.72 / 0.90
cell output, static (.myst-jp-safe-output-text, div.cell_output pre) 14px / 20px 16.2px / 20.25px 0.78 / 0.90

The theme therefore renders three different monospace sizes — 16px inline, 13px in blocks, 14px in static outputs — in two different font stacks (menlo, consolas, … for the first two, ui-monospace, SFMono-Regular, … for the outputs), where the Sphinx build renders one size in one stack. For context on the units: the root is 16px and deliberately left there (the UNITS note at styles/quantecon.css:55-74, WCAG 1.4.4), and .article carries the 18px content size at styles/quantecon.css:314 (font-size: 1.125rem), so rem and em mean different things in this file and the choice between them carries meaning.

Defect 1 — non-executable blocks get no frame at all

myst-to-react@1.3.0 renders every code node as <CodeBlock … shadow={true} border={node.executable} background={!node.executable}> at dist/code.js:64, and at dist/code.js:55-57 those three booleans select mutually exclusive class sets on the wrapper div.myst-code: shadow gives my-5 text-sm shadow hover:shadow-md dark:shadow-2xl dark:shadow-neutral-900, background gives bg-stone-200/10, and border gives border border-l-4 border-gray-200 border-l-blue-400 dark:border-l-blue-400 dark:border-gray-800. node.executable is set in exactly two places in mystmd — the code-cell directive and ipynb code cells — so a {code-block} directive or a plain fence takes the background branch and gets no border, no accent, and a 10%-opacity fill. On the reviewed page, 30 of 32 wrappers carry the bordered class list and exactly 2 carry no-execute … bg-stone-200/10 with no border class at all.

Property Theme, executable Theme, non-executable Sphinx, executable (div.cell_input) Sphinx, non-executable (div.highlight)
background transparent rgba(231,229,228,0.1) #f7f7f7 #f7f7f7
border top / right / bottom 1px rgb(229,231,235) none 1px #cccccc 1px #e1e1e1
border left 4px rgb(96,165,250) none 3px #0072bc 1px #e1e1e1
border radius 0 0 7.2px, top corners only 4.5px
interior padding 12px (p-3) 12px (p-3) 9px 10px 9px 10px
box shadow Tailwind shadow, shadow-md on hover same none none

The executable / non-executable split itself is right, and matches Sphinx. In quantecon-book-theme the blue accent is body div.cell div.cell_input { border-left-color: colors.$primary } at _quantecon-defaults.scss:3-6, with $primary: #0072bc (_colors.scss:1), and that is the only code-related border-left rule in the whole sheet — every other one is announcements, autodoc, RTL or margin content — so a non-executable block in the Sphinx build gets no blue accent either. What Sphinx does give it is a card, html body:not(.use-pygments-style) .highlight { border: 1px solid #e1e1e1; background: #f7f7f7 } at _syntax.scss:14-17, and that is what this theme omits. The dark twins are in the same theme and should be carried across rather than invented: --qe-dark-code-bg: #1e1e32 and --qe-dark-border: #3a3a5c (_dark-theme.scss:28,36) applied through .highlight { background: var(--qe-dark-code-bg) !important; border-color: var(--qe-dark-border) !important } at _dark-theme.scss:338-341, and div.cell div.cell_input { background-color: var(--qe-dark-code-bg); border-left-color: var(--qe-dark-link) } at _dark-theme.scss:495-498, with --qe-dark-link: #6cb6ff.

Defect 2 — the 13px, and why it is not text-sm

text-sm (14px) is on the .myst-code wrapper div and only ever reaches the <pre> by inheritance. It never wins. thebe-core@0.5.0 ships the vendored JupyterLab rule at node_modules/thebe-core/dist/lib/thebe-core.css:263-271:

tt,
code,
kbd,
samp,
pre {
  font-family: var(--jp-code-font-family);
  font-size: var(--jp-code-font-size);
  line-height: var(--jp-code-line-height);
}

with --jp-code-font-size: 13px and --jp-code-line-height: 1.3077 at thebe-core.css:105-106, giving exactly the 13px / 17.0001px measured. It is (0,0,1) — the same specificity as Tailwind preflight's code,kbd,pre,samp{…font-size:1em} — and app/root.tsx:158-159 loads thebeCoreCss immediately after the Tailwind bundle, so it wins the tie on source order and lands directly on the <pre>, which outranks anything inherited from the wrapper. The measured font-family on the deploy is menlo, consolas, "DejaVu Sans Mono", monospace, i.e. --jp-code-font-family-default rather than preflight's ui-monospace stack — independent confirmation that this rule is the winner. Inline code escapes it because styles/quantecon.css:217 scopes .article :not(pre) > code at (0,1,2); the same JupyterLab sheet also puts Jupyter-rendered output on the variable, at thebe-core.css:2760-2764.

The root cause is one selector away from the fix, and the file records the near miss. styles/quantecon.css:217-234 sets inline literals to font-size: 1rem under a comment explaining that "the Sphinx build sizes inline code at .9rem off its own root (16.2px) … Against this theme's 16px root, 1rem is the same 16px", and the adjacent comment at :220-224 notes that the monospace family "that Tailwind's preflight and JupyterLab's stylesheet already put on code is the right one — the same stack the code blocks use." JupyterLab's sheet was known to be setting the family and was never checked for also setting the size on pre. The :not(pre) > scoping that deliberately kept the literal colour off blocks also kept the size decision off them.

Implementing the size

Two vectors, and they are not equivalent. .article .myst-code pre at (0,2,1) is narrow and obvious, but it leaves the static output text at 14px and does nothing for whatever Thebe swaps in when live compute is enabled, so code could visibly change size when a reader clicks the toggle. Overriding --jp-code-font-size on :root has a small and knowable blast radius: the variable is used in exactly four places in thebe-core.css — the element rule at :269, the Jupyter-output rule at :2764, and two calc()s (--jp-notebook-scroll-padding at :173, --jp-private-code-span-padding at :2752) — and it fixes source blocks and Jupyter-rendered outputs together, keeping them consistent when Thebe activates. It is the better vector for that reason. It does not reach the static .myst-jp-safe-output-text div, which carries font-mono text-sm from @myst-theme/jupyter/dist/safe.js:58 and needs its own rule either way.

The hook for the executable branch

.border-l-blue-400 is upstream's own executable marker — it is emitted if and only if border={node.executable} — so it is semantically exact but couples to a Tailwind class name. The structural alternative is @myst-theme/jupyter's myst-jp-nb-block, but that class appears twice in dist/block.js (on the wrapper at :10 and again on an inner sticky control bar at :13), so it is the sloppier of the two. The lecture-authored no-execute class is passed through onto the wrapper (node.class reaches classNames at code.js:64) and is tempting as a hook for the other branch — do not use it. It is exactly the case the warning at styles/quantecon.css:40-44 describes: layered rules are tree-shaken against Tailwind's content globs, which never include lecture markdown, so a rule keyed on it would be purged from the build.

Cascade traps for whoever writes the CSS

Four things that look right and are not. All checked against the compiled Tailwind bundle in public/build/_assets/, not inferred.

  1. box-shadow: none will not remove the shadow on hover. .article .myst-code is (0,2,0); .hover\:shadow-md:hover is also (0,2,0) — one class plus one pseudo-class — and is emitted much later in the bundle, so it wins the tie on source order. The result is not "no shadow" but "no shadow at rest, shadow on hover", an inconsistency neither build has. Either add a .article .myst-code:hover rule at (0,3,0) or leave the shadow declaration out entirely.
  2. Dark mode breaks in both branches if it is not written explicitly. .article .myst-code.border-l-blue-400 at (0,3,0) beats .dark\:border-gray-800:is(.dark *) at (0,2,0) — darkMode: 'class' in tailwind.config.js:4, Tailwind 3.4.19 — so executable cells would take a light frame on a dark page. Non-executable blocks are worse: they carry no border utility at all, so an unconditional border: 1px solid #e1e1e1 paints a light hairline on dark with nothing to override it. Use the --qe-dark-* values above. The dark fill is largely moot, since .dark .hljs{background:#1e1e1e!important} already paints the inner .myst-code-body; the border is not.
  3. Rounding the outer corners needs overflow: hidden. That same opaque #1e1e1e !important inner body has square corners and will overhang a rounded parent. Upstream's own exercise Callout carries overflow-hidden for exactly this reason (myst-to-react/dist/exercise.js:40).
  4. What does hold. .bg-stone-200\/10 compiles to (0,1,0), so a .article .myst-code rule at (0,2,0) beats it; @layer components is emitted before @layer utilities, verified by byte offset in the built CSS; and both myst-code and border-l-blue-400 survive the components-layer tree-shaking, because they appear in myst-to-react/dist/code.js, which is inside @myst-theme/styles' content globs (index.js:4). The .article-prefixed pattern is the one the footer block already uses for the same reason, with the rationale written out at styles/quantecon.css:375-380: at (0,1,0) a bare class loses to the utilities already on the element because utilities are emitted after components, so the extra .article wins on specificity instead of relying on source order.

What this does to #172

#172's premise sentence — "Code blocks render at text-sm, so the 3:1 large-text allowance does not apply" — is wrong for the reason above; blocks render at 13px, not 14px. Its conclusion survives: none of 13px, 14px, 16px or 16.2px is WCAG large text (18pt / 24px, or 14pt / 18.66px bold), so the 3:1 allowance never applies at any candidate size. Only the stated number is wrong, and it should be corrected there.

More consequentially, #172 measures its contrast against white, which is correct today — executable blocks compute a fully transparent background and non-executable ones compute rgba(231,229,228,0.1), which flattens to #fdfcfc. Putting an opaque card under both kinds moves the ground its whole table is measured against. Against Sphinx's #f7f7f7 every figure drops by exactly 6.7%:

Token Sphinx class Colour On white (#172 as written) On #f7f7f7 AA
f-string substitution .si #70a0d0 2.75:1 2.57:1
comment .c .c1 #60a0b0 2.94:1 2.74:1
number .m .mi .mf #40a070 3.24:1 3.02:1
variable (non-Python) .nv #bb60d5 3.65:1 3.41:1
class name .nc #0e84b5 4.21:1 3.93:1
string .s #4070a0 5.19:1 4.85:1
keyword, builtin .k .nb #007020 6.29:1 5.87:1
decorator .nd #555555 7.46:1 6.96:1
function name .nf #06287e 13.02:1 12.16:1

The count of five failures is unchanged and no colour crosses the line, but every margin shrinks, and #172's candidate replacements — computed to clear 4.5:1 on white — would land at 4.21–4.36:1 on #f7f7f7 and would need recomputing. #172's table should be restated in the same PR that lands the ground colour, otherwise it documents a contrast that no longer exists; a card darker than #f7f7f7 costs more than 6.7% and makes that recomputation mandatory rather than tidy. Inline literals are unaffected: --qe-literal-color: #af5f5f sits on prose background, and .article :not(pre) > code keeps it off blocks.

Baselines

Three snapshot names move — features.png (the fixture's one plain ```python fence at tests/visual/fixture/features.md:54, i.e. the non-executable branch), `history-open.png` (which renders the same `/features` route), and `notebook.png` (`notebook.ipynb` has three code cells) — across all four platform-suffixed directories, so 12 PNGs. `intro.png`, `sidebar-open.png` and `lists.png` are clean: `intro.md`, `lists.md` and `footer.md` contain no code fences, and `sidebar-open` renders `/`. Locally this needs `--update-snapshots=all`, not the bare flag, because a font-size change confined to one block can sit under the 1% `maxDiffPixelRatio` budget at `tests/visual/theme.spec.ts:34` (#113); on CI, the `/update-snapshots` PR comment. No fixture change is strictly required — both branches are already covered — though adding an explicit `{code-block}` case to `features.md` would exercise the non-executable path more directly than the bare fence does.

Where the rule goes

At the foot of styles/quantecon.css, in the CODE HIGHLIGHTING region. That is the same region PR #171 rewrites, and the overlap with it is textual as well as binary in three places: #171's styles/quantecon.css hunk is @@ -468,6 +526,252 @@, exactly where this rule lands; its PLAN.md hunk is @@ -370,6 +371,13 @@, inserting a Phase 3 status block at precisely the lines any widening of the Phase 3 entry would touch (Phase 3's scope as written at PLAN.md:371 is token colours plus text colour schemes, which does not cover the block frame or the code size); and its 20-PNG rewrite covers features.png, history-open.png and notebook.png in all four directories, which is every baseline this moves. So this belongs on top of #171 rather than beside it — shipping the two in parallel reproduces the #165 × #166 × #167 binary conflict. There is no overlap with #174 (ProjectFrontmatter.tsx, root.tsx and the two route files) or with #175.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions