From 37b51ec0579afa486295789f6e2a89705e02eeb3 Mon Sep 17 00:00:00 2001 From: "Thierry V." <46031203+thierryvm@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:33:13 +0200 Subject: [PATCH] fix(ui): show every character of monospace text, no font ligatures JetBrains Mono ships programming ligatures (contextual alternates). It drew `--` as one continuous line: a learner typing `git merge --no-ff` saw one of the two dashes disappear as soon as the space was typed, and it reappeared after Enter. They had to paste the command to finish the exercise (reported by @thierryvm, 24 September 2026, reproduced in Chrome). The characters were never altered: the input held "-- " (recorded keydown/beforeinput/input events). Only the rendering merged them. Ligatures are now disabled for .font-mono, code, kbd, samp and pre. The property is inherited, so it covers the terminal output, its input, the landing terminal demo, lesson code blocks and inline code chips. A static test guards the rule (jsdom does not apply stylesheets); the visual effect was checked in Chrome. Known limit: WebKit (Safari macOS / iOS) is documented as unable to turn off contextual alternates from CSS. Not verified on a real Apple device yet; the ligature-free font fallback is tracked in THI-354. Refs THI-353, THI-354 Co-Authored-By: Claude Opus 5.5 --- src/styles/theme.css | 24 ++++++++++++++++++++++++ src/test/monospaceLigatures.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 src/test/monospaceLigatures.test.ts diff --git a/src/styles/theme.css b/src/styles/theme.css index 12b1f5ad..c907d9e1 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -217,6 +217,30 @@ max-width: 100vw; } + /* + * Monospace text is what learners read and type character by character. + * JetBrains Mono ships programming ligatures (contextual alternates): `--` + * is drawn as one continuous line, `->` as an arrow, `!=` as `≠`. In a + * terminal course that hides the very characters being taught — a learner + * typing `--no-ff` saw one of the two dashes "disappear" as soon as the + * space was typed, and had to paste the command to succeed (reported + * 24 September 2026). The property is inherited, so this covers the + * terminal output, its input and every code block or inline code chip. + * + * Known limit: WebKit (Safari macOS / iOS) is documented as unable to turn + * off contextual alternates from CSS. Verified in Chrome only; if Safari + * still merges `--`, the fix is a ligature-free font (JetBrains Mono NL), + * tracked in THI-354. + */ + .font-mono, + code, + kbd, + samp, + pre { + font-variant-ligatures: none; + font-feature-settings: "liga" 0, "calt" 0; + } + /** * Default typography styles for HTML elements (h1-h4, p, label, button, input). * These are in @layer base, so Tailwind utility classes (like text-sm, text-lg) automatically override them. diff --git a/src/test/monospaceLigatures.test.ts b/src/test/monospaceLigatures.test.ts new file mode 100644 index 00000000..f1e309b1 --- /dev/null +++ b/src/test/monospaceLigatures.test.ts @@ -0,0 +1,24 @@ +/** + * Regression guard (24 September 2026): JetBrains Mono draws `--` as one + * continuous line, so a learner typing `--no-ff` saw a dash "disappear" and had + * to paste the command. jsdom does not apply stylesheets, so this checks the + * rule itself; the visual effect was verified in Chrome. + */ +import { describe, it, expect } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +const css = readFileSync(resolve(__dirname, '../styles/theme.css'), 'utf8'); + +describe('monospace text shows every character as typed', () => { + it('JetBrains Mono is the monospace font (the reason this rule exists)', () => { + expect(css).toMatch(/--font-mono:\s*'JetBrains Mono'/); + }); + + it('disables ligatures and contextual alternates for .font-mono, code, kbd, samp and pre', () => { + const rule = css.match(/\.font-mono,\s*code,\s*kbd,\s*samp,\s*pre\s*\{([^}]*)\}/); + expect(rule, 'monospace ligature rule missing from theme.css').not.toBeNull(); + expect(rule![1]).toMatch(/font-variant-ligatures:\s*none/); + expect(rule![1]).toMatch(/"calt"\s*0/); + }); +});