refactor: dashboard design critique — eliminate redundancy, differentiate sections
- Remove GreetingStatStrip (duplicated PerformanceCards data) - Strip left-border accent from stat cards (AI slop pattern) - Redesign KnowledgeBaseCards: icon grid → compact row list with icon badges - Redesign TeamSummary: distinct inline-row layout, no longer identical twin - Differentiate hover: stat cards use subtle border-hover, sessions keep springy lift - Add loading skeletons to PerformanceCards, KnowledgeBaseCards, TeamSummary - Add error state to PerformanceCards - Extract timeAgo() to shared lib/timeAgo.ts (replaced 4 duplicates) - Fix Skeleton bg-brand-border (undefined CSS var) → border-default - Fix double text-xs text-[0.5625rem] class conflicts across dashboard Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
**Theme:** Dark mode primary (charcoal palette). Light mode planned but not yet implemented.
|
**Theme:** Dark mode primary (charcoal palette). Light mode planned but not yet implemented.
|
||||||
|
|
||||||
**Accent:** Ember orange (#f97316) — conveys urgency fitting a troubleshooting context. Used sparingly (max 5% of UI). Warning uses yellow (#eab308), not amber, to stay distinct.
|
**Accent:** Electric blue (#60a5fa dark / #2563eb light) — conveys trust, precision, and reliability fitting a troubleshooting tool MSP engineers depend on during outages. Used sparingly (max 5% of UI). Warning uses amber (#fbbf24), info uses cyan (#67e8f9).
|
||||||
|
|
||||||
**Hard rules:** No glassmorphism, no gradient surfaces, no ambient orbs, no backdrop blur, no decorative shadows at rest. Elevation = lighter surface + border, not shadow.
|
**Hard rules:** No glassmorphism, no gradient surfaces, no ambient orbs, no backdrop blur, no decorative shadows at rest. Elevation = lighter surface + border, not shadow.
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,7 @@ import { Clock, ArrowRight, Route, MessageCircle } from 'lucide-react'
|
|||||||
import { aiSessionsApi } from '@/api/aiSessions'
|
import { aiSessionsApi } from '@/api/aiSessions'
|
||||||
import type { AISessionSummary } from '@/types/ai-session'
|
import type { AISessionSummary } from '@/types/ai-session'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { timeAgo } from '@/lib/timeAgo'
|
||||||
function timeAgo(dateStr: string): string {
|
|
||||||
const diffMs = Date.now() - new Date(dateStr).getTime()
|
|
||||||
const minutes = Math.floor(diffMs / 60000)
|
|
||||||
if (minutes < 1) return 'just now'
|
|
||||||
if (minutes < 60) return `${minutes}m ago`
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
if (hours < 24) return `${hours}h ago`
|
|
||||||
return `${Math.floor(hours / 24)}d ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ActiveFlowPilotSessions({ hideHeader = false }: { hideHeader?: boolean }) {
|
export function ActiveFlowPilotSessions({ hideHeader = false }: { hideHeader?: boolean }) {
|
||||||
const [sessions, setSessions] = useState<AISessionSummary[]>([])
|
const [sessions, setSessions] = useState<AISessionSummary[]>([])
|
||||||
@@ -68,7 +59,7 @@ export function ActiveFlowPilotSessions({ hideHeader = false }: { hideHeader?: b
|
|||||||
)}
|
)}
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'font-sans text-xs text-[0.5625rem] uppercase px-1.5 py-0.5 rounded',
|
'font-sans text-[0.5625rem] uppercase px-1.5 py-0.5 rounded',
|
||||||
session.confidence_tier === 'guided' && 'bg-emerald-400/10 text-emerald-400',
|
session.confidence_tier === 'guided' && 'bg-emerald-400/10 text-emerald-400',
|
||||||
session.confidence_tier === 'exploring' && 'bg-amber-400/10 text-amber-400',
|
session.confidence_tier === 'exploring' && 'bg-amber-400/10 text-amber-400',
|
||||||
session.confidence_tier === 'discovery' && 'bg-blue-400/10 text-blue-400',
|
session.confidence_tier === 'discovery' && 'bg-blue-400/10 text-blue-400',
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Network, Code2, ListChecks, ArrowRight } from 'lucide-react'
|
import { Network, Code2, ListChecks, ArrowRight, ChevronRight } from 'lucide-react'
|
||||||
import { sidebarApi } from '@/api'
|
import { sidebarApi } from '@/api'
|
||||||
|
import { Skeleton } from '@/components/ui/Skeleton'
|
||||||
|
|
||||||
export function KnowledgeBaseCards() {
|
export function KnowledgeBaseCards() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [flowCount, setFlowCount] = useState(0)
|
const [flowCount, setFlowCount] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sidebarApi.getStats()
|
sidebarApi.getStats()
|
||||||
.then((stats) => setFlowCount(stats.tree_counts.total))
|
.then((stats) => setFlowCount(stats.tree_counts.total))
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
@@ -33,23 +36,35 @@ export function KnowledgeBaseCards() {
|
|||||||
Browse <ArrowRight size={10} />
|
Browse <ArrowRight size={10} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 divide-x" style={{ borderColor: 'var(--color-border-default)' }}>
|
<div className="py-1">
|
||||||
{items.map((item) => (
|
{loading ? Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-3 px-5 py-3" style={{ borderBottom: i < 2 ? '1px solid var(--color-border-default)' : undefined }}>
|
||||||
|
<Skeleton className="h-8 w-8 rounded-md shrink-0" />
|
||||||
|
<Skeleton className="h-4 flex-1 max-w-24" />
|
||||||
|
<Skeleton className="h-5 w-8" />
|
||||||
|
</div>
|
||||||
|
)) : items.map((item, i) => (
|
||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
onClick={() => navigate(item.href)}
|
onClick={() => navigate(item.href)}
|
||||||
className="flex flex-col items-center gap-2 py-5 rounded-lg hover:bg-card-hover transition-all duration-350"
|
className="flex w-full items-center gap-3 px-5 py-3 text-left hover:bg-[var(--color-bg-card-hover)] transition-colors group"
|
||||||
style={{ transition: 'transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1), background 200ms ease' }}
|
style={{
|
||||||
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)' }}
|
borderBottom: i < items.length - 1 ? '1px solid var(--color-border-default)' : undefined,
|
||||||
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)' }}
|
}}
|
||||||
>
|
>
|
||||||
<item.icon size={20} style={{ color: item.color }} />
|
<span
|
||||||
<p className="font-heading text-xl font-extrabold text-foreground">{item.value}</p>
|
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md"
|
||||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
style={{ backgroundColor: `${item.color}15` }}
|
||||||
{item.label}
|
>
|
||||||
</p>
|
<item.icon size={15} style={{ color: item.color }} />
|
||||||
|
</span>
|
||||||
|
<span className="flex-1 text-sm font-medium text-foreground">{item.label}</span>
|
||||||
|
<span className="font-heading text-base font-bold text-foreground tabular-nums mr-1">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
<ChevronRight size={14} className="text-muted-foreground opacity-0 -translate-x-1 group-hover:opacity-100 group-hover:translate-x-0 transition-all" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
)))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,16 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import { AlertTriangle } from 'lucide-react'
|
import { AlertTriangle } from 'lucide-react'
|
||||||
import { aiSessionsApi } from '@/api/aiSessions'
|
import { aiSessionsApi } from '@/api/aiSessions'
|
||||||
import type { AISessionSummary } from '@/types/ai-session'
|
import type { AISessionSummary } from '@/types/ai-session'
|
||||||
|
import { timeAgo } from '@/lib/timeAgo'
|
||||||
function timeAgo(dateStr: string): string {
|
|
||||||
const diffMs = Date.now() - new Date(dateStr).getTime()
|
|
||||||
const minutes = Math.floor(diffMs / 60000)
|
|
||||||
if (minutes < 1) return 'just now'
|
|
||||||
if (minutes < 60) return `${minutes}m ago`
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
if (hours < 24) return `${hours}h ago`
|
|
||||||
return `${Math.floor(hours / 24)}d ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PendingEscalations() {
|
export function PendingEscalations() {
|
||||||
const [escalations, setEscalations] = useState<AISessionSummary[]>([])
|
const [escalations, setEscalations] = useState<AISessionSummary[]>([])
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { CheckCircle, Clock, TrendingUp, Timer } from 'lucide-react'
|
|||||||
import type { LucideIcon } from 'lucide-react'
|
import type { LucideIcon } from 'lucide-react'
|
||||||
import { sidebarApi } from '@/api'
|
import { sidebarApi } from '@/api'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Skeleton } from '@/components/ui/Skeleton'
|
||||||
|
|
||||||
interface StatCard {
|
interface StatCard {
|
||||||
label: string
|
label: string
|
||||||
@@ -19,6 +20,8 @@ export function PerformanceCards() {
|
|||||||
const [resolved, setResolved] = useState(0)
|
const [resolved, setResolved] = useState(0)
|
||||||
const [active, setActive] = useState(0)
|
const [active, setActive] = useState(0)
|
||||||
const [totalMinutes, setTotalMinutes] = useState(0)
|
const [totalMinutes, setTotalMinutes] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
const [error, setError] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
sidebarApi.getStats()
|
sidebarApi.getStats()
|
||||||
@@ -27,9 +30,31 @@ export function PerformanceCards() {
|
|||||||
setActive(stats.active_count)
|
setActive(stats.active_count)
|
||||||
setTotalMinutes(stats.total_session_minutes_today)
|
setTotalMinutes(stats.total_session_minutes_today)
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => setError(true))
|
||||||
|
.finally(() => setLoading(false))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||||
|
{Array.from({ length: 4 }).map((_, i) => (
|
||||||
|
<div key={i} className="card-flat p-4 space-y-2">
|
||||||
|
<Skeleton className="h-3 w-2/3" />
|
||||||
|
<Skeleton className="h-7 w-1/2" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="card-flat p-4 text-center">
|
||||||
|
<p className="text-sm text-muted-foreground">Unable to load performance data</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const avgMttr = resolved > 0 ? Math.round(totalMinutes / resolved) : 0
|
const avgMttr = resolved > 0 ? Math.round(totalMinutes / resolved) : 0
|
||||||
|
|
||||||
const cards: StatCard[] = [
|
const cards: StatCard[] = [
|
||||||
@@ -70,14 +95,13 @@ export function PerformanceCards() {
|
|||||||
<button
|
<button
|
||||||
key={card.label}
|
key={card.label}
|
||||||
onClick={() => navigate(card.href)}
|
onClick={() => navigate(card.href)}
|
||||||
className="card-interactive p-4 text-left fade-in"
|
className="card-flat p-4 text-left fade-in hover:border-[var(--color-border-hover)] transition-colors cursor-pointer"
|
||||||
style={{
|
style={{
|
||||||
animationDelay: `${400 + i * 60}ms`,
|
animationDelay: `${400 + i * 60}ms`,
|
||||||
borderLeft: `3px solid ${card.iconColor}`,
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
<p className="font-sans text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
||||||
{card.label}
|
{card.label}
|
||||||
</p>
|
</p>
|
||||||
<card.icon size={14} style={{ color: card.iconColor }} />
|
<card.icon size={14} style={{ color: card.iconColor }} />
|
||||||
|
|||||||
@@ -3,18 +3,7 @@ import { Link, useNavigate } from 'react-router-dom'
|
|||||||
import { CheckCircle, AlertTriangle, XCircle, ArrowRight, MessageCircle } from 'lucide-react'
|
import { CheckCircle, AlertTriangle, XCircle, ArrowRight, MessageCircle } from 'lucide-react'
|
||||||
import { aiSessionsApi } from '@/api/aiSessions'
|
import { aiSessionsApi } from '@/api/aiSessions'
|
||||||
import type { AISessionSummary } from '@/types/ai-session'
|
import type { AISessionSummary } from '@/types/ai-session'
|
||||||
|
import { timeAgo } from '@/lib/timeAgo'
|
||||||
function timeAgo(dateStr: string): string {
|
|
||||||
const diffMs = Date.now() - new Date(dateStr).getTime()
|
|
||||||
const minutes = Math.floor(diffMs / 60000)
|
|
||||||
if (minutes < 1) return 'just now'
|
|
||||||
if (minutes < 60) return `${minutes}m ago`
|
|
||||||
const hours = Math.floor(minutes / 60)
|
|
||||||
if (hours < 24) return `${hours}h ago`
|
|
||||||
const days = Math.floor(hours / 24)
|
|
||||||
if (days === 1) return 'yesterday'
|
|
||||||
return `${days}d ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_CONFIG: Record<string, { icon: typeof CheckCircle; color: string }> = {
|
const STATUS_CONFIG: Record<string, { icon: typeof CheckCircle; color: string }> = {
|
||||||
resolved: { icon: CheckCircle, color: '#34d399' },
|
resolved: { icon: CheckCircle, color: '#34d399' },
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { Users, AlertTriangle, Activity, ArrowRight } from 'lucide-react'
|
import { Users, AlertTriangle, Activity, ArrowRight } from 'lucide-react'
|
||||||
import { usePermissions } from '@/hooks/usePermissions'
|
import { usePermissions } from '@/hooks/usePermissions'
|
||||||
import { aiSessionsApi } from '@/api/aiSessions'
|
import { aiSessionsApi } from '@/api/aiSessions'
|
||||||
|
import { Skeleton } from '@/components/ui/Skeleton'
|
||||||
|
|
||||||
export function TeamSummary() {
|
export function TeamSummary() {
|
||||||
const { isAccountOwner } = usePermissions()
|
const { isAccountOwner } = usePermissions()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const [escalationCount, setEscalationCount] = useState(0)
|
const [escalationCount, setEscalationCount] = useState(0)
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isAccountOwner) return
|
if (!isAccountOwner) { setLoading(false); return }
|
||||||
aiSessionsApi.getEscalationQueue()
|
aiSessionsApi.getEscalationQueue()
|
||||||
.then((esc) => setEscalationCount(esc.length))
|
.then((esc) => setEscalationCount(esc.length))
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
|
.finally(() => setLoading(false))
|
||||||
}, [isAccountOwner])
|
}, [isAccountOwner])
|
||||||
|
|
||||||
if (!isAccountOwner) return null
|
if (!isAccountOwner) return null
|
||||||
@@ -38,23 +41,28 @@ export function TeamSummary() {
|
|||||||
Manage <ArrowRight size={10} />
|
Manage <ArrowRight size={10} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-3 divide-x" style={{ borderColor: 'var(--color-border-default)' }}>
|
<div className="p-4 space-y-3">
|
||||||
{items.map((item) => (
|
{loading ? Array.from({ length: 3 }).map((_, i) => (
|
||||||
|
<div key={i} className="flex items-center gap-3 px-3 py-2.5">
|
||||||
|
<Skeleton className="h-4 w-4 rounded shrink-0" />
|
||||||
|
<Skeleton className="h-4 flex-1 max-w-28" />
|
||||||
|
<Skeleton className="h-5 w-8" />
|
||||||
|
</div>
|
||||||
|
)) : items.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
onClick={() => navigate(item.href)}
|
onClick={() => navigate(item.href)}
|
||||||
className="flex flex-col items-center gap-2 py-5 rounded-lg hover:bg-card-hover transition-all duration-350"
|
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 hover:bg-[var(--color-bg-card-hover)] transition-colors group"
|
||||||
style={{ transition: 'transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1), background 200ms ease' }}
|
|
||||||
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)' }}
|
|
||||||
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)' }}
|
|
||||||
>
|
>
|
||||||
<item.icon size={20} style={{ color: item.color }} />
|
<item.icon size={15} style={{ color: item.color }} className="shrink-0" />
|
||||||
<p className="font-heading text-xl font-extrabold text-foreground">{item.value}</p>
|
<span className="flex-1 text-left text-[0.8125rem] text-muted-foreground group-hover:text-foreground transition-colors">
|
||||||
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
|
|
||||||
{item.label}
|
{item.label}
|
||||||
</p>
|
</span>
|
||||||
|
<span className="font-heading text-lg font-bold text-foreground tabular-nums">
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
)))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -10,14 +10,7 @@ import {
|
|||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { notificationsApi } from '@/api/notifications'
|
import { notificationsApi } from '@/api/notifications'
|
||||||
import type { AppNotification } from '@/types/notification'
|
import type { AppNotification } from '@/types/notification'
|
||||||
|
import { timeAgo } from '@/lib/timeAgo'
|
||||||
function timeAgo(dateStr: string): string {
|
|
||||||
const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 1000)
|
|
||||||
if (diff < 60) return 'just now'
|
|
||||||
if (diff < 3600) return `${Math.floor(diff / 60)}m ago`
|
|
||||||
if (diff < 86400) return `${Math.floor(diff / 3600)}h ago`
|
|
||||||
return `${Math.floor(diff / 86400)}d ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
function EventIcon({ event }: { event: string }) {
|
function EventIcon({ event }: { event: string }) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export function Skeleton({ className, ...props }: SkeletonProps) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'animate-pulse rounded-lg bg-brand-border',
|
'animate-pulse rounded-lg bg-[var(--color-border-default)]',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
14
frontend/src/lib/timeAgo.ts
Normal file
14
frontend/src/lib/timeAgo.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Formats a date string as a relative time (e.g., "5m ago", "2h ago", "yesterday").
|
||||||
|
*/
|
||||||
|
export function timeAgo(dateStr: string): string {
|
||||||
|
const diffMs = Date.now() - new Date(dateStr).getTime()
|
||||||
|
const minutes = Math.floor(diffMs / 60000)
|
||||||
|
if (minutes < 1) return 'just now'
|
||||||
|
if (minutes < 60) return `${minutes}m ago`
|
||||||
|
const hours = Math.floor(minutes / 60)
|
||||||
|
if (hours < 24) return `${hours}h ago`
|
||||||
|
const days = Math.floor(hours / 24)
|
||||||
|
if (days === 1) return 'yesterday'
|
||||||
|
return `${days}d ago`
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import { ActiveFlowPilotSessions } from '@/components/dashboard/ActiveFlowPilotS
|
|||||||
import { PerformanceCards } from '@/components/dashboard/PerformanceCards'
|
import { PerformanceCards } from '@/components/dashboard/PerformanceCards'
|
||||||
import { KnowledgeBaseCards } from '@/components/dashboard/KnowledgeBaseCards'
|
import { KnowledgeBaseCards } from '@/components/dashboard/KnowledgeBaseCards'
|
||||||
import { TeamSummary } from '@/components/dashboard/TeamSummary'
|
import { TeamSummary } from '@/components/dashboard/TeamSummary'
|
||||||
import { GreetingStatStrip } from '@/components/dashboard/GreetingStatStrip'
|
|
||||||
|
|
||||||
function SectionLabel({ children, action }: { children: React.ReactNode; action?: React.ReactNode }) {
|
function SectionLabel({ children, action }: { children: React.ReactNode; action?: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
@@ -37,18 +36,14 @@ export function QuickStartPage() {
|
|||||||
<div className="overflow-y-auto h-full">
|
<div className="overflow-y-auto h-full">
|
||||||
<PageMeta title="ResolutionFlow" />
|
<PageMeta title="ResolutionFlow" />
|
||||||
<div className="max-w-4xl mx-auto px-6 pt-12 pb-12">
|
<div className="max-w-4xl mx-auto px-6 pt-12 pb-12">
|
||||||
{/* Hero: Greeting + Stat Strip */}
|
{/* Hero: Greeting */}
|
||||||
<div className="flex items-end justify-between mb-8 animate-fade-in-up">
|
<div className="mb-8 animate-fade-in-up">
|
||||||
<div>
|
<p className="font-sans text-xs uppercase tracking-[0.12em] text-muted-foreground mb-1">
|
||||||
<p className="font-sans text-xs uppercase tracking-[0.12em] text-muted-foreground mb-1">
|
{dayOfWeek}, {formattedDate}
|
||||||
{dayOfWeek}, {formattedDate}
|
</p>
|
||||||
</p>
|
<h1 className="font-heading text-3xl sm:text-4xl font-extrabold tracking-tight text-[#f0f2f5] leading-tight">
|
||||||
<h1 className="font-heading text-3xl sm:text-4xl font-extrabold tracking-tight text-[#f0f2f5] leading-tight">
|
Good {greeting}, {firstName}.
|
||||||
Good {greeting},<br className="hidden sm:block" />
|
</h1>
|
||||||
{firstName}.
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
<GreetingStatStrip />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat-style input */}
|
{/* Chat-style input */}
|
||||||
|
|||||||
Reference in New Issue
Block a user