The selected companion everywhere, each with a small trait; next-APK notes
- state/companion.ts: one store for the chosen companion (also in localStorage so splash/login show it).
BrandMark (the orange «π» tile) now shows the companion's symbol (Σ Δ π ω) in its colour; screen-header
mascots, loaders and empty/error states default to it; the assistant is «از {name} بپرس» with its face,
colour and wording; the shop, lesson and quiz «ask» buttons name it; the active tab takes its tint.
- Traits, all client-side and light: Σ sigma keeps a running tally of correct answers in the quiz header;
Δ delta shows a flame «×N» chip and an extra buzz for answers in a row; π pi gives a short solving tip
after a wrong answer; ω omega throws gold confetti at the end of every island whatever the score.
Each companion's «correct» sound is pitched differently; confetti uses its colours. Characters screen
lists each trait.
- android/README.md: what MainActivity.kt has that the installed APK lacks (vibrate bridge, media gesture,
in-app gateway + paysInApp), what to check on a phone after building, signing owner, Bazaar policy note.
Checked in headless Chrome with Sigma, Delta and Omega selected (home, assistant, shop, characters, quiz).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { sounds } from '@/lib/sound';
|
||||
import { useCompanion } from '@/state/companion';
|
||||
import { onWhite } from '@/lib/color';
|
||||
import { Brain, ShoppingBag, Home, BookOpen, UserRound, type LucideIcon } from 'lucide-react';
|
||||
import type { TabName } from '@/nav/routes';
|
||||
|
||||
@@ -11,6 +13,8 @@ const ITEMS: { id: TabName; icon: LucideIcon; label: string }[] = [
|
||||
];
|
||||
|
||||
export function BottomNav({ active, onChange }: { active: TabName | null; onChange: (tab: TabName) => void }) {
|
||||
// تبِ روشن به رنگِ همراهِ انتخابی (تیرهشده تا روی سفید خوانا بماند؛ امگای طلایی بیآن کمرنگ بود).
|
||||
const tint = onWhite(useCompanion().color);
|
||||
return (
|
||||
<nav className="flex flex-shrink-0 items-center justify-around border-t border-line bg-white px-2 pb-safe pt-1.5">
|
||||
{ITEMS.map(({ id, icon: Icon, label }) => {
|
||||
@@ -24,9 +28,10 @@ export function BottomNav({ active, onChange }: { active: TabName | null; onChan
|
||||
onChange(id);
|
||||
}}
|
||||
aria-current={on ? 'page' : undefined}
|
||||
className={`flex min-w-14 flex-col items-center gap-0.5 rounded-xl px-2 pb-1.5 transition-[color,transform] active:scale-90 ${on ? 'text-coral' : 'text-muted'}`}
|
||||
className={`flex min-w-14 flex-col items-center gap-0.5 rounded-xl px-2 pb-1.5 transition-[color,transform] active:scale-90 ${on ? '' : 'text-muted'}`}
|
||||
style={on ? { color: tint } : undefined}
|
||||
>
|
||||
<span className={`rounded-xl p-1.5 transition-colors ${on ? 'bg-[#fde9e5]' : ''}`}>
|
||||
<span className="rounded-xl p-1.5 transition-colors" style={on ? { backgroundColor: `${tint}1F` } : undefined}>
|
||||
<Icon size={20} strokeWidth={on ? 2.5 : 1.9} />
|
||||
</span>
|
||||
<span className="text-[11px] font-bold">{label}</span>
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import { useId } from 'react';
|
||||
import { useCompanion } from '@/state/companion';
|
||||
|
||||
/**
|
||||
* نشانِ تدریسینو: یک کاشیِ گرد با «π» و جرقهی کوچکِ چشمکزن، کنارِ نامِ تدریسینو.
|
||||
* خودِ SVG است (بیفایلِ تصویر)، پس در هر اندازه تیز است و چیزی دانلود نمیشود.
|
||||
* نشانِ تدریسینو: یک کاشیِ گرد با نمادِ همراهِ انتخابیِ کاربر (Σ، Δ، π، ω) به رنگِ همان همراه، و جرقهی کوچکِ
|
||||
* چشمکزن، کنارِ نامِ تدریسینو. پیشتر همیشه «π»ِ نارنجی بود و با همراهِ کاربر جور نبود (خواستِ مالک، ۱۴۰۵-۰۷).
|
||||
* خودِ SVG است (بیفایلِ تصویر).
|
||||
*/
|
||||
export function BrandMark({ size = 28, tone = 'light', withName = true }: { size?: number; tone?: 'light' | 'dark'; withName?: boolean }) {
|
||||
const ink = tone === 'light' ? '#fff' : '#3C3F8F';
|
||||
const c = useCompanion();
|
||||
const gid = useId();
|
||||
return (
|
||||
<span className="inline-flex items-center gap-1.5" aria-label="تدریسینو">
|
||||
<svg width={size} height={size} viewBox="0 0 32 32" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="bm-g" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stopColor="#FF9A5C" />
|
||||
<stop offset="1" stopColor="#ED6956" />
|
||||
<linearGradient id={gid} x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stopColor={c.light} />
|
||||
<stop offset="1" stopColor={c.color} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="1" y="1" width="30" height="30" rx="9" fill="url(#bm-g)" stroke={tone === 'light' ? 'rgba(255,255,255,.55)' : 'none'} />
|
||||
<path d="M9 11.5h14M12.5 11.5v10M19 11.5v8.2c0 1.2.7 1.8 1.8 1.8" stroke="#fff" strokeWidth="2.6" strokeLinecap="round" fill="none" />
|
||||
<rect x="1" y="1" width="30" height="30" rx="9" fill={`url(#${gid})`} stroke={tone === 'light' ? 'rgba(255,255,255,.55)' : 'none'} />
|
||||
<text x="16" y="17" textAnchor="middle" dominantBaseline="central" fill="#fff" fontSize="19" fontWeight="900" fontFamily="Vazirmatn, system-ui, sans-serif">
|
||||
{c.symbol}
|
||||
</text>
|
||||
<path className="spark" d="M25 4.5l.9 1.9 1.9.9-1.9.9-.9 1.9-.9-1.9-1.9-.9 1.9-.9z" fill="#FFD23F" />
|
||||
</svg>
|
||||
{withName && <span className="font-black tracking-tight" style={{ color: ink, fontSize: size * 0.62 }}>تدریسینو</span>}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useNav } from '@/nav/Navigator';
|
||||
import { SeyarMascot } from '@/mascots/SeyarMascot';
|
||||
import { PrimaryButton, Screen } from '@/components/ui';
|
||||
import { toFa } from '@/lib/format';
|
||||
import { useCompanion } from '@/state/companion';
|
||||
|
||||
/**
|
||||
* «قلبهات تموم شد»: با قلبِ صفر سرور هیچ جوابی نمیپذیرد (۴۰۳ با code=no_hearts) و پایانِ درس یا
|
||||
@@ -56,6 +57,7 @@ export function NoHeartsScreen({
|
||||
/** قلب برگشت و کاربر «ادامه بده» زد. */
|
||||
onRefilled: () => void;
|
||||
}) {
|
||||
const companionName = useCompanion().name;
|
||||
const nav = useNav();
|
||||
const { companion } = useAppData();
|
||||
const now = useNow();
|
||||
@@ -174,7 +176,7 @@ export function NoHeartsScreen({
|
||||
<Brain size={20} />
|
||||
</span>
|
||||
<span className="flex-1">
|
||||
<span className="block text-sm font-black text-ink">تا اون موقع از «پی» بپرس</span>
|
||||
<span className="block text-sm font-black text-ink">تا اون موقع از «{companionName}» بپرس</span>
|
||||
<span className="block text-xs leading-6 text-muted">جایی که گیر کردی رو با هم مرور کنیم؛ قلب خرج نمیکنه.</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { SeyarChar, SeyarMood } from '@/mascots/seyar';
|
||||
import { friendlyError } from '@/lib/errors';
|
||||
import { useNav } from '@/nav/Navigator';
|
||||
import { onWhite, shade } from '@/lib/color';
|
||||
import { useCompanion } from '@/state/companion';
|
||||
|
||||
/* ─── سربرگِ رنگیِ صفحه ──────────────────────────────────────────────────── */
|
||||
|
||||
@@ -22,12 +23,14 @@ export function ScreenHeader({
|
||||
color: string;
|
||||
/** دکمهی بازگشت؛ true یعنی صفحهی قبل در پشته. */
|
||||
back?: boolean | (() => void);
|
||||
mascot?: { char: SeyarChar; mood?: SeyarMood; size?: number };
|
||||
/** بی `char` همراهِ انتخابیِ کاربر است؛ همهی صفحهها همین را میخواهند. */
|
||||
mascot?: { char?: SeyarChar; mood?: SeyarMood; size?: number };
|
||||
/** رنگ را همانطور که هست بگذار (بیتیرهکردن برای کنتراست)؛ انتخابِ مالک برای چند صفحه. */
|
||||
exact?: boolean;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
const nav = useNav();
|
||||
const companion = useCompanion().char;
|
||||
const onBack = typeof back === 'function' ? back : nav.back;
|
||||
const bg = exact ? color : onWhite(color);
|
||||
return (
|
||||
@@ -52,7 +55,7 @@ export function ScreenHeader({
|
||||
<h1 className="text-xl font-black leading-snug">{title}</h1>
|
||||
{subtitle && <div className="mt-1 text-xs text-white/75">{subtitle}</div>}
|
||||
</div>
|
||||
{mascot && <SeyarMascot char={mascot.char} mood={mascot.mood ?? 'happy'} size={mascot.size ?? 64} />}
|
||||
{mascot && <SeyarMascot char={mascot.char ?? companion} mood={mascot.mood ?? 'happy'} size={mascot.size ?? 64} />}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
@@ -128,10 +131,11 @@ export function Spinner({ size = 20, className = '' }: { size?: number; classNam
|
||||
|
||||
/* ─── حالتهای بارگذاری / خطا / خالی ───────────────────────────────────────── */
|
||||
|
||||
function StateBox({ char, mood, text, children }: { char: SeyarChar; mood: SeyarMood; text: ReactNode; children?: ReactNode }) {
|
||||
function StateBox({ char, mood, text, children }: { char?: SeyarChar; mood: SeyarMood; text: ReactNode; children?: ReactNode }) {
|
||||
const companion = useCompanion().char;
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center gap-3 px-8 py-16 text-center">
|
||||
<SeyarMascot char={char} mood={mood} size={72} />
|
||||
<SeyarMascot char={char ?? companion} mood={mood} size={72} />
|
||||
<p className="text-sm leading-7 text-muted">{text}</p>
|
||||
{children}
|
||||
</div>
|
||||
@@ -143,7 +147,7 @@ function StateBox({ char, mood, text, children }: { char: SeyarChar; mood: Seyar
|
||||
* همان چند فریم، پرشِ ریزِ «لودر ← محتوا» میساخت؛ حالا تا آن لحظه جای خالی میماند. اگر پاسخ
|
||||
* کند باشد کاربر باز هم لودر را میبیند. در مرورگر با شبکهی کند آزموده نشده.
|
||||
*/
|
||||
export function LoadingState({ text = 'یه لحظه صبر کن…', char = 'sigma' as SeyarChar, delayMs = 300 }) {
|
||||
export function LoadingState({ text = 'یه لحظه صبر کن…', char, delayMs = 300 }: { text?: string; char?: SeyarChar; delayMs?: number }) {
|
||||
const [show, setShow] = useState(delayMs <= 0);
|
||||
useEffect(() => {
|
||||
if (delayMs <= 0) return;
|
||||
@@ -155,7 +159,7 @@ export function LoadingState({ text = 'یه لحظه صبر کن…', char = 'si
|
||||
|
||||
export function ErrorState({ error, onRetry, text }: { error?: unknown; onRetry?: () => void; text?: string }) {
|
||||
return (
|
||||
<StateBox char="pi" mood="sad" text={text ?? friendlyError(error)}>
|
||||
<StateBox mood="sad" text={text ?? friendlyError(error)}>
|
||||
{onRetry && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -169,7 +173,7 @@ export function ErrorState({ error, onRetry, text }: { error?: unknown; onRetry?
|
||||
);
|
||||
}
|
||||
|
||||
export function EmptyState({ text, char = 'pi' as SeyarChar }: { text: string; char?: SeyarChar }) {
|
||||
export function EmptyState({ text, char }: { text: string; char?: SeyarChar }) {
|
||||
return <StateBox char={char} mood="idle" text={text} />;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user