Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 84 additions & 32 deletions frontend/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -652,8 +652,15 @@ body {
}

.btn-loader {
width: 1rem;
height: 1rem;
width: 1.125rem;
height: 1.125rem;
animation: spin 0.8s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
.btn-loader {
animation-duration: 2s;
}
}

.connect-wallet-btn {
Expand Down Expand Up @@ -1007,10 +1014,10 @@ body {
}

.skeleton-bone--chart-donut {
width: min(100%, 240px);
height: min(100vw, 240px);
max-width: 240px;
max-height: 240px;
width: min(100%, 260px);
height: min(100vw, 260px);
max-width: 260px;
max-height: 260px;
}

.legend-item {
Expand Down Expand Up @@ -1920,9 +1927,20 @@ h2 {
}

/* ── Skeleton loaders ────────────────────────────────────────── */
/*
* Design rules:
* - Bones sit on top of the same card backgrounds as real content
* (stat-card: rgba(255,255,255,0.03), goal-section: rgba(255,255,255,0.02))
* so the bone fill must be brighter than the card to be visible.
* - Shimmer peak matches the MetricTile inline skeleton for consistency.
* - border-radius per bone type: text lines → 4px, pill/progress → 999px,
* circular → 50%, swatch/icon → 4–8px, card-level → matches real card.
* - Dimensions are locked to the real component measurements to prevent
* layout shift on swap.
*/
.skeleton-bone {
background: rgba(255, 255, 255, 0.03);
border-radius: 12px;
background: rgba(255, 255, 255, 0.07);
border-radius: 4px;
position: relative;
overflow: hidden;
}
Expand All @@ -1931,13 +1949,18 @@ h2 {
margin-bottom: 2.5rem;
}

/* Stat card skeleton — matches .stat-card padding so bones don't sit flush */
.skeleton-stat-card {
gap: 0;
gap: 0.5rem;
padding: 1.5rem;
}

.skeleton-goal-copy {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.4rem;
}

.skeleton-progress-row {
Expand All @@ -1958,89 +1981,119 @@ h2 {
.skeleton-legend-text {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.3rem;
}

/* ── Bone dimensions — locked to real component measurements ── */

/* stat-label: matches .metric-tile__label font-size ~0.75rem → ~12px rendered */
.skeleton-bone--stat-label {
width: 60%;
height: 14px;
margin-bottom: 12px;
width: 55%;
height: 12px;
border-radius: 4px;
}

/* stat-value: matches .metric-tile__value font-size 1.4rem → ~22px rendered */
.skeleton-bone--stat-value {
width: 80%;
height: 36px;
width: 75%;
height: 22px;
border-radius: 4px;
}

/* stat-trend: matches .metric-tile__trend font-size 0.72rem → ~11px */
.skeleton-bone--stat-trend {
width: 35%;
height: 11px;
border-radius: 4px;
}

/* goal-title: matches .goal-title font-size 1.25rem → ~20px */
.skeleton-bone--goal-title {
width: 55%;
height: 18px;
margin-bottom: 8px;
height: 20px;
border-radius: 4px;
}

/* goal-line: matches .text-muted .goal-subtitle font-size 0.9rem → ~14px */
.skeleton-bone--goal-line {
width: 70%;
height: 13px;
margin-bottom: 6px;
height: 14px;
border-radius: 4px;
}

/* goal-line-short: status indicator ~14px tall */
.skeleton-bone--goal-line-short {
width: 40%;
height: 13px;
height: 14px;
border-radius: 4px;
}

/* goal-icon: matches Target size={32} */
.skeleton-bone--goal-icon {
width: 32px;
height: 32px;
border-radius: 50%;
flex-shrink: 0;
}

/* progress bar: matches .progress-bar-container height 12px, border-radius 999px */
.skeleton-bone--progress {
width: 100%;
height: 12px;
border-radius: 999px;
margin: 1rem 0;
}

/* progress stats: matches .progress-stats font-size 0.85rem → ~13px */
.skeleton-bone--progress-stat {
width: 30%;
height: 13px;
border-radius: 4px;
}

/* chart donut: matches PortfolioChart default width/height={320} minus 18px padding
outerRadius = 320/2 - 18 = 142 → full SVG viewBox is 320×320 */
.skeleton-bone--chart-donut {
width: min(100%, 280px);
height: min(100%, 280px);
max-width: 280px;
max-height: 280px;
width: min(100%, 320px);
height: min(100%, 320px);
max-width: 320px;
max-height: 320px;
border-radius: 50%;
margin: 0 auto;
}

/* legend swatch: matches .legend-color 16×16 */
.skeleton-bone--legend-swatch {
width: 16px;
height: 16px;
border-radius: 4px;
flex-shrink: 0;
}

/* legend name: matches .legend-name font-size 0.9rem → ~14px */
.skeleton-bone--legend-name {
width: 60%;
height: 13px;
margin-bottom: 6px;
height: 14px;
border-radius: 4px;
}

/* legend percentage: matches .legend-percentage font-size 0.8rem → ~12px */
.skeleton-bone--legend-pct {
width: 30%;
height: 11px;
height: 12px;
border-radius: 4px;
}

/* ── Shimmer sweep ── */
.skeleton-shimmer {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.08) 50%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%
);
animation: skeleton-sweep 1.6s infinite linear;
Expand Down Expand Up @@ -2520,7 +2573,6 @@ h2 {
white-space: nowrap;
user-select: none;
}
.status-pill__icon { font-size: 0.6rem; line-height: 1; }
.status-pill--success { background: rgba(0,200,100,0.12); color: var(--success, #00c864); border: 1px solid rgba(0,200,100,0.25); }
.status-pill--warning { background: rgba(255,180,0,0.12); color: #ffb400; border: 1px solid rgba(255,180,0,0.25); }
.status-pill--error { background: rgba(255,60,60,0.12); color: #ff4444; border: 1px solid rgba(255,60,60,0.25); }
Expand Down Expand Up @@ -2559,16 +2611,16 @@ h2 {
.metric-tile__trend--flat { color: var(--text-muted, #888); }
.metric-tile__trend-label { font-family: 'JetBrains Mono', monospace; }

/* Loading skeleton for MetricTile */
/* Loading skeleton for MetricTile — uses same bone brightness as SkeletonLoader */
.metric-tile--loading .metric-tile__label-skeleton,
.metric-tile--loading .metric-tile__value-skeleton {
border-radius: 4px;
background: linear-gradient(90deg, rgba(255,255,255,0.05) 25%, rgba(255,255,255,0.1) 50%, rgba(255,255,255,0.05) 75%);
background: linear-gradient(90deg, rgba(255,255,255,0.07) 25%, rgba(255,255,255,0.12) 50%, rgba(255,255,255,0.07) 75%);
background-size: 200% 100%;
animation: shimmer 1.6s ease-in-out infinite;
}
.metric-tile--loading .metric-tile__label-skeleton { height: 0.75rem; width: 60%; margin-bottom: 0.25rem; }
.metric-tile--loading .metric-tile__value-skeleton { height: 1.6rem; width: 80%; }
.metric-tile--loading .metric-tile__label-skeleton { height: 0.75rem; width: 55%; margin-bottom: 0.25rem; }
.metric-tile--loading .metric-tile__value-skeleton { height: 1.4rem; width: 75%; }

/* ── EmptyState (#105) ──────────────────────────────────────────── */
.empty-state {
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/components/features/chat/ChatInterface.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,13 @@ export function ChatInterface({
>
{isConnected ? (
<CheckCircle2
size={12}
size={14}
fill="var(--success)"
color="var(--bg-card)"
aria-hidden="true"
/>
) : (
<AlertCircle size={12} aria-hidden="true" />
<AlertCircle size={14} aria-hidden="true" />
)}{" "}
{isConnected ? connectionCopy.online : connectionCopy.offline}
</motion.div>
Expand All @@ -121,7 +121,7 @@ export function ChatInterface({
animate={{ opacity: 1, y: 0 }}
transition={prefersReduced ? { duration: 0.12 } : { duration: 0.2 }}
>
<AlertCircle size={14} aria-hidden="true" />
<AlertCircle size={16} aria-hidden="true" />
<span>Notification service is offline. You can still draft and send local chat messages.</span>
</motion.div>
)}
Expand Down Expand Up @@ -161,7 +161,7 @@ export function ChatInterface({
animate={{ opacity: 1, x: 0 }}
transition={prefersReduced ? { duration: 0.12 } : { delay: 0.2 }}
>
<AlertCircle size={12} aria-hidden="true" /> Proactive Nudge
<AlertCircle size={14} aria-hidden="true" /> Proactive Nudge
</motion.div>
)}
<div className="message-bubble">
Expand Down Expand Up @@ -230,7 +230,7 @@ export function ChatInterface({
whileTap={prefersReduced ? undefined : { scale: 0.95 }}
transition={prefersReduced ? { duration: 0.01 } : { duration: 0.15 }}
>
<Send size={18} aria-hidden="true" />
<Send size={20} aria-hidden="true" />
</motion.button>
</form>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/components/features/portfolio/GoalTracker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function GoalTracker({
const clampedProgress = Math.max(0, Math.min(100, progressPercentage));

return (
<div className="goal-section skeleton-fade-in">
<div className="goal-section">
<div className="goal-header">
<div>
<h3 className="goal-title">{goalName}</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ function getTone(status: WebSocketConnectionStatus | WalletConnectionStatus) {

function StatusIcon({ tone }: { tone: ReturnType<typeof getTone> }) {
if (tone === "online") {
return <CheckCircle2 size={14} aria-hidden="true" />;
return <CheckCircle2 size={16} aria-hidden="true" />;
}

if (tone === "pending") {
return <Loader2 className="connectivity-widget-spinner" size={14} aria-hidden="true" />;
return <Loader2 className="connectivity-widget-spinner" size={16} aria-hidden="true" />;
}

return <AlertTriangle size={14} aria-hidden="true" />;
return <AlertTriangle size={16} aria-hidden="true" />;
}

export function ConnectivityWidget({
Expand Down Expand Up @@ -109,7 +109,7 @@ export function ConnectivityWidget({
<span className="connectivity-widget-summary">{summary}</span>
<ChevronDown
className={`connectivity-widget-chevron${isOpen ? " connectivity-widget-chevron--open" : ""}`}
size={14}
size={16}
aria-hidden="true"
/>
</motion.button>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/features/wallet/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export const Drawer: React.FC<DrawerProps> = ({ isOpen, onClose, title, children
className="drawer-close-btn"
onClick={onClose}
aria-label="Close drawer"
title="Close drawer"
>
<X size={20} />
</button>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/features/wallet/WalletModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export const WalletModal: React.FC<WalletModalProps> = ({ isOpen, onClose }) =>
className="modal-close-icon"
onClick={onClose}
aria-label="Close dialog"
title="Close dialog"
>
<X size={20} aria-hidden="true" />
</button>
Expand Down
Loading