fix: resolve all 21 QA issues from comprehensive audit#25
fix: resolve all 21 QA issues from comprehensive audit#25ryota-murakami merged 22 commits intomainfrom
Conversation
…essing
- Update regex in processEslintMarkdown() to optionally match trailing
JSON config metadata (e.g., { "sourceType": "script" })
- Fixes raw syntax leak in no-dupe-args, no-unused-vars,
no-unused-expressions, and no-redeclare rules
Closes #5
…ssion - Investigation found no code path causing hash navigation on theme toggle click — likely QA automation artifact from Playwright click precision - Added type="button" as defensive measure Closes #6
…le ID
- Prefix h1/h2/h3 IDs inside rule cards with rule.id (e.g., eqeqeq-rule-details)
- Remove duplicate HeadingAnchor id from CardTitle (Card already has id={rule.id})
- Remove duplicate id from installation/configuration sections in page.tsx
Closes #7
- Strip plugin-specific badges (💼,⚠️ , 🔧, 💡, 🚫) and config/fixable metadata lines that are not relevant to the documentation site Closes #2
CSS Cascade Layers spec: un-layered styles beat layered styles regardless of specificity. Moves @media (prefers-reduced-transparency), @media (prefers-reduced-motion), GPU acceleration, and @supports not blocks inside @layer utilities so they cascade correctly with the base glass classes. Fixes broken indentation from partial edit. Closes #15
Replace prefers-color-scheme media queries with .dark class-based scoping for highlight.js themes. This ensures syntax highlighting follows the next-themes toggle instead of OS preference. Adds complete GitHub Dark token colors scoped to .dark class. Also darkens light-mode keyword red from #d73a49 (4.30:1) to #c9242f (~5.1:1) and comment gray from #6a737d to #57606a for WCAG AA compliance. Closes #9 Closes #10
Add custom <a> component to ReactMarkdown with blue-700/blue-400 colors and underline for clear link visibility in both light and dark modes. External links open in new tab. Closes #13
Reduce hero h1 from text-5xl to text-3xl on mobile (<640px) and subtitle from text-xl to text-lg to prevent overflow on 375px screens. Closes #17
Radix UI Sheet (Dialog primitive) requires a title for screen readers. Add visually hidden SheetTitle with "Navigation Menu" text. Closes #14
Add :focus-visible outline using --color-ring variable to ensure all interactive elements show a visible focus ring for keyboard users. Closes #11
- GitHub/theme toggle buttons: add min-h/min-w 44px - Sidebar nav links: increase padding to min-h 44px - Copy link/clipboard buttons: expand from 32px to 44px - Mobile sidebar toggle: add min-h/min-w 44px Closes #8
Restructure HeadingAnchor so the copy-link button is a sibling of the heading rather than a child. This prevents screen readers from appending "Copy link" to every heading name. Closes #20
Increase close button to 44px tap target with hover background, larger X icon (h-5 w-5), and visible hover state for better discoverability on mobile. Closes #21
Add custom h4 ReactMarkdown component styled at text-base (16px) to prevent headings from rendering at the browser default 12px. Closes #18
Change inline code padding from px-1.5/py-0.5 (6px/2px) to px-2/py-1 (8px/4px) to follow the 4px grid system. Off-grid values in shadcn/ui component defaults are left unchanged as they follow the library standard. Closes #19
Change --radius-md from calc(--radius - 2px) = 6px to --radius = 8px to align with the 4/8/12/20px graduated corner radius system. Closes #22
Replace misleading SF Pro declarations with Inter as primary font (loaded via next/font/google) with system-ui fallback. The previous SF Pro stack never took effect since Inter was applied via className. Closes #24
Add glass morphism footer with Laststance.io copyright, GitHub and npm links. Provides the contentinfo ARIA landmark and closes the page properly instead of ending abruptly after the last rule card. Closes #16
Track which rule card is currently visible using IntersectionObserver and highlight the corresponding sidebar link with a blue active state. Uses rootMargin to account for the fixed header. Closes #23
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughUpdates refine global CSS (fonts, radii, reduced-motion handling, focus-visible), restructure markdown styling for class-based dark mode, adjust hero typography and page footer, enlarge icon/touch targets across UI, add scroll‑spy in the rules sidebar, improve markdown link/heading handling, and tweak code-block copy button sizing and positions. Changes
Sequence Diagram(s)sequenceDiagram
participant User as "User (scroll)"
participant Document as "Document / Rule Content"
participant IO as "IntersectionObserver"
participant Sidebar as "RulesSidebar"
User->>Document: Scrolls viewport
Document->>IO: Elements intersect/leave viewport
IO-->>Sidebar: Emits activeId updates
Sidebar-->>Sidebar: Update active item styling
Sidebar->>User: Highlight active rule in UI
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (7)
components/heading-anchor.tsx (1)
82-82:w-8 h-8declarations are dead code —min-h-[44px] min-w-[44px]always winsCSS
min-*properties overridewidth/heightwhen the minimum exceeds the explicit size. Thew-8(32px) andh-8(32px) declarations have no effect here and may mislead the next reader into thinking the button is 32px.♻️ Proposed cleanup
- 'w-8 h-8 min-h-[44px] min-w-[44px] rounded-md', + 'min-h-[44px] min-w-[44px] rounded-md',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/heading-anchor.tsx` at line 82, The class string in the HeadingAnchor component includes redundant 'w-8 h-8' which are overridden by 'min-h-[44px] min-w-[44px]' and should be removed; edit the class list (the array/string that currently contains "'w-8 h-8 min-h-[44px] min-w-[44px] rounded-md'") in components/heading-anchor.tsx to delete 'w-8 h-8' so the remaining classes reflect the actual sizing.components/rules-sidebar.tsx (1)
33-45: Non-deterministic active item when multiple entries intersect simultaneouslyIf more than one rule section enters the intersection zone in the same observer callback (e.g., on page load, after a programmatic scroll, or when sections are taller than the intersection window), the loop
for (const entry of entries)will setactiveIdto whichever intersecting entry the browser delivers last — which is not guaranteed to be the topmost/first rule in the list.♻️ Suggested fix: track all intersecting IDs and deterministically select the first one in rule order
+ const intersectingIds = React.useRef<Set<string>>(new Set()) + useEffect(() => { const ruleIds = rules.map((r) => r.id) const elements = ruleIds .map((id) => document.getElementById(id)) .filter(Boolean) as HTMLElement[] if (elements.length === 0) return const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { - if (entry.isIntersecting) { - setActiveId(entry.target.id) - } + if (entry.isIntersecting) { + intersectingIds.current.add(entry.target.id) + } else { + intersectingIds.current.delete(entry.target.id) + } } + const first = ruleIds.find((id) => intersectingIds.current.has(id)) + if (first) setActiveId(first) }, { rootMargin: '-96px 0px -50% 0px' }, ) for (const el of elements) observer.observe(el) - return () => observer.disconnect() + return () => { + observer.disconnect() + intersectingIds.current.clear() + } }, [rules])🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/rules-sidebar.tsx` around lines 33 - 45, The IntersectionObserver callback currently loops entries and calls setActiveId for each intersecting entry which yields a non-deterministic active item when multiple entries intersect; modify the callback used to create observer so it first collects all entries.filter(e => e.isIntersecting).map(e => e.target.id) then deterministically selects the first id according to the original DOM order in elements (e.g., find the first element in elements whose id is in that intersecting-id set) and call setActiveId once with that id; keep observer, elements and setActiveId references intact and ensure the cleanup (observer.disconnect) remains unchanged.styles/markdown.css (1)
7-14:!importanton background but not on border—intentional?Line 12 uses
!importantto override the importedgithub.cssbackground. Line 13'sborderdeclaration doesn't need!importanttoday becausegithub.cssdoesn't set one, but if highlight.js ever adds a border to its theme, this would silently lose. Low risk; just noting the asymmetry.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@styles/markdown.css` around lines 7 - 14, The .hljs rule applies !important to background but not to border, creating an asymmetry that could let upstream highlight.js/GitHub theme borders override ours in the future; update the .hljs CSS rule to make the border declaration consistent (add !important to the border property) so both background and border use equivalent specificity and will reliably override imported github.css.components/rule-card.tsx (2)
119-128: Inline code detection heuristic is fragile.
const isInline = !classNameassumes that all<code>elements without aclassNameare inline. This works today becauserehype-highlightaddshljsclasses to code blocks inside<pre>, but it's a coupling to an external plugin's behavior. If that changes (or a fenced block without a detected language is rendered), block code would silently get inline styling. This is a pre-existing pattern, so no action needed now—just calling it out.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/rule-card.tsx` around lines 119 - 128, The inline-vs-block detection in the custom code renderer (the anonymous code: ({ children, className }) => { ... }) is fragile because it relies on const isInline = !className; update the heuristic to detect block code more robustly (for example check className for language- or hljs- prefixes and/or detect a newline in children) and replace the isInline check with that logic so fenced blocks without a language or changes to rehype-highlight won't be misclassified; update the code renderer's isInline usage accordingly.
78-113: Heading IDs with inline code elements produce mangled slugs.The markdown documentation contains headings with inline code (e.g.,
#### \maxDepth`,### `allowAny`). When ReactMarkdown renders these, the headingchildrenprop is an array like["The ",foo, " option"]. CallingString()on this array produces"The ,[object Object], option"`, creating non-deterministic anchor IDs that break deep linking.Extract text recursively from children:
// lib/extract-text.ts import { Children, isValidElement, type ReactNode } from 'react' export function extractText(node: ReactNode): string { if (typeof node === 'string' || typeof node === 'number') return String(node) if (Array.isArray(node)) return node.map(extractText).join('') if (isValidElement(node)) return extractText(node.props.children) return '' }Then update each heading renderer:
- id={`${rule.id}-${slugify(String(children))}`} + id={`${rule.id}-${slugify(extractText(children))}`}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/rule-card.tsx` around lines 78 - 113, Heading anchor IDs are being built from String(children) which mangles headings that include inline code or React elements; add a utility function named extractText that recursively extracts plain text from a ReactNode (handling strings, numbers, arrays, and valid React elements) and then replace usages of String(children) in the HeadingAnchor id expressions (the expressions using `${rule.id}-${slugify(String(children))}`) with `${rule.id}-${slugify(extractText(children))}` so slugs are deterministic; ensure the new extractText is imported where the h1/h2/h3/h4 renderers are defined and covers the same renderer locations that reference HeadingAnchor, slugify, and rule.id.lib/markdown-utils.ts (1)
41-54: Duplicated replacement template across the two code-block handlers.Lines 44 and 52 produce identical replacement strings. If formatting ever changes, they'll diverge silently.
♻️ Extract a shared helper
+ const wrapCodeBlockWithOption = (option: string, code: string) => + `<div class="code-block-with-option">\n<div class="code-option">${option}</div>\n\n\`\`\`ts\n${code.trim()}\n\`\`\`\n</div>` + // Handle TypeScript ESLint specific syntax for code blocks with options processedContent = processedContent.replace( /```ts option='([^']+)' showPlaygroundButton\s*\n([\s\S]*?)\n```/g, - (match, option, code) => { - return `<div class="code-block-with-option">\n<div class="code-option">${option}</div>\n\n\`\`\`ts\n${code.trim()}\n\`\`\`\n</div>` - }, + (_match, option, code) => wrapCodeBlockWithOption(option, code), ) // Handle code blocks with just options processedContent = processedContent.replace( /```ts option='([^']+)'\s*\n([\s\S]*?)\n```/g, - (match, option, code) => { - return `<div class="code-block-with-option">\n<div class="code-option">${option}</div>\n\n\`\`\`ts\n${code.trim()}\n\`\`\`\n</div>` - }, + (_match, option, code) => wrapCodeBlockWithOption(option, code), )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@lib/markdown-utils.ts` around lines 41 - 54, There is duplicated HTML replacement logic in the two processedContent.replace callbacks; extract that shared template into a helper function (e.g., wrapCodeBlockWithOption(option, code)) and have both replace calls use (_match, option, code) => wrapCodeBlockWithOption(option, code) instead of inlining the template; update the helper to return the exact HTML string currently duplicated (using code.trim()) and replace both occurrences that handle the /```ts option='([^']+)' showPlaygroundButton.../ and /```ts option='([^']+)'.../ regexes to call this helper.app/page.tsx (1)
253-275: Extract sidebar width as a Tailwind theme value to prevent layout drift.The sidebar (
components/rules-sidebar.tsx) useslg:w-72while the footer (app/page.tsx:254) useslg:ml-72. These must stay synchronized manually—if the sidebar width changes, the footer margin will fall out of alignment. Add a--sidebar-width: 18rem;to the@themeblock inapp/globals.cssand use it in both locations to keep them in sync.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/page.tsx` around lines 253 - 275, Add a CSS variable for the sidebar width and use it in both the sidebar and footer so they stay in sync: add --sidebar-width: 18rem; to the `@theme` block in app/globals.css, then replace the hardcoded tailwind classes lg:w-72 in components/rules-sidebar.tsx and lg:ml-72 in app/page.tsx with responsive utility classes that reference the variable (e.g., lg:w-[var(--sidebar-width)] and lg:ml-[var(--sidebar-width)]) so the width and margin are derived from the same source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/globals.css`:
- Around line 112-117: The font-family stack includes an orphaned CSS variable
reference var(--font-inter) that is never defined; either remove
var(--font-inter) from the font-family declaration in globals.css and rely on
Next.js next/font via inter.className in layout.tsx, or define --font-inter
(e.g., map it to 'Inter, sans-serif') in a root selector so the custom property
resolves; update the font-family line and/or add the --font-inter definition
near :root to make the chosen approach consistent.
- Line 45: The change made --radius-md (and --radius-lg) resolve to
var(--radius) (8px) which increases the rounded-md Tailwind visual radius across
many components; revert these tokens to their previous computed value so
rounded-md stays 6px by setting --radius-md: calc(var(--radius) - 2px) (and
similarly restore --radius-lg if it was changed) in globals.css, then run a
quick visual smoke test of components that use rounded-md (buttons, inputs,
modals, menus, selects, tooltips) to confirm no unintended layout/regression.
In `@components/heading-anchor.tsx`:
- Around line 55-73: The aria-label uses String(children) which breaks when
react-markdown passes an array/React elements; update HeadingAnchor to compute a
readable label: use React.Children.toArray(children) to flatten, map each child
to its text by checking typeof child === 'string' ? child :
(child.props?.children ? recursively extract text from child.props.children :
''), join the pieces into a single string, fallback to an empty string if
nothing readable, and then set aria-label={`Navigate to ${label}`}; refer to the
HeadingAnchor component and its usage of children/aria-label to locate where to
apply this defensive extraction.
In `@lib/markdown-utils.ts`:
- Around line 8-11: The character-class regex passed to processedContent.replace
incorrectly treats surrogate pairs as separate code units, so update the regex
literal used in the replace call (the one matching
/^[💼⚠️🔧💡🚫][^\n]*(?:configs?|fixable|option|deprecated)[^\n]*$/gm) to include
the Unicode flag (u) so it matches full code points (e.g., use /.../gmu or
/.../umg); keep the same pattern text but add the u flag to avoid matching
unintended emojis/variation selectors.
In `@styles/markdown.css`:
- Around line 26-31: The inline comment above the syntax highlighting rules is
stale: update the comment to reflect the actual color used (`#57606a`) and its
correct WCAG contrast ratio (6.00:1) so it matches the CSS selectors
.hljs-comment, .hljs-code, .hljs-formula; replace the existing “…#636c76
(5.3:1)” text with “…#57606a (6.00:1)” (or equivalent accurate wording) to keep
the comment consistent with the applied color.
---
Nitpick comments:
In `@app/page.tsx`:
- Around line 253-275: Add a CSS variable for the sidebar width and use it in
both the sidebar and footer so they stay in sync: add --sidebar-width: 18rem; to
the `@theme` block in app/globals.css, then replace the hardcoded tailwind classes
lg:w-72 in components/rules-sidebar.tsx and lg:ml-72 in app/page.tsx with
responsive utility classes that reference the variable (e.g.,
lg:w-[var(--sidebar-width)] and lg:ml-[var(--sidebar-width)]) so the width and
margin are derived from the same source of truth.
In `@components/heading-anchor.tsx`:
- Line 82: The class string in the HeadingAnchor component includes redundant
'w-8 h-8' which are overridden by 'min-h-[44px] min-w-[44px]' and should be
removed; edit the class list (the array/string that currently contains "'w-8 h-8
min-h-[44px] min-w-[44px] rounded-md'") in components/heading-anchor.tsx to
delete 'w-8 h-8' so the remaining classes reflect the actual sizing.
In `@components/rule-card.tsx`:
- Around line 119-128: The inline-vs-block detection in the custom code renderer
(the anonymous code: ({ children, className }) => { ... }) is fragile because it
relies on const isInline = !className; update the heuristic to detect block code
more robustly (for example check className for language- or hljs- prefixes
and/or detect a newline in children) and replace the isInline check with that
logic so fenced blocks without a language or changes to rehype-highlight won't
be misclassified; update the code renderer's isInline usage accordingly.
- Around line 78-113: Heading anchor IDs are being built from String(children)
which mangles headings that include inline code or React elements; add a utility
function named extractText that recursively extracts plain text from a ReactNode
(handling strings, numbers, arrays, and valid React elements) and then replace
usages of String(children) in the HeadingAnchor id expressions (the expressions
using `${rule.id}-${slugify(String(children))}`) with
`${rule.id}-${slugify(extractText(children))}` so slugs are deterministic;
ensure the new extractText is imported where the h1/h2/h3/h4 renderers are
defined and covers the same renderer locations that reference HeadingAnchor,
slugify, and rule.id.
In `@components/rules-sidebar.tsx`:
- Around line 33-45: The IntersectionObserver callback currently loops entries
and calls setActiveId for each intersecting entry which yields a
non-deterministic active item when multiple entries intersect; modify the
callback used to create observer so it first collects all entries.filter(e =>
e.isIntersecting).map(e => e.target.id) then deterministically selects the first
id according to the original DOM order in elements (e.g., find the first element
in elements whose id is in that intersecting-id set) and call setActiveId once
with that id; keep observer, elements and setActiveId references intact and
ensure the cleanup (observer.disconnect) remains unchanged.
In `@lib/markdown-utils.ts`:
- Around line 41-54: There is duplicated HTML replacement logic in the two
processedContent.replace callbacks; extract that shared template into a helper
function (e.g., wrapCodeBlockWithOption(option, code)) and have both replace
calls use (_match, option, code) => wrapCodeBlockWithOption(option, code)
instead of inlining the template; update the helper to return the exact HTML
string currently duplicated (using code.trim()) and replace both occurrences
that handle the /```ts option='([^']+)' showPlaygroundButton.../ and /```ts
option='([^']+)'.../ regexes to call this helper.
In `@styles/markdown.css`:
- Around line 7-14: The .hljs rule applies !important to background but not to
border, creating an asymmetry that could let upstream highlight.js/GitHub theme
borders override ours in the future; update the .hljs CSS rule to make the
border declaration consistent (add !important to the border property) so both
background and border use equivalent specificity and will reliably override
imported github.css.
- Remove orphaned var(--font-inter) from CSS font-family (next/font handles it) - Add unicode flag (u) to emoji regex to prevent surrogate pair false positives - Use id fallback in aria-label when children is not a string - Fix stale WCAG contrast comment (#636c76 → #57606a, 6.00:1)
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
styles/markdown.css (1)
280-284:⚠️ Potential issue | 🟡 MinorUse
:focus-visibleinstead of:focusto avoid spurious mouse-click outlines.
:focusapplies the ring on every interaction (mouse, touch, keyboard).:focus-visiblerestricts it to keyboard/programmatic focus, which is the intended behavior. The PR description explicitly states "Add global:focus-visibleindicator", so this appears to be an unintentional omission.🔧 Proposed fix
-.markdown-content a:focus, -.markdown-content button:focus { +.markdown-content a:focus-visible, +.markdown-content button:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@styles/markdown.css` around lines 280 - 284, Replace the use of :focus on the focus styles for links and buttons to :focus-visible to avoid showing the ring on mouse/touch interactions; specifically update the selectors .markdown-content a:focus and .markdown-content button:focus to .markdown-content a:focus-visible and .markdown-content button:focus-visible and keep the existing outline and outline-offset rules unchanged.
🧹 Nitpick comments (4)
components/heading-anchor.tsx (2)
38-50: UnguardedsetTimeout— store and clear the handle on unmount.If the component unmounts within 2 s of a copy, the stale timer fires; React 18 makes the setState call harmless, but the orphaned timer is still a resource leak pattern.
♻️ Proposed fix using
useRefcleanup+import { useState, useRef, useEffect } from 'react' -import { useState } from 'react'const [copied, setCopied] = useState(false) + const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null) + + useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current) }, []) const handleCopyLink = async () => { const url = `${window.location.origin}${window.location.pathname}#${id}` try { await navigator.clipboard.writeText(url) setCopied(true) - setTimeout(() => setCopied(false), 2000) + if (timerRef.current) clearTimeout(timerRef.current) + timerRef.current = setTimeout(() => setCopied(false), 2000) } catch (err) { console.error('Failed to copy link:', err) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/heading-anchor.tsx` around lines 38 - 50, The setTimeout in handleCopyLink creates an untracked timer that can leak when the component unmounts; introduce a ref (e.g., timeoutRef) to store the timer id when calling setTimeout in handleCopyLink, and add a useEffect cleanup that clears the timer (clearTimeout(timeoutRef.current)) and resets the ref on unmount; also clear any existing timer before setting a new one in handleCopyLink to avoid overlapping timers and ensure setCopied state updates are tied to a live timer handle.
67-67: Preferfocus-visible:overfocus:for focus rings on both the anchor and the button.The PR adds a global
:focus-visibleindicator, signalling the intent to show rings only on keyboard navigation. Usingfocus:shows the ring on mouse clicks as well, which is visually noisy and contradicts that intent.♻️ Proposed fix — anchor (line 67) and button (line 93)
- 'focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2', + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2',- 'focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-1', + 'focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-1',Also applies to: 93-93
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/heading-anchor.tsx` at line 67, In the HeadingAnchor component update the Tailwind focus utilities to use focus-visible instead of focus: so the focus ring only appears for keyboard navigation; specifically replace 'focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2' on the anchor and the analogous focus classes on the toggle button with 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2' so both the anchor element and the button use focus-visible utilities.styles/markdown.css (2)
16-24: Update the CSS comment to reflect the precise contrast ratio for#c9242f.The actual contrast ratio for
#c9242fis 5.56:1 (not 5.1:1 as stated), while#d73a49is 4.57:1 (not 4.30:1). Both safely exceed WCAG AA's 4.5:1 requirement, but the comment should be accurate.Consider updating the comment to:
/* WCAG AA contrast fix — darken keyword red from#d73a49(4.57:1) to#c9242f(5.56:1) */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@styles/markdown.css` around lines 16 - 24, The comment above the color rule has incorrect contrast ratio numbers; update the CSS comment to accurately state the contrast ratios for the old and new hex values. Replace the existing comment text with one that reads: "WCAG AA contrast fix — darken keyword red from `#d73a49` (4.57:1) to `#c9242f` (5.56:1)" so the comment above the selectors (.hljs-doctag, .hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type, .hljs-variable.language_) reflects the precise contrast values.
150-171: Remove the redundant[data-theme='dark']selectors from ESLint block dark-mode rules.The app uses
next-themesconfigured withattribute="class", which applies theming exclusively via the.darkclass. The syntax-highlighting dark mode (lines 33–113) consistently uses only.dark, and the ESLint block section should follow the same pattern. The[data-theme='dark']selectors are unused and create unnecessary divergence.♻️ Proposed cleanup
-[data-theme='dark'] .eslint-block.incorrect, .dark .eslint-block.incorrect { background-color: rgb(127 29 29 / 0.3); border-color: rgb(239 68 68); } -[data-theme='dark'] .eslint-block.incorrect .eslint-block-header, .dark .eslint-block.incorrect .eslint-block-header { color: rgb(254 87 87); } -[data-theme='dark'] .eslint-block.correct, .dark .eslint-block.correct { background-color: rgb(20 83 45 / 0.3); border-color: rgb(74 222 128); } -[data-theme='dark'] .eslint-block.correct .eslint-block-header, .dark .eslint-block.correct .eslint-block-header { color: rgb(134 239 172); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@styles/markdown.css` around lines 150 - 171, Remove the redundant [data-theme='dark'] selectors from the ESLint block dark-mode rules so they match the rest of the stylesheet that uses attribute="class" theming; update the selectors that reference [data-theme='dark'] .eslint-block.incorrect, [data-theme='dark'] .eslint-block.correct and their respective .eslint-block-header variants to use only the .dark .eslint-block... form (e.g., .dark .eslint-block.incorrect and .dark .eslint-block.incorrect .eslint-block-header, and the corresponding .correct selectors), leaving the same background-color, border-color, and color declarations unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@components/heading-anchor.tsx`:
- Line 76: The copy button in the HeadingAnchor component is missing an explicit
type, so it defaults to type="submit" and may inadvertently submit surrounding
forms; update the <button> element used for the copy action in
components/heading-anchor.tsx (the copy button inside the
HeadingAnchor/HeadingAnchorButton JSX) to include type="button" to prevent form
submission and keep behavior consistent with the theme toggle change.
In `@lib/markdown-utils.ts`:
- Around line 13-37: The regexes used in the four processedContent.replace calls
for the "::: incorrect" and "::: correct" blocks use \{[^}]*\} which fails on
nested JSON metadata; update each pattern to use [^\n]* instead of \{[^}]*\} so
the optional metadata token consumes everything up to the end of the line (e.g.,
change the two multiline block-match regexes and the two standalone-line regexes
to replace \{[^}]*\} with [^\n]* in the processedContent.replace calls that
handle ":::" incorrect and ":::" correct).
---
Outside diff comments:
In `@styles/markdown.css`:
- Around line 280-284: Replace the use of :focus on the focus styles for links
and buttons to :focus-visible to avoid showing the ring on mouse/touch
interactions; specifically update the selectors .markdown-content a:focus and
.markdown-content button:focus to .markdown-content a:focus-visible and
.markdown-content button:focus-visible and keep the existing outline and
outline-offset rules unchanged.
---
Duplicate comments:
In `@lib/markdown-utils.ts`:
- Around line 8-11: The regex in the processedContent.replace call previously
risked surrogate-pair mismatches; ensure the Unicode flag remains on the pattern
used in processedContent.replace (the
/^[💼⚠️🔧💡🚫][^\n]*(?:configs?|fixable|option|deprecated)[^\n]*$/gmu regex) —
keep the trailing u flag, confirm the g and m flags are present, and add/adjust
a unit test covering emoji-starting lines to prevent regressions.
In `@styles/markdown.css`:
- Around line 26-31: The WCAG color update in the CSS is correct and requires no
code changes; confirm and keep the selector rule (.hljs-comment, .hljs-code,
.hljs-formula { color: `#57606a`; }) as-is, mark the change approved, and remove
any duplicate review comments referencing this same change to avoid clutter.
---
Nitpick comments:
In `@components/heading-anchor.tsx`:
- Around line 38-50: The setTimeout in handleCopyLink creates an untracked timer
that can leak when the component unmounts; introduce a ref (e.g., timeoutRef) to
store the timer id when calling setTimeout in handleCopyLink, and add a
useEffect cleanup that clears the timer (clearTimeout(timeoutRef.current)) and
resets the ref on unmount; also clear any existing timer before setting a new
one in handleCopyLink to avoid overlapping timers and ensure setCopied state
updates are tied to a live timer handle.
- Line 67: In the HeadingAnchor component update the Tailwind focus utilities to
use focus-visible instead of focus: so the focus ring only appears for keyboard
navigation; specifically replace 'focus:outline-none focus:ring-2
focus:ring-primary/50 focus:ring-offset-2' on the anchor and the analogous focus
classes on the toggle button with 'focus-visible:outline-none
focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2'
so both the anchor element and the button use focus-visible utilities.
In `@styles/markdown.css`:
- Around line 16-24: The comment above the color rule has incorrect contrast
ratio numbers; update the CSS comment to accurately state the contrast ratios
for the old and new hex values. Replace the existing comment text with one that
reads: "WCAG AA contrast fix — darken keyword red from `#d73a49` (4.57:1) to
`#c9242f` (5.56:1)" so the comment above the selectors (.hljs-doctag,
.hljs-keyword, .hljs-template-tag, .hljs-template-variable, .hljs-type,
.hljs-variable.language_) reflects the precise contrast values.
- Around line 150-171: Remove the redundant [data-theme='dark'] selectors from
the ESLint block dark-mode rules so they match the rest of the stylesheet that
uses attribute="class" theming; update the selectors that reference
[data-theme='dark'] .eslint-block.incorrect, [data-theme='dark']
.eslint-block.correct and their respective .eslint-block-header variants to use
only the .dark .eslint-block... form (e.g., .dark .eslint-block.incorrect and
.dark .eslint-block.incorrect .eslint-block-header, and the corresponding
.correct selectors), leaving the same background-color, border-color, and color
declarations unchanged.
- Add type="button" to copy link button in heading-anchor to prevent accidental form submission (HTML buttons default to type="submit") - Fix regex patterns in markdown-utils to handle nested JSON metadata in ESLint ::: incorrect/correct blocks (e.g. nested parserOptions)
Summary
Resolves all 21 open issues identified by the QA agent team audit. Each issue was investigated, implemented, and verified individually with its own commit.
Bug Fixes
::: incorrect/correctwith JSON metadata #5: Handle ESLint::: incorrect/correctblocks with JSON metadatathis is included reccmed#2: Strip ESLint plugin metadata lines (💼, 🔧, etc.) from rendered contenttype="button"to theme togglenoneon glass morphism elements #15: Move glass override blocks inside@layer utilities(CSS Cascade Layers fix)prefers-color-schemeimports with.darkclass-based syntax highlightingscroll-behavior: smoothwithprefers-reduced-motionAccessibility
:focus-visibleindicator for keyboard navigationDesign System
Features
Test plan
pnpm lint— passespnpm typecheck— passespnpm build— passespnpm test— 3 passed, 8 skipped (same as baseline)Closes #2, #5, #6, #7, #8, #9, #10, #11, #12, #13, #14, #15, #16, #17, #18, #19, #20, #21, #22, #23, #24
Summary by CodeRabbit
New Features
Style
Accessibility