diff --git a/frontend/.gitattributes b/frontend/.gitattributes new file mode 100644 index 000000000..3914ba8a6 --- /dev/null +++ b/frontend/.gitattributes @@ -0,0 +1,2 @@ +# 자동 생성되는 라우트 트리 파일의 줄바꿈을 LF로 고정 +src/routeTree.gen.ts text eol=lf diff --git a/frontend/src/components/report-page/ReportLegend.tsx b/frontend/src/components/report-page/ReportLegend.tsx new file mode 100644 index 000000000..7b29cc872 --- /dev/null +++ b/frontend/src/components/report-page/ReportLegend.tsx @@ -0,0 +1,30 @@ +import clsx from 'clsx'; + +const LEGEND_COLOR = { + primary: 'bg-primary-normal', + secondary: 'bg-cool-neutral-90', +} as const; + +const SHAPE_STYLE = { + box: 'size-2.5', + line: 'w-6.25 h-1 rounded-modal-10', +} as const; + +interface ReportLegendProps { + color: keyof typeof LEGEND_COLOR; + label: string; + variant?: keyof typeof SHAPE_STYLE; +} + +const ReportLegend = ({ color, label, variant = 'box' }: ReportLegendProps) => { + return ( +
+
+ + {label} + +
+ ); +}; + +export default ReportLegend; diff --git a/frontend/src/components/report-page/VerticalGrid.tsx b/frontend/src/components/report-page/VerticalGrid.tsx new file mode 100644 index 000000000..589bb25a5 --- /dev/null +++ b/frontend/src/components/report-page/VerticalGrid.tsx @@ -0,0 +1,67 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import { clsx } from 'clsx'; + +interface VerticalGridProps extends ComponentPropsWithoutRef<'div'> { + steps?: number; + labels: (number | string)[]; + positions?: number[]; // 커스텀 위치 배열 +} + +const VerticalGrid = ({ + steps, + labels, + positions, + className, +}: VerticalGridProps) => { + const gridPositions = + positions || + (steps !== undefined + ? Array.from({ length: steps + 1 }, (_, i) => (i / steps) * 100) + : []); + + if (gridPositions.length === 0) return null; + + return ( +
+
+ {gridPositions.map((position, i) => ( +
+ ))} +
+
+ {gridPositions.map((position, i) => { + const label = labels[i]; + if (label === undefined || label === null) return null; + + return ( +
+ {typeof label === 'number' + ? Math.round(label).toLocaleString() + : label} +
+ ); + })} +
+
+ ); +}; + +export default VerticalGrid; diff --git a/frontend/src/components/report-page/category/ReportBar.tsx b/frontend/src/components/report-page/category/ReportBar.tsx index 3fb9f4a40..2f3a9d05b 100644 --- a/frontend/src/components/report-page/category/ReportBar.tsx +++ b/frontend/src/components/report-page/category/ReportBar.tsx @@ -6,7 +6,6 @@ import CurrencyAmountDisplay from '@/components/currency/CurrencyAmountDisplay'; import CurrencyBadge from '@/components/currency/CurrencyBadge'; import { useReportContext } from '@/components/report-page/ReportContext'; -import { type CountryCode } from '@/data/countryCode'; import { useAccountBookStore } from '@/stores/useAccountBookStore'; interface ReportBarProps { @@ -30,8 +29,10 @@ const VARIANT_STYLES = { const ReportBar = ({ value, variant, maxValue }: ReportBarProps) => { const { currencyType } = useReportContext(); - const countryCode = useAccountBookStore((state) => state.accountBook?.localCountryCode) as CountryCode; - + const countryCode = useAccountBookStore( + (state) => state.accountBook?.localCountryCode, + ); + const styles = VARIANT_STYLES[variant]; const percentage = (value / maxValue) * 100; const [showAmount, setShowAmount] = useState(false); @@ -46,6 +47,8 @@ const ReportBar = ({ value, variant, maxValue }: ReportBarProps) => { return () => clearTimeout(timer); }, []); + if (!countryCode) return null; + return (
{ - return ( -
-
- - {label} - -
- ); -}; - +import { type CategoryId } from '@/types/category'; interface ReportBarGraphProps { maxLabel: number; items: { @@ -49,8 +27,8 @@ const ReportBarGraph = ({ maxLabel, items }: ReportBarGraphProps) => { return (
- - + +
diff --git a/frontend/src/components/report-page/category/ReportBarList.tsx b/frontend/src/components/report-page/category/ReportBarList.tsx index 3eb5d1908..278f62528 100644 --- a/frontend/src/components/report-page/category/ReportBarList.tsx +++ b/frontend/src/components/report-page/category/ReportBarList.tsx @@ -1,5 +1,5 @@ import ReportBarRow from '@/components/report-page/category/ReportBarRow'; -import VerticalGrid from '@/components/report-page/category/VerticalGrid'; +import VerticalGrid from '@/components/report-page/VerticalGrid'; import type { CategoryId } from '@/types/category'; @@ -12,9 +12,15 @@ interface ReportBarListProps { }[]; } const ReportBarList = ({ items, maxLabel }: ReportBarListProps) => { + const steps = 6; + const labels = Array.from( + { length: steps + 1 }, + (_, i) => (maxLabel / steps) * i, + ); + return (
- +
{items.map((item) => ( { return ( - +

나랑 같은 국가의 교환학생보다{' '} - {category} 소비가 유독{' '} + {category} 소비가 유독{' '} {data.isOverSpent ? '많아요' : '적어요'}

diff --git a/frontend/src/components/report-page/category/VerticalGrid.tsx b/frontend/src/components/report-page/category/VerticalGrid.tsx deleted file mode 100644 index 4971a070c..000000000 --- a/frontend/src/components/report-page/category/VerticalGrid.tsx +++ /dev/null @@ -1,47 +0,0 @@ -interface VerticalGridProps { - steps: number; - maxLabel: number; -} - -const VerticalGrid = ({ steps, maxLabel }: VerticalGridProps) => { - if (steps <= 0) { - return null; - } - - const values = Array.from( - { length: steps + 1 }, - (_, i) => (maxLabel / steps) * i, - ); - - return ( -
-
- {values.map((_, i) => ( -
- ))} -
-
- {values.map((value, i) => ( -
- {Math.round(value).toLocaleString()} -
- ))} -
-
- ); -}; - -export default VerticalGrid; diff --git a/frontend/src/components/report-page/layout/ReportContent.tsx b/frontend/src/components/report-page/layout/ReportContent.tsx index 7d3d4de3b..b2a079201 100644 --- a/frontend/src/components/report-page/layout/ReportContent.tsx +++ b/frontend/src/components/report-page/layout/ReportContent.tsx @@ -17,7 +17,7 @@ const ReportContent = ({ return (
diff --git a/frontend/src/components/report-page/mock.ts b/frontend/src/components/report-page/mock.ts index 7a10c7b5d..84234f58c 100644 --- a/frontend/src/components/report-page/mock.ts +++ b/frontend/src/components/report-page/mock.ts @@ -6,6 +6,11 @@ interface CategoryItem { averageSpentAmount: string; } +interface MyselfItem { + date: string; + cumulatedAmount: string; +} + const mockData = { countryCode: 'US', compareWithAverage: { @@ -16,70 +21,72 @@ const mockData = { }, compareWithLastMonth: { diff: '150', - thisMonth: '2026-01', - thisMonthCount: 20, - lastMonth: '2025-12', - lastMonthCount: 31, - totalSpent: { - thisMonthToDate: '830', - lastMonthToSameDay: '680', - lastMonthTotal: '2311.46', + thisMonth: { + label: '1월', + dayCount: 20, + totalSpent: '830', + items: [ + { date: '2026-01-01', cumulatedAmount: '12.5' }, + { date: '2026-01-02', cumulatedAmount: '35.2' }, + { date: '2026-01-03', cumulatedAmount: '52.8' }, + { date: '2026-01-04', cumulatedAmount: '78.1' }, + { date: '2026-01-05', cumulatedAmount: '101' }, + { date: '2026-01-06', cumulatedAmount: '128.4' }, + { date: '2026-01-07', cumulatedAmount: '160.9' }, + { date: '2026-01-08', cumulatedAmount: '190.3' }, + { date: '2026-01-09', cumulatedAmount: '230.7' }, + { date: '2026-01-10', cumulatedAmount: '260.1' }, + { date: '2026-01-11', cumulatedAmount: '300.6' }, + { date: '2026-01-12', cumulatedAmount: '338.9' }, + { date: '2026-01-13', cumulatedAmount: '380.2' }, + { date: '2026-01-14', cumulatedAmount: '420.1' }, + { date: '2026-01-15', cumulatedAmount: '465' }, + { date: '2026-01-16', cumulatedAmount: '520.4' }, + { date: '2026-01-17', cumulatedAmount: '600.9' }, + { date: '2026-01-18', cumulatedAmount: '700.3' }, + { date: '2026-01-19', cumulatedAmount: '780.1' }, + { date: '2026-01-20', cumulatedAmount: '830' }, + ] as MyselfItem[], + }, + + lastMonth: { + label: '12월', + dayCount: 31, + totalSpent: '2311.46', + items: [ + { date: '2025-12-01', cumulatedAmount: '18' }, + { date: '2025-12-02', cumulatedAmount: '26' }, + { date: '2025-12-03', cumulatedAmount: '41' }, + { date: '2025-12-04', cumulatedAmount: '58' }, + { date: '2025-12-05', cumulatedAmount: '82' }, + { date: '2025-12-06', cumulatedAmount: '103' }, + { date: '2025-12-07', cumulatedAmount: '121' }, + { date: '2025-12-08', cumulatedAmount: '150' }, + { date: '2025-12-09', cumulatedAmount: '190' }, + { date: '2025-12-10', cumulatedAmount: '220' }, + { date: '2025-12-11', cumulatedAmount: '260' }, + { date: '2025-12-12', cumulatedAmount: '300' }, + { date: '2025-12-13', cumulatedAmount: '330' }, + { date: '2025-12-14', cumulatedAmount: '370' }, + { date: '2025-12-15', cumulatedAmount: '420' }, + { date: '2025-12-16', cumulatedAmount: '480' }, + { date: '2025-12-17', cumulatedAmount: '540' }, + { date: '2025-12-18', cumulatedAmount: '610' }, + { date: '2025-12-19', cumulatedAmount: '650' }, + { date: '2025-12-20', cumulatedAmount: '680' }, + { date: '2025-12-21', cumulatedAmount: '710' }, + { date: '2025-12-22', cumulatedAmount: '760' }, + { date: '2025-12-23', cumulatedAmount: '810' }, + { date: '2025-12-24', cumulatedAmount: '860' }, + { date: '2025-12-25', cumulatedAmount: '930' }, + { date: '2025-12-26', cumulatedAmount: '990' }, + { date: '2025-12-27', cumulatedAmount: '1100' }, + { date: '2025-12-28', cumulatedAmount: '1300' }, + { date: '2025-12-29', cumulatedAmount: '1600' }, + { date: '2025-12-30', cumulatedAmount: '2000' }, + { date: '2025-12-31', cumulatedAmount: '2311.46' }, + ] as MyselfItem[], }, - thisMonthItem: [ - { date: '2026-01-01', cumulatedAmount: '12.5' }, - { date: '2026-01-02', cumulatedAmount: '35.2' }, - { date: '2026-01-03', cumulatedAmount: '52.8' }, - { date: '2026-01-04', cumulatedAmount: '78.1' }, - { date: '2026-01-05', cumulatedAmount: '101' }, - { date: '2026-01-06', cumulatedAmount: '128.4' }, - { date: '2026-01-07', cumulatedAmount: '160.9' }, - { date: '2026-01-08', cumulatedAmount: '190.3' }, - { date: '2026-01-09', cumulatedAmount: '230.7' }, - { date: '2026-01-10', cumulatedAmount: '260.1' }, - { date: '2026-01-11', cumulatedAmount: '300.6' }, - { date: '2026-01-12', cumulatedAmount: '338.9' }, - { date: '2026-01-13', cumulatedAmount: '380.2' }, - { date: '2026-01-14', cumulatedAmount: '420.1' }, - { date: '2026-01-15', cumulatedAmount: '465' }, - { date: '2026-01-16', cumulatedAmount: '520.4' }, - { date: '2026-01-17', cumulatedAmount: '600.9' }, - { date: '2026-01-18', cumulatedAmount: '700.3' }, - { date: '2026-01-19', cumulatedAmount: '780.1' }, - { date: '2026-01-20', cumulatedAmount: '830' }, - ], - prevMonthItem: [ - { date: '2025-12-01', cumulatedAmount: '18' }, - { date: '2025-12-02', cumulatedAmount: '26' }, - { date: '2025-12-03', cumulatedAmount: '41' }, - { date: '2025-12-04', cumulatedAmount: '58' }, - { date: '2025-12-05', cumulatedAmount: '82' }, - { date: '2025-12-06', cumulatedAmount: '103' }, - { date: '2025-12-07', cumulatedAmount: '121' }, - { date: '2025-12-08', cumulatedAmount: '150' }, - { date: '2025-12-09', cumulatedAmount: '190' }, - { date: '2025-12-10', cumulatedAmount: '220' }, - { date: '2025-12-11', cumulatedAmount: '260' }, - { date: '2025-12-12', cumulatedAmount: '300' }, - { date: '2025-12-13', cumulatedAmount: '330' }, - { date: '2025-12-14', cumulatedAmount: '370' }, - { date: '2025-12-15', cumulatedAmount: '420' }, - { date: '2025-12-16', cumulatedAmount: '480' }, - { date: '2025-12-17', cumulatedAmount: '540' }, - { date: '2025-12-18', cumulatedAmount: '610' }, - { date: '2025-12-19', cumulatedAmount: '650' }, - { date: '2025-12-20', cumulatedAmount: '680' }, - { date: '2025-12-21', cumulatedAmount: '710' }, - { date: '2025-12-22', cumulatedAmount: '760' }, - { date: '2025-12-23', cumulatedAmount: '810' }, - { date: '2025-12-24', cumulatedAmount: '860' }, - { date: '2025-12-25', cumulatedAmount: '930' }, - { date: '2025-12-26', cumulatedAmount: '990' }, - { date: '2025-12-27', cumulatedAmount: '1100' }, - { date: '2025-12-28', cumulatedAmount: '1300' }, - { date: '2025-12-29', cumulatedAmount: '1600' }, - { date: '2025-12-30', cumulatedAmount: '2000' }, - { date: '2025-12-31', cumulatedAmount: '2311.46' }, - ], }, compareByCategory: { maxDiffCategoryIndex: 4, @@ -88,45 +95,21 @@ const mockData = { items: [ { categoryIndex: 1, - mySpentAmount: '302.62', - averageSpentAmount: '379.21', + mySpentAmount: '302', + averageSpentAmount: '379', }, { categoryIndex: 3, - mySpentAmount: '210.1', - averageSpentAmount: '180.05', - }, - { - categoryIndex: 4, - mySpentAmount: '480', - averageSpentAmount: '300', - }, - { - categoryIndex: 6, - mySpentAmount: '150', - averageSpentAmount: '220', - }, - { - categoryIndex: 2, - mySpentAmount: '600', - averageSpentAmount: '650', - }, - { - categoryIndex: 5, - mySpentAmount: '280', - averageSpentAmount: '240', - }, - { - categoryIndex: 7, - mySpentAmount: '60', - averageSpentAmount: '55', - }, - { - categoryIndex: 8, - mySpentAmount: '120', - averageSpentAmount: '130', + mySpentAmount: '210', + averageSpentAmount: '180', }, - ] as Array, + { categoryIndex: 4, mySpentAmount: '480', averageSpentAmount: '300' }, + { categoryIndex: 6, mySpentAmount: '150', averageSpentAmount: '220' }, + { categoryIndex: 2, mySpentAmount: '600', averageSpentAmount: '650' }, + { categoryIndex: 5, mySpentAmount: '280', averageSpentAmount: '240' }, + { categoryIndex: 7, mySpentAmount: '60', averageSpentAmount: '55' }, + { categoryIndex: 8, mySpentAmount: '120', averageSpentAmount: '130' }, + ] as CategoryItem[], }, } as const; diff --git a/frontend/src/components/report-page/monthly/ReportMonthly.tsx b/frontend/src/components/report-page/monthly/ReportMonthly.tsx new file mode 100644 index 000000000..4c2a2bdd7 --- /dev/null +++ b/frontend/src/components/report-page/monthly/ReportMonthly.tsx @@ -0,0 +1,17 @@ +import ReportContainer from '@/components/report-page/layout/ReportContainer'; +import ReportContent from '@/components/report-page/layout/ReportContent'; + +const ReportMonthly = () => { + return ( + + +

+ 나랑 같은 국가의 교환학생보다
+ 234달러 덜 썼어요 +

+
+
+ ); +}; + +export default ReportMonthly; diff --git a/frontend/src/components/report-page/myself/ReportLineChart.tsx b/frontend/src/components/report-page/myself/ReportLineChart.tsx new file mode 100644 index 000000000..6a8185509 --- /dev/null +++ b/frontend/src/components/report-page/myself/ReportLineChart.tsx @@ -0,0 +1,79 @@ +import { + buildAreaPath, + buildLinePath, +} from '@/components/report-page/myself/buildPath'; +import { type ChartItem } from '@/components/report-page/reportType'; + +type MonthlyChartData = { + dayCount: number; + items: ChartItem[]; +}; + +interface ReportLineChartProps { + thisMonth: MonthlyChartData; + lastMonth: MonthlyChartData; + maxValue: number; + width?: number; + height?: number; +} + +const ReportLineChart = ({ + thisMonth, + lastMonth, + maxValue, + width = 348, + height = 140, +}: ReportLineChartProps) => { + const maxDay = Math.max(thisMonth.dayCount, lastMonth.dayCount); + + const thisPath = buildLinePath( + thisMonth.items, + width, + height, + maxValue, + maxDay, + ); + const lastLinePath = buildLinePath( + lastMonth.items, + width, + height, + maxValue, + maxDay, + ); + const lastAreaPath = buildAreaPath( + lastMonth.items, + width, + height, + maxValue, + maxDay, + ); + + const thisMonthWidth = (thisMonth.items.length / maxDay) * width; + + return ( + + + + + + + + + + + + + + + ); +}; + +export default ReportLineChart; diff --git a/frontend/src/components/report-page/myself/ReportLineGraph.tsx b/frontend/src/components/report-page/myself/ReportLineGraph.tsx new file mode 100644 index 000000000..ab76f020d --- /dev/null +++ b/frontend/src/components/report-page/myself/ReportLineGraph.tsx @@ -0,0 +1,52 @@ +import ReportLineChart from '@/components/report-page/myself/ReportLineChart'; +import ReportLegend from '@/components/report-page/ReportLegend'; +import { type ChartItem } from '@/components/report-page/reportType'; +import VerticalGrid from '@/components/report-page/VerticalGrid'; + +type MonthlyGraphData = { + label: string; + dayCount: number; + items: ChartItem[]; +}; + +interface ReportLineGraphProps { + thisMonth: MonthlyGraphData; + lastMonth: MonthlyGraphData; + maxValue: number; +} + +const ReportLineGraph = ({ + thisMonth, + lastMonth, + maxValue, +}: ReportLineGraphProps) => { + const maxDays = Math.max(thisMonth.dayCount, lastMonth.dayCount); + + const positions = [0, ((thisMonth.dayCount - 1) / (maxDays - 1)) * 100, 100]; + const labels = ['1일', `${thisMonth.dayCount}일`, `${maxDays}일`]; + + return ( +
+
+ + +
+
+ +
+ +
+
+
+ ); +}; + +export default ReportLineGraph; diff --git a/frontend/src/components/report-page/myself/ReportMyself.tsx b/frontend/src/components/report-page/myself/ReportMyself.tsx new file mode 100644 index 000000000..e9d869989 --- /dev/null +++ b/frontend/src/components/report-page/myself/ReportMyself.tsx @@ -0,0 +1,68 @@ +import ReportContainer from '@/components/report-page/layout/ReportContainer'; +import ReportContent from '@/components/report-page/layout/ReportContent'; +import ReportLineGraph from '@/components/report-page/myself/ReportLineGraph'; +import { useReportContext } from '@/components/report-page/ReportContext'; +import { type ChartItem } from '@/components/report-page/reportType'; + +import { type CountryCode } from '@/data/countryCode'; +import { getCountryInfo } from '@/lib/country'; +import { useAccountBookStore } from '@/stores/useAccountBookStore'; + +type MonthlyData = { + label: string; + dayCount: number; + totalSpent: string; + items: ChartItem[]; +}; + +interface ReportMyselfProps { + data: { + diff: string; + thisMonth: MonthlyData; + lastMonth: MonthlyData; + }; +} + +const ReportMyself = ({ data }: ReportMyselfProps) => { + const { currencyType } = useReportContext(); + const countryCode = useAccountBookStore((state) => + currencyType === 'LOCAL' + ? state.accountBook?.localCountryCode + : state.accountBook?.baseCountryCode, + ) as CountryCode; + + const unit = getCountryInfo(countryCode)?.currencyUnitKor || ''; + + const maxValue = Math.max( + Number(data.lastMonth.totalSpent), + Number(data.thisMonth.totalSpent), + ); + + return ( + + +
+

+ 지난달보다{' '} + + {data.diff} + {unit} 더 + {' '} + 쓰는 중이에요 +

+ + 오늘까지 {data.thisMonth.totalSpent} + {unit} 썼어요 + +
+ +
+
+ ); +}; + +export default ReportMyself; diff --git a/frontend/src/components/report-page/myself/buildPath.ts b/frontend/src/components/report-page/myself/buildPath.ts new file mode 100644 index 000000000..67e4f4cb0 --- /dev/null +++ b/frontend/src/components/report-page/myself/buildPath.ts @@ -0,0 +1,46 @@ +import { type ChartItem } from '@/components/report-page/reportType'; + +const buildLineSegments = ( + data: ChartItem[], + stepX: number, + height: number, + maxValue: number, +) => + data + .map((item, index) => { + const x = stepX * index; + const value = Number(item.cumulatedAmount); + const y = height - (value / maxValue) * height; + + return `${index === 0 ? 'M' : 'L'} ${x} ${y}`; + }) + .join(' '); + +export const buildLinePath = ( + data: ChartItem[], + width: number, + height: number, + maxValue: number, + maxDay: number, +) => { + const stepX = width / (maxDay - 1); + + return buildLineSegments(data, stepX, height, maxValue); +}; + +export const buildAreaPath = ( + data: ChartItem[], + width: number, + height: number, + maxValue: number, + maxDay: number, +) => { + const stepX = width / (maxDay - 1); + const linePath = buildLineSegments(data, stepX, height, maxValue); + + const lastIndex = data.length - 1; + const lastX = stepX * lastIndex; + const closingPath = `L ${lastX} ${height} L 0 ${height} Z`; + + return linePath + ' ' + closingPath; +}; diff --git a/frontend/src/components/report-page/reportType.ts b/frontend/src/components/report-page/reportType.ts new file mode 100644 index 000000000..50d8d72ea --- /dev/null +++ b/frontend/src/components/report-page/reportType.ts @@ -0,0 +1,4 @@ +export type ChartItem = { + date: string; + cumulatedAmount: string; +}; diff --git a/frontend/src/pages/ReportPage.tsx b/frontend/src/pages/ReportPage.tsx index c4c479048..78637cb5c 100644 --- a/frontend/src/pages/ReportPage.tsx +++ b/frontend/src/pages/ReportPage.tsx @@ -2,20 +2,28 @@ import { useState } from 'react'; import ReportCategory from '@/components/report-page/category/ReportCategory'; import mockData from '@/components/report-page/mock'; +import ReportMonthly from '@/components/report-page/monthly/ReportMonthly'; +import ReportMyself from '@/components/report-page/myself/ReportMyself'; import ReportProvider from '@/components/report-page/ReportProvider'; import { type CurrencyType } from '@/types/currency'; const ReportPage = () => { const categoryData = mockData.compareByCategory; + const myselfData = mockData.compareWithLastMonth; const [currencyType, setCurrencyType] = useState('LOCAL'); // @TODO: 드롭다운 추가 예정 return ( -
+
+
+ + +
+
diff --git a/frontend/src/styles/animation.css b/frontend/src/styles/animation.css index 2d40f21bb..6fb56b163 100644 --- a/frontend/src/styles/animation.css +++ b/frontend/src/styles/animation.css @@ -56,10 +56,20 @@ } } + @keyframes draw-path { + from { + stroke-dashoffset: 1000; + } + to { + stroke-dashoffset: 0; + } + } + --animate-float: float 2s ease-in-out infinite; --animate-marquee: marquee 30s linear infinite; --animate-marquee-reverse: marquee-reverse 30s linear infinite; --animate-ripple: ripple 2s ease-in-out infinite; --animate-grow: scale-up 0.2s ease-in-out forwards; --animate-expand-width: expand-width ease-out forwards; + --animate-draw-path: draw-path 2s ease-in-out forwards; }