diff --git a/.changelog/next/changed-issue-4138.md b/.changelog/next/changed-issue-4138.md
new file mode 100644
index 0000000000..3cf6c03b36
--- /dev/null
+++ b/.changelog/next/changed-issue-4138.md
@@ -0,0 +1 @@
+- Shared ui/ProgressBar primitive replaces six hand-rolled progress meters; every meter now carries role=progressbar with aria-valuenow/min/max and an accessible name
diff --git a/client/src/components/meatspace/post/MemoryPractice.jsx b/client/src/components/meatspace/post/MemoryPractice.jsx
index 92422a9408..0b71c0577d 100644
--- a/client/src/components/meatspace/post/MemoryPractice.jsx
+++ b/client/src/components/meatspace/post/MemoryPractice.jsx
@@ -1,6 +1,7 @@
import { useState, useRef, useEffect, useMemo } from 'react';
import { ChevronLeft, Check, X, SkipForward, RotateCcw, Target, ChevronDown, Loader, ShieldCheck } from 'lucide-react';
import { submitMemoryPractice, getChunkMastery, getMemoryItem, attestMemoryMastery } from '../../../services/api';
+import ProgressBar from '../../ui/ProgressBar';
import PostCompletionActions from './PostCompletionActions';
import { startRetryableSave } from './completionSave';
@@ -386,7 +387,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
-
+
{/* Chunk info */}
@@ -480,7 +481,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
{currentIdx + 1} / {lines.length}
-
@@ -547,7 +548,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
{currentIdx + 1} / {lines.length - 1}
-
+
Current line:
@@ -629,7 +630,7 @@ function MemoryPracticeRunner({ item, mode, onSelectMode, onExitMode, onBack, on
{currentIdx + 1} / {lines.length}
-
+
{displayText}
@@ -904,14 +905,18 @@ function ChunkMasteryOverview({ item }) {
const accuracy = typeof stats?.masteredAt === 'string'
? 100
: stats?.attempts > 0 ? Math.round((stats.correct / stats.attempts) * 100) : 0;
- const barColor = accuracy >= 80 ? 'bg-port-success' : accuracy >= 40 ? 'bg-port-warning' : 'bg-gray-600';
+ const barTone = accuracy >= 80 ? 'success' : accuracy >= 40 ? 'warning' : 'muted';
return (
{chunk.label}
-
+
{accuracy}%
);
@@ -962,12 +967,17 @@ function SpeedRunLine({ line, index, onResult }) {
);
}
-function ProgressBar({ current, total }) {
- const pct = Math.round((current / total) * 100);
+// The name stays generic rather than "line 3 of 12": the spaced mode measures a
+// synthetic chunk×line position, so only the percentage is meaningful across all
+// four modes — and `aria-valuenow` already carries it.
+function PracticeProgress({ current, total }) {
return (
-
+
);
}
diff --git a/client/src/components/meatspace/post/PostLlmDrillRunner.jsx b/client/src/components/meatspace/post/PostLlmDrillRunner.jsx
index 52a259fbe8..55d98ea647 100644
--- a/client/src/components/meatspace/post/PostLlmDrillRunner.jsx
+++ b/client/src/components/meatspace/post/PostLlmDrillRunner.jsx
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef, useCallback } from 'react';
import { CheckCircle, XCircle } from 'lucide-react';
+import ProgressBar from '../../ui/ProgressBar';
import { scorePostLlmDrill } from '../../../services/api';
import { DRILL_LABELS, WORDPLAY_LLM_DRILL_TYPES } from './constants';
import { AILoadingIndicator, MissedExamplesDisplay, CompoundChainUI, BridgeWordUI, DoubleMeaningUI, IdiomTwistUI, scoreWordplayResponse } from './WordplayDrillUI';
@@ -188,9 +189,9 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
}
const timePct = timeLimitMs > 0 ? (timeLeft / timeLimitMs) * 100 : 0;
- let timerColor = 'bg-port-accent';
- if (timePct <= 10) timerColor = 'bg-port-error';
- else if (timePct <= 25) timerColor = 'bg-port-warning';
+ let timerTone = 'accent';
+ if (timePct <= 10) timerTone = 'error';
+ else if (timePct <= 25) timerTone = 'warning';
// Training mode: feedback overlay
if (isTraining && trainingFeedback) {
@@ -231,14 +232,7 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
>
Next
-
-
- Prompt {questionIndex + 1} of {totalPrompts}
-
-
-
+
);
}
@@ -257,9 +251,14 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr
{/* Timer bar (hidden in training mode) */}
{!isTraining && (
<>
-
+
{Math.ceil(timeLeft / 1000)}s remaining
>
)}
@@ -427,7 +426,7 @@ export default function PostLlmDrillRunner({ drill, timeLimitSec, drillIndex, dr