1 Commits

Author SHA1 Message Date
AnRil
ad000c722e fix(meals): плавная анимация переключателя в обе стороны
Список «Питания» разбивался на два <Card> (активные/выключенные). При
переключении строка переезжала между списками → её Switch размонтировался и
монтировался заново, и ползунок анимировался только при включении (mount
x:0→20), а при выключении — нет.

Теперь единый keyed-список (включённые сверху): Switch остаётся смонтированным,
ползунок плавно ездит в обе стороны, а строка «переезжает» в свою группу через
framer layout-анимацию. Иконке добавлен transition-colors. Глобальный Switch не
трогал — Упражнения/Дашборд не затронуты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 00:13:07 +07:00
2 changed files with 37 additions and 41 deletions

View File

@@ -100,6 +100,7 @@ export const ru: Dict = {
'meals.kicker': 'Режим питания', 'meals.kicker': 'Режим питания',
'meals.title': 'Питание', 'meals.title': 'Питание',
'meals.presets': 'Быстрое добавление', 'meals.presets': 'Быстрое добавление',
'meals.schedule': 'Расписание',
'meals.section.active': 'Активные · {n}', 'meals.section.active': 'Активные · {n}',
'meals.section.disabled': 'Выключенные · {n}', 'meals.section.disabled': 'Выключенные · {n}',
'meals.empty': 'Пока нет приёмов пищи — добавь первый или выбери пресет', 'meals.empty': 'Пока нет приёмов пищи — добавь первый или выбери пресет',
@@ -460,6 +461,7 @@ export const en: Dict = {
'meals.kicker': 'Eating schedule', 'meals.kicker': 'Eating schedule',
'meals.title': 'Meals', 'meals.title': 'Meals',
'meals.presets': 'Quick add', 'meals.presets': 'Quick add',
'meals.schedule': 'Schedule',
'meals.section.active': 'Active · {n}', 'meals.section.active': 'Active · {n}',
'meals.section.disabled': 'Disabled · {n}', 'meals.section.disabled': 'Disabled · {n}',
'meals.empty': 'No meals yet — add one or pick a preset', 'meals.empty': 'No meals yet — add one or pick a preset',

View File

@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import { Plus, ChevronRight, UtensilsCrossed } from 'lucide-react' import { Plus, ChevronRight, UtensilsCrossed } from 'lucide-react'
import { AnimatePresence, motion } from 'framer-motion'
import { useAppStore } from '../store/appStore' import { useAppStore } from '../store/appStore'
import { MealEditor, type MealDraft } from '../components/MealEditor' import { MealEditor, type MealDraft } from '../components/MealEditor'
import { Button } from '../components/ui/Button' import { Button } from '../components/ui/Button'
@@ -27,8 +28,15 @@ export default function Meals(): JSX.Element {
const [editing, setEditing] = useState<Meal | null>(null) const [editing, setEditing] = useState<Meal | null>(null)
const { t } = useT() const { t } = useT()
const enabled = meals.filter((m) => m.enabled) // Единый список (включённые сверху). Важно: НЕ разбиваем на два <Card>, иначе
const disabled = meals.filter((m) => !m.enabled) // при переключении строка переезжает между списками → её Switch
// размонтируется/монтируется заново, и анимация ползунка есть только при
// включении (mount с x:0→20), а при выключении нет. В одном keyed-списке
// компонент остаётся смонтированным → ползунок плавно ездит в обе стороны,
// а строка «переезжает» в свою группу через layout-анимацию.
const ordered = [...meals].sort((a, b) =>
a.enabled === b.enabled ? 0 : a.enabled ? -1 : 1
)
async function addPreset( async function addPreset(
preset: (typeof MEAL_PRESETS)[number] preset: (typeof MEAL_PRESETS)[number]
@@ -82,46 +90,32 @@ export default function Meals(): JSX.Element {
))} ))}
</div> </div>
{enabled.length > 0 && ( {meals.length > 0 && (
<> <>
<SectionHeader <SectionHeader title={t('meals.schedule')} />
title={t('meals.section.active', { n: enabled.length })}
/>
<Card className="mb-6">
{enabled.map((m, i) => (
<MealRow
key={m.id}
meal={m}
last={i === enabled.length - 1}
meta={`${m.time} · ${daysLabel(m.days, t)}`}
onEdit={() => {
setEditing(m)
setEditorOpen(true)
}}
/>
))}
</Card>
</>
)}
{disabled.length > 0 && (
<>
<SectionHeader
title={t('meals.section.disabled', { n: disabled.length })}
/>
<Card> <Card>
{disabled.map((m, i) => ( <AnimatePresence initial={false}>
<MealRow {ordered.map((m, i) => (
<motion.div
key={m.id} key={m.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.18, ease: 'easeOut' }}
>
<MealRow
meal={m} meal={m}
last={i === disabled.length - 1} last={i === ordered.length - 1}
meta={`${m.time} · ${daysLabel(m.days, t)}`} meta={`${m.time} · ${daysLabel(m.days, t)}`}
onEdit={() => { onEdit={() => {
setEditing(m) setEditing(m)
setEditorOpen(true) setEditorOpen(true)
}} }}
/> />
</motion.div>
))} ))}
</AnimatePresence>
</Card> </Card>
</> </>
)} )}
@@ -169,7 +163,7 @@ function MealRow({
<Row last={last}> <Row last={last}>
<div <div
className={[ className={[
'w-9 h-9 rounded-lg grid place-items-center shrink-0', 'w-9 h-9 rounded-lg grid place-items-center shrink-0 transition-colors duration-200',
meal.enabled ? 'bg-accent text-white' : 'bg-text/15 text-text/45' meal.enabled ? 'bg-accent text-white' : 'bg-text/15 text-text/45'
].join(' ')} ].join(' ')}
> >