diff --git a/frontend/src/components/agents/ResearchReportRenderer.tsx b/frontend/src/components/agents/ResearchReportRenderer.tsx index 2c92f4e..75e1c5f 100644 --- a/frontend/src/components/agents/ResearchReportRenderer.tsx +++ b/frontend/src/components/agents/ResearchReportRenderer.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo, useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; @@ -14,9 +14,57 @@ interface Props { searchQuery?: string; } +interface Heading { + level: number; + text: string; + id: string; +} + const ResearchReportRenderer: React.FC = ({ result }) => { const { t } = useTranslation(); const markdown = getMarkdown(result); + const [headings, setHeadings] = useState([]); + const contentRef = useRef(null); + + useEffect(() => { + if (!contentRef.current) return; + + const headingElements = contentRef.current.querySelectorAll('h1, h2, h3, h4, h5, h6'); + const headingList: Heading[] = []; + let h2Count = 0; + let h3Count = 0; + + headingElements.forEach((heading, idx) => { + const level = parseInt(heading.tagName[1]); + const text = heading.textContent || ''; + const id = `heading-${idx}`; + + if (level === 2) { + h2Count++; + h3Count = 0; + heading.textContent = `${h2Count}. ${text}`; + } else if (level === 3) { + h3Count++; + heading.textContent = `${h2Count}.${h3Count} ${text}`; + } + + heading.id = id; + heading.classList.add('report-heading'); + + const link = document.createElement('a'); + link.href = `#${id}`; + link.className = 'heading-anchor'; + link.setAttribute('aria-label', `Link to ${text}`); + link.innerHTML = '🔗'; + heading.appendChild(link); + + if (level <= 3) { + headingList.push({ level, text, id }); + } + }); + + setHeadings(headingList); + }, [markdown]); if (!markdown) { return ( diff --git a/frontend/src/components/wallet/PaymentChart.tsx b/frontend/src/components/wallet/PaymentChart.tsx index 2ed2b42..de9496b 100644 --- a/frontend/src/components/wallet/PaymentChart.tsx +++ b/frontend/src/components/wallet/PaymentChart.tsx @@ -1,4 +1,4 @@ -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, @@ -7,6 +7,7 @@ import { import type { TransactionEvent } from '../../hooks/useTransactionHistory' import { aggregateDailySpend, aggregateByCounterparty } from '../../hooks/useTransactionHistory' import { formatDate } from '../../utils/format' +import { useTheme } from '../../hooks/useTheme' import styles from './PaymentChart.module.css' import { AccessibleChart } from '../common/AccessibleChart' @@ -27,6 +28,12 @@ interface AgentSpendSlicePayload { export function PaymentChart({ transactions }: PaymentChartProps) { const { t, i18n } = useTranslation() + const { effectiveTheme } = useTheme() + const [legendOpen, setLegendOpen] = useState(true) + + const SLICE_COLORS = effectiveTheme === 'dark' ? DARK_COLORS : LIGHT_COLORS + const gridStroke = effectiveTheme === 'dark' ? 'var(--border-color)' : '#e6e9ee' + const textColor = effectiveTheme === 'dark' ? '#f8fafc' : '#0A0E14' const dailySpend = useMemo(() => aggregateDailySpend(transactions, 30), [transactions]) const byAgent = useMemo(() => aggregateByCounterparty(transactions), [transactions]) @@ -41,17 +48,23 @@ export function PaymentChart({ transactions }: PaymentChartProps) { {hasDailySpend ? ( - + formatDate(value, i18n.language).slice(0, 5)} - tick={{ fontSize: 11 }} + tick={{ fontSize: 11, fill: textColor }} interval={4} /> - + [`${value.toFixed(7)} XLM`, t('wallet.chart.spent')]} labelFormatter={(value: string) => formatDate(value, i18n.language)} + contentStyle={{ + backgroundColor: effectiveTheme === 'dark' ? '#1A1F2E' : '#F8FAFC', + border: `1px solid ${effectiveTheme === 'dark' ? '#2A3040' : '#E6E9EE'}`, + borderRadius: '8px', + color: textColor, + }} /> @@ -62,7 +75,22 @@ export function PaymentChart({ transactions }: PaymentChartProps) {
-

{t('wallet.chart.byAgentHeading')}

+
+

{t('wallet.chart.byAgentHeading')}

+ +
{hasBreakdown ? ( { + const location = useLocation() + const scrollPositions = useRef>({}) + + useEffect(() => { + const key = location.pathname + location.search + const scrollContainer = document.querySelector('.main-content') + + if (scrollContainer) { + if (scrollPositions.current[key] !== undefined) { + setTimeout(() => { + scrollContainer.scrollTop = scrollPositions.current[key] + }, 0) + } else { + scrollContainer.scrollTop = 0 + } + } + + return () => { + if (scrollContainer) { + scrollPositions.current[key] = scrollContainer.scrollTop + } + } + }, [location]) + + useEffect(() => { + const main = document.querySelector('main') || document.querySelector('[role="main"]') + if (main) { + main.focus() + } + }, [location.pathname]) +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 7085fb7..9925a1b 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -1,6 +1,7 @@ @import './tokens.css'; @import './animations.css'; @import './micro-interactions.css'; +@import './report.css'; @tailwind base; @tailwind components; diff --git a/frontend/src/styles/micro-interactions.css b/frontend/src/styles/micro-interactions.css index 4a87bf7..ec5f0cf 100644 --- a/frontend/src/styles/micro-interactions.css +++ b/frontend/src/styles/micro-interactions.css @@ -50,7 +50,112 @@ transition-duration: 500ms; } +/* Button press/tap ripple animation */ +.btn-ripple { + position: relative; + overflow: hidden; +} + +.btn-ripple::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + transform: translate(-50%, -50%); + pointer-events: none; +} + +.btn-ripple:active::before { + animation: ripple 0.6s ease-out; +} + +@keyframes ripple { + to { + width: 300px; + height: 300px; + opacity: 0; + } +} + +/* Card hover lift with refined shadow */ +.card-interactive { + transition: transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1), + box-shadow 300ms ease; + cursor: pointer; +} + +.card-interactive:hover, +.card-interactive:focus-within { + transform: translateY(-8px); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3); +} + +/* Navigation link underline grow effect */ +.nav-link { + position: relative; + text-decoration: none; + color: var(--text-secondary); + transition: color 300ms ease; +} + +.nav-link::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 0; + height: 2px; + background: var(--accent-cyan); + transition: width 300ms ease; +} + +.nav-link:hover::after, +.nav-link:focus-visible::after { + width: 100%; +} + +.nav-link:hover, +.nav-link:focus-visible { + color: var(--accent-cyan); +} + +/* Press scale for primary CTAs */ +.btn-primary { + transition: transform 100ms cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.btn-primary:active { + transform: scale(0.98); +} + @media (prefers-reduced-motion: reduce) { + .btn-ripple::before { + display: none; + } + + .btn-ripple:active::before { + animation: none; + } + + .card-interactive, + .nav-link::after, + .btn-primary { + transition: none !important; + } + + .card-interactive:hover, + .card-interactive:focus-within { + transform: none; + } + + .btn-primary:active { + transform: none; + } + .hover-lift, .hover-glow, .hover-scale, diff --git a/frontend/src/styles/report.css b/frontend/src/styles/report.css new file mode 100644 index 0000000..20885c1 --- /dev/null +++ b/frontend/src/styles/report.css @@ -0,0 +1,92 @@ +/* Report heading and TOC styles */ + +.report-heading { + scroll-margin-top: 80px; + position: relative; + padding-right: 28px; +} + +.heading-anchor { + position: absolute; + right: 0; + opacity: 0; + transition: opacity 200ms ease; + text-decoration: none; + font-size: 0.9em; + padding: 4px 8px; + border-radius: 4px; +} + +.report-heading:hover .heading-anchor { + opacity: 1; +} + +.heading-anchor:hover { + background: var(--accent-cyan); + color: var(--bg-primary); +} + +.toc-sidebar { + scrollbar-width: thin; + scrollbar-color: var(--border-color) transparent; +} + +.toc-sidebar::-webkit-scrollbar { + width: 6px; +} + +.toc-sidebar::-webkit-scrollbar-track { + background: transparent; +} + +.toc-sidebar::-webkit-scrollbar-thumb { + background: var(--border-color); + border-radius: 3px; +} + +.toc-sidebar::-webkit-scrollbar-thumb:hover { + background: var(--text-secondary); +} + +/* Print styles */ +@media print { + .toc-sidebar { + display: none; + } + + .report-heading { + padding-right: 0; + break-after: avoid; + } + + .heading-anchor { + display: none; + } + + .markdown-body { + font-size: 12pt; + line-height: 1.6; + } + + .markdown-body h1, + .markdown-body h2, + .markdown-body h3, + .markdown-body h4, + .markdown-body h5, + .markdown-body h6 { + break-after: avoid; + page-break-after: avoid; + } + + .markdown-body p { + orphans: 3; + widows: 3; + } +} + +/* Mobile responsive for TOC */ +@media (max-width: 1024px) { + .toc-sidebar { + display: none; + } +}