From 5b211c27869da039deee3cd6cc25fbbdf23f2d93 Mon Sep 17 00:00:00 2001 From: AliReza Date: Wed, 7 Oct 2026 16:26:13 +0330 Subject: [PATCH 1/3] Lesson screen: render fixed lesson figures under intro, example and steps Co-Authored-By: Claude Sonnet 5.5 --- src/api/types.ts | 9 +++++++++ src/screens/LessonScreen.tsx | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/api/types.ts b/src/api/types.ts index 682d4e4..5580e08 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -106,10 +106,19 @@ export interface LessonIntro { kind: 'quiz' | 'video'; example: string; steps: string[]; + /** شکل‌های ثابتِ درس (یک بار ساخته و تأییدشده؛ هیچ تولیدِ زنده‌ای نیست). */ + figures: LessonFigure[]; videoUrl: string; durationMinutes: number | null; } +export interface LessonFigure { + where: 'intro' | 'example' | 'step'; + /** فقط برای where='step'، از صفر. */ + index?: number; + diagram: DiagramSpec; +} + export interface QuestionOption { id: string; value: string; diff --git a/src/screens/LessonScreen.tsx b/src/screens/LessonScreen.tsx index 86e72a0..445bc97 100644 --- a/src/screens/LessonScreen.tsx +++ b/src/screens/LessonScreen.tsx @@ -9,6 +9,7 @@ import { moodFromServer } from '@/mascots/seyar'; import { ErrorState, LoadingState, PrimaryButton, Screen, ScreenBody, ScreenHeader, Sep } from '@/components/ui'; import { toFa } from '@/lib/format'; import { MathText } from '@/lib/MathText'; +import { Diagram } from '@/diagram'; /** معرفیِ درس، پیش از تمرین. */ export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?: string }) { @@ -18,6 +19,10 @@ export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?: const ref = findNode(home, nodeId) ?? findNodeOfLesson(home, lessonId); const color = ref ? unitColor(ref.unitIndex) : '#ED6956'; const data = intro.data; + const figs = (where: 'intro' | 'example' | 'step', index?: number) => + (data?.figures ?? []) + .filter((f) => f.where === where && (where !== 'step' || f.index === index)) + .map((f, k) => ); return ( @@ -47,6 +52,7 @@ export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?:

+ {figs('intro')}
@@ -58,6 +64,7 @@ export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?:

مثال

+ {figs('example')}
)} @@ -70,7 +77,7 @@ export function LessonScreen({ lessonId, nodeId }: { lessonId: number; nodeId?: {toFa(i + 1)} - + {figs('step', i)} ))} From ec1bfc721db249ce87fef8afcbaa3698245a90dc Mon Sep 17 00:00:00 2001 From: AliReza Date: Wed, 7 Oct 2026 17:02:18 +0330 Subject: [PATCH 2/3] Page switch: show the loading box only after 300ms, not for the 1-2 frames lazy() always takes Co-Authored-By: Claude Sonnet 5.5 --- src/app/MainApp.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 03a5aed..6765620 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -67,6 +67,21 @@ export function MainApp({ profile, grades, payment, onSignedOut }: Props) { ); } +/** + * صفحه‌ها از قبل گرفته شده‌اند، ولی `lazy` حتی برای ماژولِ آماده هم یک رندرِ تهی می‌گیرد؛ نشان دادنِ + * «یه لحظه صبر کن» در همان یک-دو فریم همان پرشِ ریزِ بینِ صفحه‌ها بود. لودر فقط اگر بارگیری + * واقعاً طول کشید (شبکه‌ی کند، پیش از تمام شدنِ preload) پس از ۳۰۰ms ظاهر می‌شود. + * با مرورگر دیده نشده؛ فقط از روی کد. + */ +function DelayedLoading() { + const [show, setShow] = useState(false); + useEffect(() => { + const t = setTimeout(() => setShow(true), 300); + return () => clearTimeout(t); + }, []); + return show ? : null; +} + function Screens({ onSignedOut }: Pick) { const nav = useNav(); const { route } = nav; @@ -84,7 +99,7 @@ function Screens({ onSignedOut }: Pick) { exit={{ opacity: 0, x: 18 }} transition={{ duration: 0.18 }} > - }> + }> From b11016c6ea96014f8d702b9417ba52d4daf3878b Mon Sep 17 00:00:00 2001 From: AliReza Date: Wed, 7 Oct 2026 17:05:52 +0330 Subject: [PATCH 3/3] LoadingState appears only after 300ms: fast API answers no longer flash the loader (packs screen and every other data screen) Co-Authored-By: Claude Sonnet 5.5 --- src/app/MainApp.tsx | 17 +---------------- src/components/ui.tsx | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/src/app/MainApp.tsx b/src/app/MainApp.tsx index 6765620..03a5aed 100644 --- a/src/app/MainApp.tsx +++ b/src/app/MainApp.tsx @@ -67,21 +67,6 @@ export function MainApp({ profile, grades, payment, onSignedOut }: Props) { ); } -/** - * صفحه‌ها از قبل گرفته شده‌اند، ولی `lazy` حتی برای ماژولِ آماده هم یک رندرِ تهی می‌گیرد؛ نشان دادنِ - * «یه لحظه صبر کن» در همان یک-دو فریم همان پرشِ ریزِ بینِ صفحه‌ها بود. لودر فقط اگر بارگیری - * واقعاً طول کشید (شبکه‌ی کند، پیش از تمام شدنِ preload) پس از ۳۰۰ms ظاهر می‌شود. - * با مرورگر دیده نشده؛ فقط از روی کد. - */ -function DelayedLoading() { - const [show, setShow] = useState(false); - useEffect(() => { - const t = setTimeout(() => setShow(true), 300); - return () => clearTimeout(t); - }, []); - return show ? : null; -} - function Screens({ onSignedOut }: Pick) { const nav = useNav(); const { route } = nav; @@ -99,7 +84,7 @@ function Screens({ onSignedOut }: Pick) { exit={{ opacity: 0, x: 18 }} transition={{ duration: 0.18 }} > - }> + }> diff --git a/src/components/ui.tsx b/src/components/ui.tsx index eabab70..aeabac9 100644 --- a/src/components/ui.tsx +++ b/src/components/ui.tsx @@ -1,4 +1,4 @@ -import type { ButtonHTMLAttributes, CSSProperties, ReactNode } from 'react'; +import { useEffect, useState, type ButtonHTMLAttributes, type CSSProperties, type ReactNode } from 'react'; import { ChevronRight, RotateCw } from 'lucide-react'; import { SeyarMascot } from '@/mascots/SeyarMascot'; import type { SeyarChar, SeyarMood } from '@/mascots/seyar'; @@ -130,8 +130,19 @@ function StateBox({ char, mood, text, children }: { char: SeyarChar; mood: Seyar ); } -export function LoadingState({ text = 'یه لحظه صبر کن…', char = 'sigma' as SeyarChar }) { - return ; +/** + * لودر فقط بعد از `delayMs` دیده می‌شود. پاسخِ API معمولاً زیرِ ۳۰۰ms می‌رسد و نشان دادنِ لودر برای + * همان چند فریم، پرشِ ریزِ «لودر ← محتوا» می‌ساخت؛ حالا تا آن لحظه جای خالی می‌ماند. اگر پاسخ + * کند باشد کاربر باز هم لودر را می‌بیند. در مرورگر با شبکه‌ی کند آزموده نشده. + */ +export function LoadingState({ text = 'یه لحظه صبر کن…', char = 'sigma' as SeyarChar, delayMs = 300 }) { + const [show, setShow] = useState(delayMs <= 0); + useEffect(() => { + if (delayMs <= 0) return; + const t = setTimeout(() => setShow(true), delayMs); + return () => clearTimeout(t); + }, [delayMs]); + return show ? : null; } export function ErrorState({ error, onRetry, text }: { error?: unknown; onRetry?: () => void; text?: string }) {