Measured by rendering every question, option and several tutor answers in
Chrome and comparing the on-screen x of each number (2026-09-29):
- A list inside a sentence ('از −۳ به −۲، −۱، ۰ میرسیم', 'الگویِ ۲، ۵، ۸')
was locked as one left-to-right run, so all 482 read backwards. '،' and
'؛' outside brackets now split the run; sets and coordinates in brackets
stay one run. Locked runs are isolates (isolate-override), otherwise the
comma between two of them was still pulled left-to-right.
- Without MathText 704 of 931 minus signs and 602 of 983 expressions were
misplaced; lesson and island titles, the lesson intro, unit blurb,
notification bodies and ticket messages now go through it.
- The chat box takes its direction from each line's first letter, so a
line of pure math stays left-to-right while typing. A line that starts
in Persian can still show its math reordered inside the box; a textarea
cannot isolate part of its text. The sent message renders correctly.
Four expressions with Persian words inside them ('f(x) = (ریشه دوم x) + ۵۰')
remain ambiguous.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.2 KiB
TypeScript
78 lines
3.2 KiB
TypeScript
import { useState } from 'react';
|
|
import { Sparkles } from 'lucide-react';
|
|
import type { HomeNode } from '@/api';
|
|
import { useAppData } from '@/state/AppData';
|
|
import { findUnit, unitColor, unitProgress } from '@/state/curriculum';
|
|
import { useNav } from '@/nav/Navigator';
|
|
import { IslandPath } from '@/components/IslandPath';
|
|
import { IslandSheet } from '@/components/IslandSheet';
|
|
import { EmptyState, ErrorState, LoadingState, ProgressBar, Screen, ScreenHeader } from '@/components/ui';
|
|
import { toFa } from '@/lib/format';
|
|
import { MathText } from '@/lib/MathText';
|
|
|
|
/** نقشهی جزیرههای یک فصل، مثلِ مسیرِ دولینگو. */
|
|
export function UnitScreen({ unitId }: { unitId: string }) {
|
|
const { home, homeError, refreshHome, companion } = useAppData();
|
|
const nav = useNav();
|
|
const [openNode, setOpenNode] = useState<HomeNode | null>(null);
|
|
const found = findUnit(home, unitId);
|
|
|
|
if (!found) {
|
|
return (
|
|
<Screen>
|
|
<ScreenHeader title="…" color="#91ADC6" back />
|
|
{homeError ? <ErrorState error={homeError} onRetry={() => void refreshHome()} /> : home ? <EmptyState text="این فصل پیدا نشد" /> : <LoadingState />}
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
const { unit, unitIndex } = found;
|
|
const color = unitColor(unitIndex);
|
|
const progress = unitProgress(unit);
|
|
const current = unit.nodes.find((n) => n.status !== 'completed' && n.status !== 'locked') ?? null;
|
|
|
|
return (
|
|
<Screen className="bg-page">
|
|
<ScreenHeader
|
|
title={unit.title}
|
|
subtitle={unit.blurb ? <MathText text={unit.blurb} /> : `${toFa(progress.done)} از ${toFa(progress.total)} جزیره`}
|
|
color={color}
|
|
back
|
|
mascot={{ char: companion, mood: progress.percent >= 50 ? 'happy' : 'idle', size: 72 }}
|
|
>
|
|
<div className="mt-4">
|
|
<div className="mb-1.5 flex justify-between text-[10px] text-white/75">
|
|
<span>پیشرفتِ تو</span>
|
|
<span>{toFa(progress.percent)}٪</span>
|
|
</div>
|
|
<ProgressBar percent={progress.percent} color="#fff" track="rgba(0,0,0,.1)" height={10} />
|
|
</div>
|
|
</ScreenHeader>
|
|
|
|
<div className="flex-1 overflow-y-auto pb-6 pt-10">
|
|
{unit.nodes.length === 0 ? (
|
|
<EmptyState text="این فصل هنوز جزیرهای نداره" />
|
|
) : (
|
|
<>
|
|
<IslandPath
|
|
nodes={unit.nodes}
|
|
color={color}
|
|
currentId={current?.id ?? null}
|
|
onOpen={setOpenNode}
|
|
onStart={(node) => nav.push({ name: 'quiz', nodeId: node.id })}
|
|
/>
|
|
<div className="mt-6 flex flex-col items-center">
|
|
<span className="mb-1 text-[11px] font-bold text-muted">{progress.done === progress.total ? 'این فصل تموم شد!' : 'ادامه بده!'}</span>
|
|
<span className="flex h-11 w-11 items-center justify-center rounded-2xl" style={{ backgroundColor: `${color}18` }}>
|
|
<Sparkles size={20} style={{ color }} />
|
|
</span>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<IslandSheet node={openNode} color={color} onClose={() => setOpenNode(null)} />
|
|
</Screen>
|
|
);
|
|
}
|