Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 37 additions & 31 deletions src/components/WritingPad.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,31 @@ const WritingPad = () => {
? ACCENT_CHARACTERS[currentIndex]
: TRACE_WORDS[currentIndex];

// Draw the guide character/word
const drawGuide = useCallback(() => {
const canvas = canvasRef.current;
const ctx = contextRef.current;
if (!canvas || !ctx) return;

// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);

// Draw guide text
ctx.save();
ctx.fillStyle = 'rgba(139, 92, 246, 0.15)'; // Light purple
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';

const text = mode === 'characters' ? currentItem.char : currentItem.word;
const fontSize = mode === 'characters' ? 180 : Math.max(40, 200 / text.length);
ctx.font = `${fontSize}px 'Georgia', serif`;

const centerX = canvas.width / 4;
const centerY = canvas.height / 4;
ctx.fillText(text, centerX, centerY);
ctx.restore();
}, [currentItem, mode]);

// Initialize canvas
useEffect(() => {
const canvas = canvasRef.current;
Expand All @@ -105,7 +130,8 @@ const WritingPad = () => {
if (showGuide) {
drawGuide();
}
}, [currentIndex, mode, showGuide]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentIndex, mode, showGuide, drawGuide]);

// Update stroke settings
useEffect(() => {
Expand All @@ -115,31 +141,6 @@ const WritingPad = () => {
}
}, [strokeColor, strokeWidth]);

// Draw the guide character/word
const drawGuide = useCallback(() => {
const canvas = canvasRef.current;
const ctx = contextRef.current;
if (!canvas || !ctx) return;

// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);

// Draw guide text
ctx.save();
ctx.fillStyle = 'rgba(139, 92, 246, 0.15)'; // Light purple
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';

const text = mode === 'characters' ? currentItem.char : currentItem.word;
const fontSize = mode === 'characters' ? 180 : Math.max(40, 200 / text.length);
ctx.font = `${fontSize}px 'Georgia', serif`;

const centerX = canvas.width / 4;
const centerY = canvas.height / 4;
ctx.fillText(text, centerX, centerY);
ctx.restore();
}, [currentItem, mode]);

// Drawing handlers
const startDrawing = ({ nativeEvent }) => {
const { offsetX, offsetY } = getCoordinates(nativeEvent);
Expand Down Expand Up @@ -326,7 +327,8 @@ const WritingPad = () => {
<div className="p-4 flex items-center justify-between">
<button
onClick={() => navigate('/')}
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors"
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
aria-label="Go back to previous page"
Comment on lines +330 to +331

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect WritingPad.jsx around specified lines =="
sed -n '300,520p' src/components/WritingPad.jsx | nl -ba | sed -n '1,240p'

echo "== Inspect src/index.css around focus-visible outline =="
# find the rule occurrences
rg -n ":focus-visible" src/index.css src -S || true

# show around the first match lines (71-74 hinted)
sed -n '50,110p' src/index.css | nl -ba

Repository: RileyTheRiler/French-Game

Length of output: 182


🌐 Web query:

Tailwind CSS v4 focus-visible:outline-hidden utility outline-none only removes outline style documentation

💡 Result:

In Tailwind CSS v4, the outline-hidden and outline-none utilities behave differently regarding accessibility, specifically in forced colors mode [1][2]. The primary difference is: - outline-hidden: This utility sets outline-style: none but also preserves a visible outline when the browser's forced colors mode is active (e.g., Windows High Contrast Mode) [3][2]. It is designed as the recommended replacement for the legacy v3 outline-none utility to ensure focus visibility for accessibility [1][4]. - outline-none: In v4, this utility is a simple, literal implementation that sets outline-style: none and does not provide special handling for forced colors mode [1][4]. The v4 upgrade process encourages replacing all legacy instances of outline-none with outline-hidden to maintain the accessibility behavior that v3 users were implicitly receiving [1][2]. If you truly need to remove the outline entirely in all circumstances (including forced colors mode), you would use outline-none, though it is generally recommended to provide custom focus styling for accessibility when doing so [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect WritingPad.jsx around specified lines =="
sed -n '300,520p' src/components/WritingPad.jsx | nl -ba | sed -n '1,260p'

echo "== Inspect src/index.css around focus-visible outline =="
rg -n ":focus-visible" src/index.css src -S || true
sed -n '50,110p' src/index.css | nl -ba

Repository: RileyTheRiler/French-Game

Length of output: 182


🌐 Web query:

Tailwind CSS v4 focus-visible:outline-hidden utility outline-none only removes outline style documentation

💡 Result:

In Tailwind CSS v4, the outline-hidden and outline-none utilities serve distinct purposes regarding accessibility and browser behavior [1][2]. The outline-hidden utility is the recommended replacement for the behavior previously associated with outline-none in Tailwind CSS v3 [2]. It hides the browser default outline during normal interaction but preserves the focus ring in "forced colors" mode (such as Windows High Contrast Mode) [1][3]. This is done to ensure the element remains accessible to users who rely on high-contrast assistive technologies [4][3]. Conversely, the outline-none utility in v4 is a simple CSS utility that sets outline-style: none [4][2]. It does not provide the same accessibility safety net as outline-hidden for forced-colors mode [4][3]. When using focus-visible:outline-hidden, you are applying this accessibility-aware behavior specifically to the focus-visible state [1][5]. It is highly recommended to provide your own custom focus styling when using these utilities to maintain accessibility for keyboard users [1]. Summary of differences: - outline-hidden: Sets outline-style: none but preserves visibility in forced-colors mode (recommended for most use cases that previously used outline-none) [1][2][3]. - outline-none: Sets outline-style: none exclusively [4][2]. It is standard practice to use outline-hidden if you are suppressing the default browser outline to implement your own custom focus indicators [4][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect src/index.css around :focus-visible =="
# show a wider window for context (adjusted around the hinted 71-74)
sed -n '60,110p' src/index.css | awk '{printf "%4d\t%s\n", NR+60,$0}'

echo "== Find WritingPad.jsx buttons by aria-label/text =="
rg -n "Go back to previous page|audio|guide toggle|stroke width" src/components/WritingPad.jsx || true
rg -n "focus-visible:ring-|focus:outline-none|outline-hidden|outline-none" src/components/WritingPad.jsx || true

echo "== Inspect WritingPad.jsx around the back button =="
start=300; end=380
sed -n "${start},${end}p" src/components/WritingPad.jsx | awk '{printf "%4d\t%s\n", NR+('"$start"'-1),$0}'

echo "== Inspect WritingPad.jsx around audio button / guide toggle =="
start=380; end=470
sed -n "${start},${end}p" src/components/WritingPad.jsx | awk '{printf "%4d\t%s\n", NR+('"$start"'-1),$0}'

echo "== Inspect WritingPad.jsx around color picker / stroke width =="
start=440; end=520
sed -n "${start},${end}p" src/components/WritingPad.jsx | awk '{printf "%4d\t%s\n", NR+('"$start"'-1),$0}'

Repository: RileyTheRiler/French-Game

Length of output: 16924


Remove global :focus-visible outline for these ring-styled buttons

src/index.css applies a global :focus-visible { outline: 3px solid ... } (lines 72–75). WritingPad.jsx adds focus-visible:ring-* to the back/audio/guide toggle/color picker/stroke width buttons, but only uses focus:outline-none (not :focus-visible), so keyboard focus can show both the global outline and the Tailwind ring.

Add focus-visible:outline-hidden to those buttons (Tailwind v4’s recommended outline-suppression that preserves forced-colors behavior) or scope the global :focus-visible rule.

🎯 Proposed fix: add `focus-visible:outline-hidden` to the buttons
  <button
    onClick={() => navigate('/')}
-   className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
+   className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors focus:outline-none focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-indigo-400"
    aria-label="Go back to previous page"
  >

Apply the same focus-visible:outline-hidden addition to the audio button (~line 344), guide toggle (~line 419), color picker buttons (~lines 452–458), and stroke width buttons (~line 469).

Aria labels look good (including title + aria-label on the color picker).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
aria-label="Go back to previous page"
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors focus:outline-none focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-indigo-400"
aria-label="Go back to previous page"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/WritingPad.jsx` around lines 329 - 330, In WritingPad.jsx,
update the interactive buttons that currently use focus:outline-none and
focus-visible:ring-* so they don't show the global :focus-visible outline by
adding the Tailwind utility focus-visible:outline-hidden; specifically add
focus-visible:outline-hidden to the back button, the audio toggle button, the
guide toggle (the guide toggle component/element), all color picker buttons (the
color-picker button elements), and the stroke-width buttons so the ring styling
remains while suppressing the global outline.

>
<ArrowLeft className="w-5 h-5 text-slate-300" />
</button>
Expand All @@ -340,7 +342,8 @@ const WritingPad = () => {
<div className="flex items-center gap-2">
<button
onClick={playAudio}
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors"
className="p-2 rounded-lg bg-slate-800/50 hover:bg-slate-700/50 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
aria-label="Play audio"
>
<Volume2 className="w-5 h-5 text-slate-300" />
</button>
Expand Down Expand Up @@ -414,7 +417,8 @@ const WritingPad = () => {
drawGuide();
}
}}
className="absolute top-3 right-3 z-10 p-2 rounded-lg bg-slate-700/80 hover:bg-slate-600/80 transition-colors"
className="absolute top-3 right-3 z-10 p-2 rounded-lg bg-slate-700/80 hover:bg-slate-600/80 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400"
aria-label={showGuide ? "Hide guide" : "Show guide"}
>
{showGuide ? (
<Eye className="w-4 h-4 text-purple-400" />
Expand Down Expand Up @@ -446,12 +450,13 @@ const WritingPad = () => {
<button
key={color.value}
onClick={() => setStrokeColor(color.value)}
className={`w-8 h-8 rounded-full border-2 transition-transform ${strokeColor === color.value
className={`w-8 h-8 rounded-full border-2 transition-transform focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 ${strokeColor === color.value
? 'border-white scale-110'
: 'border-transparent hover:scale-105'
}`}
style={{ backgroundColor: color.value }}
title={color.name}
aria-label={`Select ${color.name} color`}
/>
))}
</div>
Expand All @@ -462,10 +467,11 @@ const WritingPad = () => {
<button
key={width}
onClick={() => setStrokeWidth(width)}
className={`flex items-center justify-center w-10 h-10 rounded-lg transition-colors ${strokeWidth === width
className={`flex items-center justify-center w-10 h-10 rounded-lg transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-400 ${strokeWidth === width
? 'bg-purple-500'
: 'bg-slate-800 hover:bg-slate-700'
}`}
aria-label={`Set stroke width to ${width} pixels`}
>
<div
className="bg-white rounded-full"
Expand Down
Loading