Skip to content

Comments

fix: resolve all 21 QA issues from comprehensive audit#25

Merged
ryota-murakami merged 22 commits intomainfrom
feat/bulk-issues-20260220
Feb 19, 2026
Merged

fix: resolve all 21 QA issues from comprehensive audit#25
ryota-murakami merged 22 commits intomainfrom
feat/bulk-issues-20260220

Conversation

@ryota-murakami
Copy link
Contributor

@ryota-murakami ryota-murakami commented Feb 19, 2026

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

Accessibility

Design System

Features

Test plan

  • pnpm lint — passes
  • pnpm typecheck — passes
  • pnpm build — passes
  • pnpm 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

    • Added Footer with copyright and external links.
    • Sidebar now highlights the active rule while scrolling.
  • Style

    • Improved dark-mode theming and code block styling.
    • Adjusted hero and body typography for better responsive scaling.
    • Updated markdown link behavior to open external links safely.
  • Accessibility

    • Larger interactive hit targets for buttons and copy controls.
    • Added visible focus indicators and respected reduced-motion preferences.

…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
Wrap scroll-behavior: smooth in @media (prefers-reduced-motion: no-preference)
so users who prefer reduced motion get instant scroll instead of animated.

Closes #12
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
@vercel
Copy link
Contributor

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
website-eslint-config-ts-prefixer Ready Ready Preview, Comment Feb 19, 2026 7:54pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Warning

Rate limit exceeded

@ryota-murakami has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 23 minutes and 18 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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.

📝 Walkthrough

Walkthrough

Updates 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

Cohort / File(s) Summary
Global Styles
app/globals.css
Adjusted radius variable, simplified font stack, moved smooth scroll under prefers-reduced-motion, added global focus-visible rules, reorganized feature/accessibility nesting and minor formatting.
Markdown Styling
styles/markdown.css
Replaced media-query highlight theme imports with single light theme and class-based .dark overrides; expanded dark-mode token color mappings and code-block styling.
Page Layout & Typography
app/page.tsx
Reduced hero headline/paragraph sizes at smaller breakpoints, removed some id-wrapped sections, and added a new Footer with external links.
Touch Targets & Buttons
components/header.tsx, components/theme-toggle.tsx, components/ui/sheet.tsx, components/rules-sidebar.tsx
Added or increased min hit-area (min-h-[44px], min-w-[44px]) and adjusted close/toggle button positioning and sizing for consistent touch targets and improved mobile hit areas.
Code Block Copy UI
components/code-block-with-copy.tsx, components/pre-code-block-with-copy.tsx
Increased copy button size (h-8→h-11, w-8→w-11) and adjusted horizontal positioning (right-2→right-1) in both inline and block code components; preserved behavior/transitions.
Heading Anchors & Link Copy
components/heading-anchor.tsx
Wrapped headings with a div.group relative, adjusted scroll offset, enlarged and repositioned copy-link button, refined aria-label behavior to prefer heading text, and removed sr-only dynamic feedback span.
Rule Card & Markdown Rendering
components/rule-card.tsx, lib/markdown-utils.ts
Removed HeadingAnchor wrapper, prefixed generated heading IDs with rule.id, added h4 renderer, increased inline code padding, added custom anchor renderers that open external links safely, and improved handling of ESLint-style metadata blocks and code-block option formatting.
Sidebar Scroll‑Spy
components/rules-sidebar.tsx
Added IntersectionObserver-based scroll-spy to set activeId, applied active styling to sidebar items, added SheetTitle for accessibility, and improved mobile header/button sizing.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related issues

Poem

🐰
I hopped through CSS and font delight,
Bigger buttons make taps feel right,
Dark themes shimmer, links behave,
Scroll‑spy guides from cave to cave,
A tiny carrot for accessible light. 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: resolve all 21 QA issues from comprehensive audit' accurately summarizes the main objective of the PR, which addresses multiple QA-identified issues across the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/bulk-issues-20260220

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
components/heading-anchor.tsx (1)

82-82: w-8 h-8 declarations are dead code — min-h-[44px] min-w-[44px] always wins

CSS min-* properties override width/height when the minimum exceeds the explicit size. The w-8 (32px) and h-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 simultaneously

If 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 set activeId to 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: !important on background but not on border—intentional?

Line 12 uses !important to override the imported github.css background. Line 13's border declaration doesn't need !important today because github.css doesn'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 = !className assumes that all <code> elements without a className are inline. This works today because rehype-highlight adds hljs classes 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 heading childrenprop is an array like["The ", foo, " option"]. Calling String()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) uses lg:w-72 while the footer (app/page.tsx:254) uses lg: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 @theme block in app/globals.css and 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)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Use :focus-visible instead of :focus to avoid spurious mouse-click outlines.

:focus applies the ring on every interaction (mouse, touch, keyboard). :focus-visible restricts it to keyboard/programmatic focus, which is the intended behavior. The PR description explicitly states "Add global :focus-visible indicator", 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: Unguarded setTimeout — 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 useRef cleanup
+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: Prefer focus-visible: over focus: for focus rings on both the anchor and the button.

The PR adds a global :focus-visible indicator, signalling the intent to show rings only on keyboard navigation. Using focus: 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 #c9242f is 5.56:1 (not 5.1:1 as stated), while #d73a49 is 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-themes configured with attribute="class", which applies theming exclusively via the .dark class. 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)
@ryota-murakami ryota-murakami merged commit 37ba7ae into main Feb 19, 2026
4 checks passed
@ryota-murakami ryota-murakami deleted the feat/bulk-issues-20260220 branch February 19, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Remove orignal eslint plugin specific text like this is included reccmed

1 participant