feat(animation): 引入 framer-motion 实现动画效果

- 在多个组件中集成 framer-motion,增强用户界面交互体验
- 更新页面过渡效果,提升页面切换流畅度
- 优化数据表格和日历单元格的动画效果

这些更改为应用程序提供了更生动的用户体验。
This commit is contained in:
2026-05-13 20:18:34 +08:00
parent 047b7c8029
commit d1b1f7fd26
24 changed files with 871 additions and 287 deletions
+6 -1
View File
@@ -4,7 +4,12 @@ import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
AnimatedDialog as Dialog,
AnimatedDialogContent as DialogContent,
AnimatedDialogHeader as DialogHeader,
AnimatedDialogTitle as DialogTitle,
} from '@/components/animated'
const EXPENSE_CATEGORIES = ['房租', '物资', '其他']
const CUSTOM_HANDLER = '__custom__'
+161 -123
View File
@@ -1,12 +1,16 @@
'use client'
import { useState, useEffect } from 'react'
import { PageTransition } from '@/components/page-transition'
import { NavHeader } from '@/components/nav-header'
import { motion } from 'framer-motion'
import { AnimatedCard, AnimatedCardContent, AnimatedNumber } from '@/components/animated'
import { staggerContainer, staggerItem } from '@/lib/motion'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { DataTable } from '@/components/data-table'
import { DeletePopover } from '@/components/delete-popover'
import { CardStatsSkeleton, TableSkeleton } from '@/components/loading-skeleton'
import { ExpenseDialog } from './expense-dialog'
import type { ColumnDef } from '@tanstack/react-table'
import { Plus, Settings2, Edit2 } from 'lucide-react'
@@ -30,6 +34,7 @@ export default function FundPoolPage() {
maxPoolAmount: number
monthlyPercent: number
} | null>(null)
const [loading, setLoading] = useState(true)
const [config, setConfig] = useState<{ monthlyPercent: number; maxPoolAmount: number }>({
monthlyPercent: 0,
maxPoolAmount: 0,
@@ -41,17 +46,17 @@ export default function FundPoolPage() {
const [allocMessage, setAllocMessage] = useState('')
useEffect(() => {
fetch('/api/fund-pool/balance')
.then((r) => r.json())
.then(setBalance)
.catch(() => {})
fetch('/api/fund-pool/config')
.then((r) => r.json())
.then(setConfig)
.catch(() => {})
fetch('/api/fund-pool/expenses')
.then((r) => r.json())
.then(setExpenses)
Promise.all([
fetch('/api/fund-pool/balance').then((r) => r.json()),
fetch('/api/fund-pool/config').then((r) => r.json()),
fetch('/api/fund-pool/expenses').then((r) => r.json()),
])
.then(([b, c, e]) => {
setBalance(b)
setConfig(c)
setExpenses(e)
})
.finally(() => setLoading(false))
.catch(() => {})
}, [refreshKey])
@@ -137,122 +142,155 @@ export default function FundPoolPage() {
return (
<div className="h-full flex flex-col">
<NavHeader />
<div className="flex-1 flex flex-col p-6 gap-4 overflow-hidden">
<div className="flex items-center justify-between shrink-0">
<h1 className="text-xl font-bold"></h1>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={handleAllocate}>
</Button>
<Button size="sm" variant="outline" onClick={() => setConfigOpen(true)}>
<Settings2 className="h-4 w-4 mr-1" />
</Button>
<PageTransition className="flex-1">
<div className="flex flex-col p-6 gap-4 overflow-hidden h-full">
<div className="flex items-center justify-between shrink-0">
<h1 className="text-xl font-bold"></h1>
<div className="flex gap-2">
<Button size="sm" variant="outline" onClick={handleAllocate}>
</Button>
<Button size="sm" variant="outline" onClick={() => setConfigOpen(true)}>
<Settings2 className="h-4 w-4 mr-1" />
</Button>
</div>
</div>
{allocMessage && (
<div className="p-2 bg-primary/10 rounded text-sm text-primary shrink-0">{allocMessage}</div>
)}
{loading ? (
<>
<CardStatsSkeleton count={4} />
<div className="flex-1 flex flex-col min-h-0 mt-4">
<TableSkeleton rows={5} />
</div>
</>
) : (
<>
<motion.div
className="flex gap-4 shrink-0"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
<motion.div className="flex-1 h-full" variants={staggerItem}>
<AnimatedCard className="h-full">
<AnimatedCardContent className="p-4 h-full flex flex-col justify-center">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥<AnimatedNumber value={balance?.balance ?? 0} />
</div>
<div className="text-xs text-muted-foreground mt-0.5">
</div>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
<motion.div className="flex-1 h-full" variants={staggerItem}>
<AnimatedCard className="h-full">
<AnimatedCardContent className="p-4 h-full flex flex-col justify-center">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥<AnimatedNumber value={balance?.totalAllocated ?? 0} />
</div>
<div className="text-xs text-muted-foreground mt-0.5"></div>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
<motion.div className="flex-1 h-full" variants={staggerItem}>
<AnimatedCard className="h-full">
<AnimatedCardContent className="p-4 h-full flex flex-col justify-center">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥<AnimatedNumber value={balance?.totalExpenses ?? 0} />
</div>
<div className="text-xs text-muted-foreground mt-0.5"></div>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
<motion.div className="flex-1 h-full" variants={staggerItem}>
<AnimatedCard className="h-full">
<AnimatedCardContent className="p-4 h-full flex flex-col justify-center">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥<AnimatedNumber value={balance?.maxPoolAmount ?? 0} />
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{config.monthlyPercent}%
</div>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
</motion.div>
<div className="flex-1 flex flex-col min-h-0">
<div className="flex items-center justify-between mb-2 shrink-0">
<h2 className="text-sm font-semibold text-muted-foreground"></h2>
<Button size="sm" onClick={() => setExpenseOpen(true)}>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<DataTable columns={columns} data={expenses} pageSize={20} tableMinWidth="700px" />
</div>
</>
)}
</div>
{allocMessage && (
<div className="p-2 bg-primary/10 rounded text-sm text-primary shrink-0">{allocMessage}</div>
{configOpen && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={() => setConfigOpen(false)}
>
<div
className="bg-background rounded-lg p-6 w-[400px] shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-lg font-semibold mb-4"></h2>
<div className="space-y-4">
<div>
<label className="text-sm text-muted-foreground"> (%)</label>
<Input
type="number"
value={config.monthlyPercent}
onChange={(e) =>
setConfig((p) => ({ ...p, monthlyPercent: Number(e.target.value) }))
}
/>
</div>
<div>
<label className="text-sm text-muted-foreground"> ()</label>
<Input
type="number"
value={config.maxPoolAmount}
onChange={(e) =>
setConfig((p) => ({ ...p, maxPoolAmount: Number(e.target.value) }))
}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" onClick={() => setConfigOpen(false)}>
</Button>
<Button onClick={handleConfigSave}></Button>
</div>
</div>
</div>
)}
<div className="flex gap-4 shrink-0">
<Card className="flex-1">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥{formatMoney(balance?.balance ?? 0)}
</div>
</CardContent>
</Card>
<Card className="flex-1">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥{formatMoney(balance?.totalAllocated ?? 0)}
</div>
</CardContent>
</Card>
<Card className="flex-1">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥{formatMoney(balance?.totalExpenses ?? 0)}
</div>
</CardContent>
</Card>
<Card className="flex-1">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="text-2xl font-bold text-foreground">
¥{formatMoney(balance?.maxPoolAmount ?? 0)}
</div>
<div className="text-xs text-muted-foreground mt-0.5"> {config.monthlyPercent}%</div>
</CardContent>
</Card>
</div>
<div className="flex-1 flex flex-col min-h-0">
<div className="flex items-center justify-between mb-2 shrink-0">
<h2 className="text-sm font-semibold text-muted-foreground"></h2>
<Button size="sm" onClick={() => setExpenseOpen(true)}>
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<DataTable columns={columns} data={expenses} pageSize={50} tableMinWidth="700px" />
</div>
</div>
{configOpen && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
onClick={() => setConfigOpen(false)}
>
<div
className="bg-background rounded-lg p-6 w-[400px] shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-lg font-semibold mb-4"></h2>
<div className="space-y-4">
<div>
<label className="text-sm text-muted-foreground"> (%)</label>
<Input
type="number"
value={config.monthlyPercent}
onChange={(e) =>
setConfig((p) => ({ ...p, monthlyPercent: Number(e.target.value) }))
}
/>
</div>
<div>
<label className="text-sm text-muted-foreground"> ()</label>
<Input
type="number"
value={config.maxPoolAmount}
onChange={(e) =>
setConfig((p) => ({ ...p, maxPoolAmount: Number(e.target.value) }))
}
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-6">
<Button variant="outline" onClick={() => setConfigOpen(false)}>
</Button>
<Button onClick={handleConfigSave}></Button>
</div>
</div>
</div>
)}
<ExpenseDialog
open={expenseOpen}
onOpenChange={(v) => {
setExpenseOpen(v)
if (!v) setEditingExpense(null)
}}
onSave={handleExpenseSave}
editRecord={editingExpense}
/>
<ExpenseDialog
open={expenseOpen}
onOpenChange={(v) => {
setExpenseOpen(v)
if (!v) setEditingExpense(null)
}}
onSave={handleExpenseSave}
editRecord={editingExpense}
/>
</PageTransition>
</div>
)
}
+22 -13
View File
@@ -1,12 +1,14 @@
'use client'
import { useState, useMemo } from 'react'
import { PageTransition } from '@/components/page-transition'
import { CalendarHeader } from '@/components/calendar-header'
import { CalendarGrid } from '@/components/calendar-grid'
import { ReservationDialog } from '@/components/reservation-dialog'
import { ConsumptionDialog } from '@/components/consumption-dialog'
import { NavHeader } from '@/components/nav-header'
import { useReservations } from '@/hooks/use-reservations'
import { CalendarSkeleton } from '@/components/loading-skeleton'
import type { Reservation, CreateReservationInput, ConsumptionRecord } from '@/lib/types'
import { formatDate } from '@/lib/date-utils'
@@ -15,7 +17,7 @@ const today = new Date()
export default function Home() {
const [year, setYear] = useState(today.getFullYear())
const [month, setMonth] = useState(today.getMonth())
const { getReservationsForDate, refresh: refreshReservations } = useReservations(year, month)
const { getReservationsForDate, loading, refresh: refreshReservations } = useReservations(year, month)
const [selectedDate, setSelectedDate] = useState<string | null>(null)
const [dialogOpen, setDialogOpen] = useState(false)
const [dialogDefaultDate, setDialogDefaultDate] = useState('')
@@ -115,6 +117,8 @@ export default function Home() {
return (
<div className="h-full flex flex-col">
<NavHeader />
<PageTransition className="flex-1 flex flex-col">
{/* Calendar */}
<div className="flex-1 flex flex-col overflow-hidden">
@@ -140,18 +144,22 @@ export default function Home() {
onToday={handleToday}
onNewReservation={() => handleNewReservation()}
/>
<CalendarGrid
year={year}
month={month}
reservationsByDate={monthReservations}
selectedDate={selectedDate}
onSelectDate={setSelectedDate}
onNewReservation={handleNewReservation}
onEditReservation={handleEditReservation}
onDeleteReservation={handleDelete}
onConvertToConsumption={handleConvertToConsumption}
onMarkNoShow={handleMarkNoShow}
/>
{loading ? (
<CalendarSkeleton />
) : (
<CalendarGrid
year={year}
month={month}
reservationsByDate={monthReservations}
selectedDate={selectedDate}
onSelectDate={setSelectedDate}
onNewReservation={handleNewReservation}
onEditReservation={handleEditReservation}
onDeleteReservation={handleDelete}
onConvertToConsumption={handleConvertToConsumption}
onMarkNoShow={handleMarkNoShow}
/>
)}
</div>
{/* Reservation Dialog */}
@@ -171,6 +179,7 @@ export default function Home() {
onSave={handleConsumptionSave}
defaultValues={consumptionDefaults}
/>
</PageTransition>
</div>
)
}
+139 -108
View File
@@ -2,12 +2,17 @@
import { useState, useEffect, useMemo } from 'react'
import { type ColumnDef } from '@tanstack/react-table'
import { PageTransition } from '@/components/page-transition'
import { NavHeader } from '@/components/nav-header'
import { AnimatedCard, AnimatedCardContent, AnimatedNumber } from '@/components/animated'
import { staggerContainer, staggerItem } from '@/lib/motion'
import { motion } from 'framer-motion'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { DataTable } from '@/components/data-table'
import { CardStatsSkeleton, TableSkeleton } from '@/components/loading-skeleton'
import { DatePickerWithRange } from '@/components/date-range-picker'
import { CommissionSettings } from '@/components/commission-settings'
import { ConsumptionDialog } from '@/components/consumption-dialog'
@@ -70,34 +75,33 @@ export default function ProfitPage() {
const [deleteTargetIds, setDeleteTargetIds] = useState<Set<string>>(new Set())
const [commissions, setCommissions] = useState<CommissionItem[]>([])
useEffect(() => {
fetch('/api/commission')
.then((r) => r.json())
.then(setCommissions)
.catch(() => {})
}, [refreshKey])
const [loading, setLoading] = useState(true)
const [allConsumptions, setAllConsumptions] = useState<ConsumptionRecord[]>([])
const [targetData, setTargetData] = useState<any>(null)
useEffect(() => {
let cancelled = false
Promise.all([
fetch('/api/commission').then((r) => r.json()),
fetch('/api/target/current').then((r) => r.json()),
fetch('/api/consumption').then((r) => r.json()),
])
.then(([c, t, a]) => {
if (cancelled) return
setCommissions(c)
setTargetData(t)
setAllConsumptions(a)
setLoading(false)
})
.catch(() => {})
return () => { cancelled = true }
}, [refreshKey])
const [targetDialogOpen, setTargetDialogOpen] = useState(false)
const [editTargetAmount, setEditTargetAmount] = useState('')
const [targetCollapsed, setTargetCollapsed] = useState(true)
useEffect(() => {
fetch('/api/target/current')
.then((r) => r.json())
.then(setTargetData)
.catch(() => {})
}, [refreshKey])
useEffect(() => {
fetch('/api/consumption')
.then((r) => r.json())
.then(setAllConsumptions)
.catch(() => {})
}, [refreshKey])
const handleDateRange = (v: DateFilter) => {
setFilter(v)
const range = getFilterRange(v)
@@ -338,7 +342,8 @@ export default function ProfitPage() {
return (
<div className="h-full flex flex-col">
<NavHeader />
<div className="flex-1 flex flex-col p-6 gap-4 overflow-hidden">
<PageTransition className="flex-1">
<div className="flex flex-col p-6 gap-4 overflow-hidden h-full">
{/* Filter */}
<div className="flex items-center gap-2 shrink-0 flex-wrap">
{FILTER_OPTIONS.map((opt) => (
@@ -371,6 +376,15 @@ export default function ProfitPage() {
</Button>
</div>
{loading ? (
<>
<CardStatsSkeleton count={2 + commissions.length || 2} />
<div className="flex-1 flex flex-col min-h-0">
<TableSkeleton rows={8} />
</div>
</>
) : (
<>
{/* Target */}
<Card className={`shrink-0 ${targetData?.needsWarning ? 'border-destructive' : ''}`}>
<CardContent className="p-4">
@@ -394,11 +408,13 @@ export default function ProfitPage() {
{targetData?.hasTarget && (
<>
<div className="flex-1 h-2 bg-muted rounded-full overflow-hidden max-w-[200px]">
<div
className={`h-full rounded-full transition-all ${targetData.needsWarning ? 'bg-destructive' : 'bg-primary'}`}
style={{
<motion.div
className={`h-full rounded-full ${targetData.needsWarning ? 'bg-destructive' : 'bg-primary'}`}
initial={{ width: 0 }}
animate={{
width: `${Math.min((targetData.currentTotal / targetData.targetAmount) * 100, 100)}%`,
}}
transition={{ type: 'spring', stiffness: 200, damping: 30, delay: 0.2 }}
/>
</div>
<span
@@ -468,109 +484,121 @@ export default function ProfitPage() {
</Card>
{/* Stats cards: fixed + dynamic commission cards */}
<div className="flex gap-4 shrink-0 flex-wrap">
<Card className="flex-1 min-w-[160px]">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xl font-bold text-foreground flex-1 min-w-0">
¥{formatMoney(totals.totalServiceFee)}
</span>
{showChart && dailyChartData && (
<Sparkline
data={dailyChartData.map((d) => ({ day: d.date, value: d.serviceFee }))}
color={CHART_COLORS[0]}
/>
)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">{allItems.length} </div>
</CardContent>
</Card>
<Card className="flex-1 min-w-[160px]">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xl font-bold text-foreground flex-1 min-w-0">
¥{formatMoney(totals.totalWaiterEarnings)}
</span>
{showChart && dailyChartData && (
<Sparkline
data={dailyChartData.map((d) => ({ day: d.date, value: d.waiterEarnings }))}
color={CHART_COLORS[1]}
/>
)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{' '}
{totals.totalServiceFee
? ((totals.totalWaiterEarnings / totals.totalServiceFee) * 100).toFixed(1)
: 0}
%
</div>
</CardContent>
</Card>
{commissions.map((c, i) => (
<Card key={c.id} className="flex-1 min-w-[160px]">
<CardContent className="p-4">
<div className="text-xs text-muted-foreground">{c.name}</div>
<motion.div
className="flex gap-4 shrink-0 flex-wrap"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
<motion.div className="flex-1 min-w-[160px]" variants={staggerItem}>
<AnimatedCard>
<AnimatedCardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xl font-bold text-foreground flex-1 min-w-0">
¥{formatMoney(totals.totalCommissionEarnings[c.id] || 0)}
¥<AnimatedNumber value={totals.totalServiceFee} />
</span>
{showChart && dailyChartData && (
<Sparkline
data={dailyChartData.map((d) => ({
day: d.date,
value: d.commissionEarnings[c.id] || 0,
}))}
color={CHART_COLORS[(i + 2) % CHART_COLORS.length]}
data={dailyChartData.map((d) => ({ day: d.date, value: d.serviceFee }))}
color={CHART_COLORS[0]}
/>
)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">{allItems.length} </div>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
<motion.div className="flex-1 min-w-[160px]" variants={staggerItem}>
<AnimatedCard>
<AnimatedCardContent className="p-4">
<div className="text-xs text-muted-foreground"></div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xl font-bold text-foreground flex-1 min-w-0">
¥<AnimatedNumber value={totals.totalWaiterEarnings} />
</span>
{showChart && dailyChartData && (
<Sparkline
data={dailyChartData.map((d) => ({ day: d.date, value: d.waiterEarnings }))}
color={CHART_COLORS[1]}
/>
)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{c.rate}% · {' '}
{' '}
{totals.totalServiceFee
? (
((totals.totalCommissionEarnings[c.id] || 0) / totals.totalServiceFee) *
100
).toFixed(1)
? ((totals.totalWaiterEarnings / totals.totalServiceFee) * 100).toFixed(1)
: 0}
%
</div>
</CardContent>
</Card>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
{commissions.map((c, i) => (
<motion.div key={c.id} className="flex-1 min-w-[160px]" variants={staggerItem}>
<AnimatedCard>
<AnimatedCardContent className="p-4">
<div className="text-xs text-muted-foreground">{c.name}</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xl font-bold text-foreground flex-1 min-w-0">
¥<AnimatedNumber value={totals.totalCommissionEarnings[c.id] || 0} />
</span>
{showChart && dailyChartData && (
<Sparkline
data={dailyChartData.map((d) => ({
day: d.date,
value: d.commissionEarnings[c.id] || 0,
}))}
color={CHART_COLORS[(i + 2) % CHART_COLORS.length]}
/>
)}
</div>
<div className="text-xs text-muted-foreground mt-0.5">
{c.rate}% · {' '}
{totals.totalServiceFee
? (((totals.totalCommissionEarnings[c.id] || 0) / totals.totalServiceFee) *
100
).toFixed(1)
: 0}
%
</div>
</AnimatedCardContent>
</AnimatedCard>
</motion.div>
))}
<CommissionSettings
items={commissions}
onAdd={async (name, rate) => {
await fetch('/api/commission', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, rate }),
})
setRefreshKey((n) => n + 1)
}}
onUpdate={async (id, name, rate) => {
await fetch(`/api/commission/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, rate }),
})
setRefreshKey((n) => n + 1)
}}
onDelete={async (id) => {
await fetch(`/api/commission/${id}`, { method: 'DELETE' })
setRefreshKey((n) => n + 1)
}}
/>
</div>
<motion.div variants={staggerItem}>
<CommissionSettings
items={commissions}
onAdd={async (name, rate) => {
await fetch('/api/commission', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, rate }),
})
setRefreshKey((n) => n + 1)
}}
onUpdate={async (id, name, rate) => {
await fetch(`/api/commission/${id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, rate }),
})
setRefreshKey((n) => n + 1)
}}
onDelete={async (id) => {
await fetch(`/api/commission/${id}`, { method: 'DELETE' })
setRefreshKey((n) => n + 1)
}}
/>
</motion.div>
</motion.div>
{/* Table */}
<div className="flex-1 flex flex-col min-h-0">
<DataTable
columns={columns}
data={allItems}
pageSize={100}
pageSize={20}
searchKey="id"
searchPlaceholder="搜索ID..."
tableMinWidth="1300px"
@@ -598,6 +626,8 @@ export default function ProfitPage() {
}
/>
</div>
</>
)}
</div>
{/* Target Dialog */}
@@ -692,6 +722,7 @@ export default function ProfitPage() {
editRecord={editingConsumption}
defaultValues={consumptionDefaults}
/>
</PageTransition>
</div>
)
}
+14 -4
View File
@@ -2,11 +2,19 @@
import { useState, useEffect, useCallback } from 'react'
import { type ColumnDef } from '@tanstack/react-table'
import { PageTransition } from '@/components/page-transition'
import { NavHeader } from '@/components/nav-header'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
import {
AnimatedDialog as Dialog,
AnimatedDialogContent as DialogContent,
AnimatedDialogHeader as DialogHeader,
AnimatedDialogTitle as DialogTitle,
} from '@/components/animated'
import { AnimatedButton } from '@/components/animated'
import { DataTable } from '@/components/data-table'
import { TableSkeleton } from '@/components/loading-skeleton'
import { WaiterForm } from '@/components/waiter-form'
import { Edit2, Trash2, Plus, Minus } from 'lucide-react'
import type { Waiter, CreateWaiterInput, UpdateWaiterInput } from '@/lib/types'
@@ -195,10 +203,11 @@ export default function WaiterPage() {
return (
<div className="h-full flex flex-col">
<NavHeader />
<PageTransition className="flex-1">
<div className="flex-1 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-foreground"></h2>
<Button
<AnimatedButton
size="sm"
onClick={() => {
setEditingWaiter(null)
@@ -206,10 +215,10 @@ export default function WaiterPage() {
}}
>
<Plus className="h-4 w-4 mr-1" />
</Button>
</AnimatedButton>
</div>
{loading ? (
<div className="text-center text-muted-foreground py-12">...</div>
<TableSkeleton rows={5} />
) : (
<DataTable
columns={columns}
@@ -229,6 +238,7 @@ export default function WaiterPage() {
<WaiterForm waiter={editingWaiter} onSave={handleSave} />
</DialogContent>
</Dialog>
</PageTransition>
</div>
)
}
+12
View File
@@ -0,0 +1,12 @@
'use client'
import { motion } from 'framer-motion'
import { Button, type ButtonProps } from '@/components/ui/button'
export function AnimatedButton({ children, className, ...props }: ButtonProps) {
return (
<motion.div whileTap={{ scale: 0.96 }} whileHover={{ scale: 1.03 }}>
<Button className={className} {...props}>{children}</Button>
</motion.div>
)
}
+22
View File
@@ -0,0 +1,22 @@
'use client'
import { motion } from 'framer-motion'
import { Card, CardContent } from '@/components/ui/card'
import type { HTMLAttributes } from 'react'
export function AnimatedCard({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return (
<motion.div
whileHover={{ y: -3, boxShadow: '0 4px 12px rgba(0,0,0,0.08)' }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
<Card className={className} {...props}>
{children}
</Card>
</motion.div>
)
}
export function AnimatedCardContent({ className, children, ...props }: HTMLAttributes<HTMLDivElement>) {
return <CardContent className={className} {...props}>{children}</CardContent>
}
+93
View File
@@ -0,0 +1,93 @@
'use client'
import * as React from 'react'
import { AnimatePresence, motion } from 'framer-motion'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import {
Dialog,
DialogPortal,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogClose,
DialogTrigger,
} from '@/components/ui/dialog'
import { cn } from '@/lib/utils'
import { fadeIn, scaleInCenter } from '@/lib/motion'
const AnimatedDialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPortal>
<AnimatePresence>
<DialogPrimitive.Overlay ref={ref} asChild forceMount {...props}>
<motion.div
className={cn('fixed inset-0 z-50 bg-black/80', className)}
variants={fadeIn}
initial="hidden"
animate="visible"
exit="hidden"
/>
</DialogPrimitive.Overlay>
</AnimatePresence>
</DialogPortal>
))
AnimatedDialogOverlay.displayName = 'AnimatedDialogOverlay'
const AnimatedDialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<AnimatePresence>
<DialogPrimitive.Overlay key="dialog-overlay" asChild forceMount>
<motion.div
className="fixed inset-0 z-50 bg-black/80"
variants={fadeIn}
initial="hidden"
animate="visible"
exit="hidden"
/>
</DialogPrimitive.Overlay>
<DialogPrimitive.Content
key="dialog-content"
ref={ref}
forceMount
className={cn(
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg sm:rounded-lg',
className,
)}
asChild
{...props}
>
<motion.div
variants={scaleInCenter}
initial="hidden"
animate="visible"
exit="exit"
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</motion.div>
</DialogPrimitive.Content>
</AnimatePresence>
</DialogPortal>
))
AnimatedDialogContent.displayName = 'AnimatedDialogContent'
export { AnimatedDialogContent, AnimatedDialogOverlay }
export {
Dialog as AnimatedDialog,
DialogHeader as AnimatedDialogHeader,
DialogFooter as AnimatedDialogFooter,
DialogTitle as AnimatedDialogTitle,
DialogDescription as AnimatedDialogDescription,
DialogClose as AnimatedDialogClose,
DialogTrigger as AnimatedDialogTrigger,
}
+26
View File
@@ -0,0 +1,26 @@
'use client'
import { useEffect, useState } from 'react'
import { useSpring } from 'framer-motion'
interface AnimatedNumberProps {
value: number
className?: string
decimals?: number
}
export function AnimatedNumber({ value, className, decimals = 2 }: AnimatedNumberProps) {
const spring = useSpring(0, { stiffness: 200, damping: 30 })
const [display, setDisplay] = useState(0)
useEffect(() => {
spring.set(value)
}, [value, spring])
useEffect(() => {
const unsub = spring.on('change', (v) => setDisplay(v))
return () => unsub()
}, [spring])
return <span className={className}>{display.toFixed(decimals)}</span>
}
+13
View File
@@ -0,0 +1,13 @@
export { AnimatedButton } from './animated-button'
export {
AnimatedDialog,
AnimatedDialogContent,
AnimatedDialogHeader,
AnimatedDialogFooter,
AnimatedDialogTitle,
AnimatedDialogDescription,
AnimatedDialogClose,
AnimatedDialogTrigger,
} from './animated-dialog'
export { AnimatedCard, AnimatedCardContent } from './animated-card'
export { AnimatedNumber } from './animated-number'
+23 -11
View File
@@ -7,9 +7,11 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from '@/components/ui/context-menu'
import { motion, AnimatePresence } from 'framer-motion'
import { Plus } from 'lucide-react'
import type { Reservation } from '@/lib/types'
import { isCurrentMonth, isToday } from '@/lib/date-utils'
import { staggerItem } from '@/lib/motion'
import { ReservationCard } from './reservation-card'
interface CalendarCellProps {
@@ -65,17 +67,27 @@ export function CalendarCell({
</span>
</div>
<div className="space-y-0.5">
{reservations.slice(0, 2).map((r) => (
<ReservationCard
key={r.id}
reservation={r}
onClick={onEditReservation}
onEdit={onEditReservation}
onDelete={onDeleteReservation}
onConvertToConsumption={onConvertToConsumption}
onMarkNoShow={onMarkNoShow}
/>
))}
<AnimatePresence mode="popLayout">
{reservations.slice(0, 2).map((r) => (
<motion.div
key={r.id}
variants={staggerItem}
initial="hidden"
animate="visible"
exit={{ opacity: 0, scale: 0.9, transition: { duration: 0.15 } }}
layout
>
<ReservationCard
reservation={r}
onClick={onEditReservation}
onEdit={onEditReservation}
onDelete={onDeleteReservation}
onConvertToConsumption={onConvertToConsumption}
onMarkNoShow={onMarkNoShow}
/>
</motion.div>
))}
</AnimatePresence>
{displayMore && (
<span
className="text-[11px] text-muted-foreground px-1 cursor-pointer hover:text-foreground"
+7 -1
View File
@@ -1,7 +1,13 @@
'use client'
import { useState, useMemo, useEffect, useRef } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import {
AnimatedDialog as Dialog,
AnimatedDialogContent as DialogContent,
AnimatedDialogHeader as DialogHeader,
AnimatedDialogTitle as DialogTitle,
AnimatedDialogFooter as DialogFooter,
} from '@/components/animated'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
+18 -7
View File
@@ -14,7 +14,9 @@ import {
getPaginationRowModel,
useReactTable,
} from '@tanstack/react-table'
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { Table, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
import { motion } from 'framer-motion'
import { staggerContainer, staggerItem } from '@/lib/motion'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
@@ -152,28 +154,37 @@ export function DataTable<TData>({
</TableRow>
))}
</TableHeader>
<TableBody>
<motion.tbody
className="[&_tr:last-child]:border-0"
variants={staggerContainer}
initial="hidden"
animate="visible"
>
{table.getRowModel().rows.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
<motion.tr
key={row.id}
variants={staggerItem}
className="border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
</motion.tr>
))
) : (
<TableRow>
<tr>
<TableCell
colSpan={allColumns.length}
className="h-24 text-center text-muted-foreground"
>
</TableCell>
</TableRow>
</tr>
)}
</TableBody>
</motion.tbody>
</Table>
</div>
</div>
+83
View File
@@ -0,0 +1,83 @@
'use client'
import { motion } from 'framer-motion'
import { cn } from '@/lib/utils'
function Pulse({ className }: { className?: string }) {
return <div className={cn('animate-pulse rounded-md bg-muted', className)} />
}
export function CalendarSkeleton() {
return (
<div className="flex-1 flex flex-col">
<div className="grid grid-cols-7 border-t border-l">
{['一', '二', '三', '四', '五', '六', '日'].map((name) => (
<div
key={name}
className="px-2 py-2 text-xs font-medium text-muted-foreground border-r border-b bg-muted/30 text-center"
>
{name}
</div>
))}
</div>
<div className="grid grid-cols-7 border-l flex-1">
{Array.from({ length: 35 }).map((_, i) => (
<div key={i} className="min-h-[90px] border-r border-b p-1 bg-background">
<Pulse className="w-6 h-6 rounded-full mb-1" />
<div className="space-y-1">
<Pulse className="h-4 w-3/4" />
<Pulse className="h-4 w-1/2" />
</div>
</div>
))}
</div>
</div>
)
}
export function CardStatsSkeleton({ count = 4 }: { count?: number }) {
return (
<div className="flex gap-4 shrink-0 flex-wrap">
{Array.from({ length: count }).map((_, i) => (
<motion.div
key={i}
className="flex-1 min-w-[160px] rounded-lg border bg-card p-4"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.05 }}
>
<Pulse className="h-3 w-16 mb-3" />
<Pulse className="h-7 w-24 mb-1" />
<Pulse className="h-3 w-12" />
</motion.div>
))}
</div>
)
}
export function TableSkeleton({ rows = 5 }: { rows?: number }) {
return (
<div className="rounded-md border overflow-hidden">
<div className="border-b bg-muted/50 px-4 py-3 flex items-center gap-4">
<Pulse className="h-4 w-24" />
<Pulse className="h-4 w-32" />
<Pulse className="h-4 w-20" />
<Pulse className="h-4 w-16" />
</div>
{Array.from({ length: rows }).map((_, i) => (
<motion.div
key={i}
className="border-b px-4 py-3 flex items-center gap-4"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: i * 0.06 }}
>
<Pulse className="h-4 w-20" />
<Pulse className="h-4 w-32" />
<Pulse className="h-4 w-16" />
<Pulse className="h-4 w-24" />
</motion.div>
))}
</div>
)
}
+30 -15
View File
@@ -3,6 +3,7 @@
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import { motion } from 'framer-motion'
import { ThemeToggle } from '@/components/theme-toggle'
import { Button } from '@/components/ui/button'
import { CalendarDays, Users, TrendingUp, Wallet, Target } from 'lucide-react'
@@ -32,28 +33,42 @@ export function NavHeader() {
}, [])
return (
<header className="flex items-center px-6 py-3 border-b bg-background">
<header className="sticky top-0 z-20 flex items-center px-6 py-3 border-b bg-background/30 backdrop-blur-sm">
<div className="w-[200px]">
<Link href="/" className="flex items-center gap-2 shrink-0">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center">
<motion.div
className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center"
whileHover={{ scale: 1.08 }}
whileTap={{ scale: 0.95 }}
>
<span className="text-primary-foreground font-bold text-sm">K</span>
</div>
</motion.div>
<span className="text-base font-bold text-foreground hidden sm:inline">Keeppay</span>
</Link>
</div>
<nav className="flex-1 flex items-center justify-center gap-1">
{NAV_ITEMS.map((item) => (
<Link key={item.href} href={item.href}>
<Button
variant={pathname === item.href ? 'secondary' : 'ghost'}
size="sm"
className="text-sm gap-1.5"
>
<item.icon className="h-4 w-4" />
{item.label}
</Button>
</Link>
))}
{NAV_ITEMS.map((item) => {
const isActive = pathname === item.href
return (
<Link key={item.href} href={item.href}>
<Button
variant={isActive ? 'secondary' : 'ghost'}
size="sm"
className="relative text-sm gap-1.5"
>
<item.icon className="h-4 w-4" />
{item.label}
{isActive && (
<motion.div
layoutId="nav-underline"
className="absolute bottom-0 left-2 right-2 h-0.5 bg-primary rounded-full"
transition={{ type: 'spring', stiffness: 400, damping: 30 }}
/>
)}
</Button>
</Link>
)
})}
</nav>
<div className="w-[200px] flex items-center justify-end gap-2">
{targetData?.hasTarget && (
+30
View File
@@ -0,0 +1,30 @@
'use client'
import { motion, AnimatePresence } from 'framer-motion'
import { usePathname } from 'next/navigation'
import type { ReactNode } from 'react'
import { fadeInSlideUp } from '@/lib/motion'
interface PageTransitionProps {
children: ReactNode
className?: string
}
export function PageTransition({ children, className }: PageTransitionProps) {
const pathname = usePathname()
return (
<AnimatePresence mode="wait">
<motion.div
key={pathname}
variants={fadeInSlideUp}
initial="hidden"
animate="visible"
exit="exit"
className={className}
>
{children}
</motion.div>
</AnimatePresence>
)
}
+7 -1
View File
@@ -1,7 +1,13 @@
'use client'
import { useState, useEffect } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
import {
AnimatedDialog as Dialog,
AnimatedDialogContent as DialogContent,
AnimatedDialogHeader as DialogHeader,
AnimatedDialogTitle as DialogTitle,
AnimatedDialogFooter as DialogFooter,
} from '@/components/animated'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
+2 -2
View File
@@ -20,7 +20,7 @@ export function Sparkline({ data, color }: SparklineProps) {
fill={color}
fillOpacity={0.15}
stroke="none"
isAnimationActive={false}
isAnimationActive={true}
/>
<Line
type="monotone"
@@ -29,7 +29,7 @@ export function Sparkline({ data, color }: SparklineProps) {
strokeWidth={1.5}
dot={false}
activeDot={false}
isAnimationActive={false}
isAnimationActive={true}
/>
</ComposedChart>
</ResponsiveContainer>
+8 -1
View File
@@ -1,6 +1,7 @@
'use client'
import { useEffect, useState } from 'react'
import { motion } from 'framer-motion'
import { Moon, Sun } from 'lucide-react'
import { Button } from '@/components/ui/button'
@@ -30,7 +31,13 @@ export function ThemeToggle() {
className="h-8 w-8"
title={isDark ? '切换到亮色模式' : '切换到暗色模式'}
>
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
<motion.div
initial={false}
animate={{ rotate: isDark ? 180 : 0 }}
transition={{ type: 'spring', stiffness: 300, damping: 25 }}
>
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
</motion.div>
</Button>
)
}
+11
View File
@@ -0,0 +1,11 @@
export {
fadeIn,
slideUp,
slideDown,
slideLeft,
scaleIn,
scaleInCenter,
staggerContainer,
staggerItem,
} from './variants'
export { fadeInSlideUp, scaleInFade, slideDownFade } from './presets'
+46
View File
@@ -0,0 +1,46 @@
import { fadeIn, slideUp, slideDown, scaleIn } from './variants'
// Fade in + slide up
export const fadeInSlideUp = {
hidden: { ...fadeIn.hidden, ...slideUp.hidden },
visible: {
...fadeIn.visible,
...slideUp.visible,
transition: { type: 'spring' as const, stiffness: 300, damping: 30 },
},
exit: {
...fadeIn.exit,
...slideUp.exit,
transition: { duration: 0.15, ease: 'easeInOut' as const },
},
}
// Scale in + fade
export const scaleInFade = {
hidden: { ...scaleIn.hidden, ...fadeIn.hidden },
visible: {
...scaleIn.visible,
...fadeIn.visible,
transition: { type: 'spring' as const, stiffness: 300, damping: 30 },
},
exit: {
...scaleIn.exit,
...fadeIn.exit,
transition: { duration: 0.15, ease: 'easeInOut' as const },
},
}
// Slide down + fade (fast)
export const slideDownFade = {
hidden: { ...slideDown.hidden, ...fadeIn.hidden },
visible: {
...slideDown.visible,
...fadeIn.visible,
transition: { type: 'spring' as const, stiffness: 400, damping: 30 },
},
exit: {
...slideDown.exit,
...fadeIn.exit,
transition: { duration: 0.1, ease: 'easeInOut' as const },
},
}
+59
View File
@@ -0,0 +1,59 @@
import type { Variants } from 'framer-motion'
// Spring default: stiffness 300, damping 30
const spring = { type: 'spring' as const, stiffness: 300, damping: 30 }
const fastSpring = { type: 'spring' as const, stiffness: 400, damping: 30 }
const exitTransition = { duration: 0.15, ease: 'easeInOut' as const }
// --- Fade ---
export const fadeIn: Variants = {
hidden: { opacity: 0 },
visible: { opacity: 1, transition: spring },
exit: { opacity: 0, transition: exitTransition },
}
// --- Slide ---
export const slideUp: Variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0, transition: spring },
exit: { opacity: 0, y: -10, transition: exitTransition },
}
export const slideDown: Variants = {
hidden: { opacity: 0, y: -10 },
visible: { opacity: 1, y: 0, transition: fastSpring },
exit: { opacity: 0, y: 10, transition: exitTransition },
}
export const slideLeft: Variants = {
hidden: { opacity: 0, x: 20 },
visible: { opacity: 1, x: 0, transition: spring },
exit: { opacity: 0, x: -20, transition: exitTransition },
}
// --- Scale ---
export const scaleIn: Variants = {
hidden: { opacity: 0, scale: 0.95 },
visible: { opacity: 1, scale: 1, transition: spring },
exit: { opacity: 0, scale: 0.95, transition: exitTransition },
}
export const scaleInCenter: Variants = {
hidden: { opacity: 0, scale: 0.95, y: 10 },
visible: { opacity: 1, scale: 1, y: 0, transition: spring },
exit: { opacity: 0, scale: 0.95, y: 10, transition: exitTransition },
}
// --- Stagger list ---
export const staggerContainer: Variants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.05, delayChildren: 0.05 },
},
}
export const staggerItem: Variants = {
hidden: { opacity: 0, y: 12 },
visible: { opacity: 1, y: 0, transition: spring },
}
+1
View File
@@ -36,6 +36,7 @@
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"framer-motion": "^12.38.0",
"lucide-react": "^1.14.0",
"next": "16.2.5",
"prisma": "^7.8.0",
+38
View File
@@ -86,6 +86,9 @@ importers:
date-fns:
specifier: ^4.1.0
version: 4.1.0
framer-motion:
specifier: ^12.38.0
version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
lucide-react:
specifier: ^1.14.0
version: 1.14.0(react@19.2.4)
@@ -2348,6 +2351,20 @@ packages:
formstream@1.5.2:
resolution: {integrity: sha512-NASf0lgxC1AyKNXQIrXTEYkiX99LhCEXTkiGObXAkpBui86a4u8FjH1o2bGb3PpqI3kafC+yw4zWeK6l6VHTgg==}
framer-motion@12.38.0:
resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==}
peerDependencies:
'@emotion/is-prop-valid': '*'
react: ^18.0.0 || ^19.0.0
react-dom: ^18.0.0 || ^19.0.0
peerDependenciesMeta:
'@emotion/is-prop-valid':
optional: true
react:
optional: true
react-dom:
optional: true
fs-constants@1.0.0:
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
@@ -2894,6 +2911,12 @@ packages:
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
hasBin: true
motion-dom@12.38.0:
resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==}
motion-utils@12.36.0:
resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==}
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
@@ -6099,6 +6122,15 @@ snapshots:
node-hex: 1.0.1
pause-stream: 0.0.11
framer-motion@12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4):
dependencies:
motion-dom: 12.38.0
motion-utils: 12.36.0
tslib: 2.8.1
optionalDependencies:
react: 19.2.4
react-dom: 19.2.4(react@19.2.4)
fs-constants@1.0.0: {}
function-bind@1.1.2: {}
@@ -6587,6 +6619,12 @@ snapshots:
dependencies:
minimist: 1.2.8
motion-dom@12.38.0:
dependencies:
motion-utils: 12.36.0
motion-utils@12.36.0: {}
ms@2.1.3: {}
mysql2@3.15.3: