Subject bar under the header in Quizzes and Lessons, synced between tabs; bidi fix for Latin math in RTL text

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 05:35:30 +03:30
parent 56dcc93202
commit f569ece55b
14 changed files with 217 additions and 21 deletions

View File

@@ -1,5 +1,5 @@
import {
BookOpen, Brain, Compass, Grid3x3, LineChart, Play, Repeat, Sigma, Sparkles, TrendingUp,
BarChart3, BookOpen, Brain, Compass, Grid3x3, LineChart, Network, Play, Repeat, Sigma, Sparkles, TrendingUp,
type LucideIcon,
} from 'lucide-react';
@@ -8,7 +8,7 @@ import {
* مجموعه‌ی کوچک در باندل است تا کلِ کتابخانه‌ی آیکن بار نشود.
*/
const ICONS: Record<string, LucideIcon> = {
Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat, BookOpen, Sparkles, Brain, Play,
Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat, BookOpen, Sparkles, Brain, Play, BarChart3, Network,
};
const FALLBACK: LucideIcon[] = [Grid3x3, TrendingUp, LineChart, Sigma, Compass, Repeat, BookOpen, Sparkles, Brain];

View File

@@ -0,0 +1,55 @@
import { useEffect, useRef } from 'react';
import { useAppData } from '@/state/AppData';
import { onWhite } from '@/lib/color';
import { iconFor } from '@/components/NodeIcon';
/**
* نوارِ نازکِ کتاب‌ها (ریاضی / هندسه / حسابان / آمار و احتمال / گسسته) زیرِ سربرگِ «آزمون‌ها» و «درس‌ها».
* انتخاب در AppData نگه داشته می‌شود و هر دو تب از همان `home` می‌خوانند، پس جابه‌جایی در یکی در دیگری
* هم دیده می‌شود. فقط وقتی پایه بیش از یک کتاب دارد نشان داده می‌شود؛ پایه‌های دیگر هیچ تغییری نمی‌بینند.
*
* دکمه‌ها عرضِ نوار را پر می‌کنند (پایه‌ی ۱۰ دو کتاب، ۱۱ و ۱۲ سه کتاب دارند)؛ اگر جا کم بود اسکرولِ افقی می‌شود.
*
* بیرونِ ناحیه‌ی اسکرول است تا با پایین‌رفتنِ نقشه گم نشود و با سربرگِ چسبانِ فصل‌ها هم‌پوشانی نکند.
*/
export function SubjectBar() {
const { home, subjectKey, selectSubject } = useAppData();
const subjects = home?.subjects ?? [];
const activeRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
// کتابِ فعال در دید بماند وقتی پنج دکمه از عرضِ گوشی بیشتر است.
activeRef.current?.scrollIntoView({ inline: 'center', block: 'nearest' });
}, [subjectKey]);
if (subjects.length < 2) return null;
return (
<nav aria-label="کتاب‌ها" className="flex-shrink-0 border-b border-line bg-white">
<div role="tablist" className="no-scrollbar flex gap-2 overflow-x-auto px-4 py-2">
{subjects.map((subject, index) => {
const active = subject.key === subjectKey;
const Icon = iconFor(subject.icon, index);
const color = onWhite(subject.color || '#4C5FD5');
return (
<button
key={subject.key}
ref={active ? activeRef : undefined}
type="button"
role="tab"
aria-selected={active}
onClick={() => !active && selectSubject(subject.key)}
className={`flex h-10 min-w-fit flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border px-3.5 text-[13px] font-black transition active:scale-95 ${
active ? 'border-transparent text-white shadow-sm' : 'border-line bg-white text-body'
}`}
style={active ? { backgroundColor: color, boxShadow: `0 3px 0 ${color}66` } : undefined}
>
<Icon size={16} strokeWidth={2.4} style={active ? undefined : { color }} />
{subject.title}
</button>
);
})}
</div>
</nav>
);
}