feat(code-block): highlight via the CSS Custom Highlight API, add lineNumbers - #131
Merged
Conversation
…eNumbers Replace react-syntax-highlighter with MicroLighter, which reads TextMate grammars and paints ranges through the CSS Custom Highlight API rather than wrapping every token in a span. The palette now lives wholly in the stylesheet and resolves through the cascade, so nested themes and live theme flips are correct without a re-render, and no component has to resolve a theme in JS. Bundle cost drops from 223 KB gzip, all of it static, to 3.0 KB gzip with grammars code-split and fetched per language. Add a lineNumbers prop: a sticky gutter that stays put while long lines scroll under it. Numbering and wrapping are mutually exclusive, since a wrapped line occupies more rows than its number does, so setting it switches long lines from wrapping to horizontal scrolling. Make the scroll region focusable (tabIndex, role=region, labelled from title) so the overflow it owns is reachable by keyboard. This closes a pre-existing gap from max-h-64 as well as the new horizontal scrolling. Export CODE_BLOCK_ATTRIBUTE and scheduleHighlight from lib/ so an application can opt its own <pre> into the same pass and palette. Token colours change in both themes: strings, numbers and keywords all shift in light, and dark moves from a near-neon palette to vscode-plus. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The flat-config `ignores: ['dist']` pattern resolves relative to the config file, so it only ever ignored a root-level /dist and `pnpm lint` failed on packages/ui/dist whenever the library had been built. Glob it instead. AvatarShapeDemo took a `variant` prop it never read and no caller passed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
baseUrl is deprecated and stops functioning in TypeScript 7. Without it, paths resolve relative to the tsconfig that declares them, so every mapping is now written that way. Nothing relied on baseUrl's bare-specifier lookup — internal imports all go through the @/, @demo/ and @eqtylab/ aliases or relative paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The file exported bare sample strings, the only one in src/components/demo that didn't export *Demo components. Export three demo components instead, with each sample inline at its point of use, and call them from the MDX the way every other docs page calls its demos. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The docs contain copy/paste issues and the wrapping behavior for long unbroken tokens appears inconsistent with the intended wrap-vs-scroll UX.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates the CodeBlock component in the Equality design system to use MicroLighter + the CSS Custom Highlight API for token coloring (removing Prism token DOM spans and JS-resolved themes), adds optional sticky line numbers, and includes related demo/docs updates plus small lint/tsconfig maintenance.
Changes:
- Replace
react-syntax-highlighterwithmicrolighter, introducing a sharedscheduleHighlight()pass and CSS-driven syntax palettes. - Add a
lineNumbers?: booleanprop toCodeBlockwith a sticky gutter and horizontal scrolling when enabled. - Update demo documentation and samples; adjust ESLint ignore globs and remove deprecated
baseUrlusage from tsconfigs.
File summaries
| File | Description |
|---|---|
| pnpm-lock.yaml | Removes react-syntax-highlighter deps and adds microlighter to the lockfile. |
| packages/ui/package.json | Bumps @eqtylab/equality to 3.4.0 and swaps highlighting dependency to microlighter. |
| packages/ui/tsconfig.json | Removes baseUrl and rewrites paths mappings to be self-relative. |
| packages/demo/tsconfig.json | Removes baseUrl and rewrites paths mappings to be self-relative. |
| packages/ui/src/types/microlighter.d.ts | Adds a local module declaration for microlighter to type highlightAll. |
| packages/ui/src/lib/highlight.ts | Introduces shared CODE_BLOCK_ATTRIBUTE and scheduleHighlight() for coalesced rescans. |
| packages/ui/src/index.ts | Exports CODE_BLOCK_ATTRIBUTE and scheduleHighlight as new public API. |
| packages/ui/src/components/code-block/code-block.tsx | Rebuilds CodeBlock markup for text-node-based highlighting, adds focusable scroll region + line numbers. |
| packages/ui/src/components/code-block/code-block.module.css | Moves token palette to CSS ::highlight() rules and adds layout styles for numbered gutter. |
| packages/demo/src/content/components/code-block.mdx | Rewrites docs for new engine, browser support, line numbers, and custom markup highlighting. |
| packages/demo/src/components/demo/code-block.tsx | Adds multi-line sample sources for docs demos (esp. line numbers). |
| packages/demo/src/components/demo/avatar.tsx | Removes an unused variant prop from AvatarShapeDemo. |
| eslint.config.mjs | Fixes ignore patterns to match dist and node_modules in any package directory. |
Review details
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
- Files reviewed: 11/13 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
`highlightAll` collects its elements before awaiting their grammars, then clears every registered range once they land. Two passes in flight therefore let the slower one finish last and wipe blocks it was too early to see: a `rust` block awaiting its grammar would drop a `json` block that mounted a frame later and resolved from cache first. Queue passes behind one another instead of clearing the coalescing slot before the pass has read the DOM. Rework the props table into the `| Name | Description | Type | Default | Required |` shape the create-documentation skill mandates, which 52 of the 59 component docs already use, and drop the `className` row the skill says to leave out. `code` is now marked required in its own column rather than by putting "Required" in the Default cell. Also correct "four color variants" to five, which is what the bullets beneath it and the Color Variants section both list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Group the scroll container rather than making it a region: `region` is a landmark, so a docs page of code blocks filled the landmark menu with eight of them. The accessible name and the tab stop that reaches the overflow both stay. Share one `--code-line-height` between the gutter and the code instead of repeating 1.5 in each with a comment warning they must match. Drop a single-argument `cn()`, a bracket lookup for a plain key, and a tsconfig include already covered by `src`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The snippet already opens with an import, so the bare hook read as an oversight rather than as the elision it is elsewhere in the docs. Matches dialog and date-range-picker, which import their hooks from react. Drop the demo file's preamble comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Context
CodeBlockhighlighted throughreact-syntax-highlighter(Prism +refractor/all), which wraps every token in a<span>and ships its themes as JS objects applied as inline styles. That forced the component to resolve the active theme in JS — the only place in the library doing so — which picked the wrong palette when two themes were on screen and couldn't see the theme at all from inside a shadow root.What changed
Highlighting engine
Swapped to MicroLighter, which reads TextMate grammars and paints ranges through the CSS Custom Highlight API. No token spans: ranges are registered against the existing text node, so the DOM a user copies is the source string. Grammars load on demand, so a page only pays for the languages it renders.
The palette moved wholly into the stylesheet and now resolves through the cascade, so nested themes and live theme flips are correct with no re-render.
Bundle cost, measured by bundling each library in isolation with esbuild
--minify, React external:react-syntax-highlighter(Prismentry)microlighterinitial chunkThe old import was static, so all 632 KB landed in the initial bundle. MicroLighter's grammars are dynamic imports — esbuild split them into 37 separate chunks, confirming the on-demand behaviour. Per-grammar cost: JSON 304 B gzip, bash 493 B, TypeScript 552 B. A page rendering JSON, TS and bash goes from 223 KB gzip to ~4.4 KB.
New
lineNumberspropNumbers every line in a sticky gutter that stays put while long lines scroll under it. Numbering and wrapping are mutually exclusive — a wrapped line occupies more rows than its number does — so setting it switches long lines from wrapping to horizontal scrolling.
Accessibility
The scroll region is now focusable (
tabIndex={0},role="region", labelled fromtitle) with the standard focus ring, so the overflow it owns is reachable by keyboard. This closes a pre-existing gap frommax-h-64as well as the new horizontal scrolling.New public exports
CODE_BLOCK_ATTRIBUTEandscheduleHighlight(fromlib/highlight.ts) let an application opt its own<pre>— a diff view, an editor — into the same pass and palette.Consumer-visible changes
::highlight()accepts only colour and decoration properties, so Markdown**bold**/_italic_and CSS!importanttake a colour instead.@eqtylab/equality3.3.1 → 3.4.0.Docs
code-block.mdxrewritten for the new engine: the 37 shipped grammars and 13 aliases, browser support,::highlight()styling limits, thelineNumberssection, and a section on highlighting your own markup.Multi-line samples moved into
packages/demo/src/components/demo/code-block.tsx— Prettier's MDX parser reformats template literals embedded in JSX expressions and flattens their indentation, which defeats the point of a line-numbers demo.Also in this PR
Two unrelated cleanups picked up along the way, each in its own commit:
chore(lint)— the flat-configignores: ['dist']pattern resolves relative to the config file, so it only ever ignored a root-level/distandpnpm lintfailed onpackages/ui/distwhenever the library had been built. Globbed to**/dist. Also dropped avariantpropAvatarShapeDemonever read.chore(tsconfig)—baseUrlis deprecated and stops functioning in TypeScript 7. Removed from both tsconfigs with everypathsmapping rewritten self-relative. Nothing relied onbaseUrl's bare-specifier lookup; internal imports all go through the@/,@demo/and@eqtylab/aliases or relative paths.Verification
pnpm lint→ exit 0pnpm format:check→ clean, whole repotsc --noEmit(ui) → exit 0;astro check(demo) → 0 errors, 0 warningspnpm buildfrom deleteddistdirs → both packages built, 67 demo pages@/specifiers indist/index.jsanddist/index.d.ts, confirming tsup/esbuild still resolves aliases withoutbaseUrl🤖 Generated with Claude Code