feat(dashboard): premium icons, rail stretch, attention as own row

Iteration 008 per Eric's iteration-007 feedback.

== Premium header icons ==

Replaced emoji glyphs in the action row with Hugeicons stroke
icons + better button chrome:

- New Chat: BubbleChatAddIcon on a real accent gradient button
  (was translucent tinted background) with inset highlight, soft
  drop shadow, and an animated overlay sheen on hover.
- Terminal: ConsoleIcon
- Skills: PuzzleIcon
- Edit: Edit02Icon \u2192 CheckmarkCircle02Icon when active
- Settings: Settings02Icon

SecondaryAction component now takes a Hugeicons icon (not an emoji
string), uses a subtle card gradient background, accent-colored
icon on hover, and matching uppercase tracking for visual unity
with the primary New Chat button.

Edit + Settings icon-only buttons get the same treatment so the
whole right-hand cluster reads as one premium control group.

== Side rail height/balance ==

- Achievements: query bumped to `achievements=5` (was 3) and the
  card renders every unlock the aggregator returns. Switched from
  compact \u2192 full-detail rows so the card has body. Together this
  fills the gap between Top Models and the rest of the rail.
- Side rail container: `min-h-full` so the column stretches to
  Sessions Intelligence height instead of collapsing to content.
- Mix & rhythm card: `flex-1 h-full` + `justify-between` so it
  consumes the remaining vertical space at the bottom of the rail
  and aligns flush with Sessions Intelligence's lower edge.

== Attention bar separated ==

Eric: 'is it cluttered or should be higher or lower / separate?'

- Lifted the AttentionMarquee out of OpsStrip into its own
  dedicated row above the gateway strip. Now a self-contained
  warning-tinted chamber with its own border + gradient. Clearly
  reads as 'things to look at' separate from 'gateway is up'.
- OpsStrip reverted to its single-row layout (no more nested
  vertical stack).
- When there are no incidents, the row simply doesn't render \u2014
  no empty frame.

Build/tests:
- pnpm exec vitest run src/server/dashboard-aggregator.test.ts (12/12)
- pnpm build (passes)
- tsc clean for src/screens/dashboard/* and src/server/dashboard*
This commit is contained in:
Aurora release bot
2026-05-02 19:57:06 -04:00
parent fa55ff0f78
commit 123f22b6a1
4 changed files with 97 additions and 56 deletions

View File

@@ -166,7 +166,7 @@ export function AchievementsCard({
{achievements.totalUnlocked} unlocked · view all
</button>
</div>
<div className="space-y-1">
<div className="flex flex-col gap-1.5">
{achievements.recentUnlocks.length === 0 ? (
<div
className="py-3 text-center text-[11px]"
@@ -175,8 +175,11 @@ export function AchievementsCard({
No unlocks yet keep working.
</div>
) : (
// Render every unlock the aggregator returns so the card
// grows to consume vertical space (Eric's iter-007 ask).
// Default count is now 5 so the rail has more presence.
achievements.recentUnlocks.map((unlock) => (
<AchievementRow key={unlock.id} unlock={unlock} compact />
<AchievementRow key={unlock.id} unlock={unlock} />
))
)}
</div>

View File

@@ -1,7 +1,6 @@
import { useNavigate } from '@tanstack/react-router'
import { cn } from '@/lib/utils'
import type { DashboardOverview } from '@/server/dashboard-aggregator'
import { AttentionMarquee } from './attention-marquee'
function formatPulse(iso: string | null): string {
if (!iso) return '—'
@@ -86,17 +85,10 @@ export function OpsStrip({
status,
cron,
platforms,
overview,
}: {
status: DashboardOverview['status']
cron: DashboardOverview['cron']
platforms: DashboardOverview['platforms']
/**
* Full overview is needed for the embedded attention marquee.
* Optional so the strip remains usable as a standalone component
* elsewhere (e.g. in tests or smaller screens that omit incidents).
*/
overview?: DashboardOverview | null
}) {
const navigate = useNavigate()
if (!status) return null
@@ -115,18 +107,11 @@ export function OpsStrip({
const next = cron ? formatNextRun(cron.nextRunAt) : null
const incidentCount = overview?.incidents?.length ?? 0
return (
<div
className="flex flex-col gap-2 rounded-md border bg-[var(--theme-card)]/50 px-3 py-2"
className="flex flex-col gap-2 rounded-md border bg-[var(--theme-card)]/50 px-3 py-2 lg:flex-row lg:items-center lg:justify-between lg:gap-4"
style={{ borderColor: 'var(--theme-border)' }}
>
{/* Top row: gateway state on the left, attention marquee in the
middle (only when there are incidents), platforms+cron on
the right. Falls back to a single-column stack on narrow
viewports. */}
<div className="flex flex-col gap-2 lg:flex-row lg:items-center lg:justify-between lg:gap-4">
{/* Gateway block: state + version + active agents */}
<div className="flex items-center gap-3 text-[11px]">
<span className="flex items-center gap-2">
@@ -274,13 +259,6 @@ export function OpsStrip({
)
})() : null}
</div>
</div>
{/* Attention marquee row — dedicated row when present so the
ticker has full width to breathe; collapses to nothing when
the gateway is all-clear. */}
{incidentCount > 0 && overview ? (
<AttentionMarquee overview={overview} />
) : null}
</div>
)
}

View File

@@ -101,7 +101,7 @@ export function TokenMixHourCard({
return (
<div
className="relative flex flex-col gap-3 overflow-hidden rounded-xl border p-3"
className="relative flex h-full flex-1 flex-col justify-between gap-3 overflow-hidden rounded-xl border p-3"
style={{
background:
'linear-gradient(150deg, color-mix(in srgb, var(--theme-card) 96%, transparent), color-mix(in srgb, var(--theme-card) 92%, transparent))',

View File

@@ -18,6 +18,7 @@ import {
import { SkillsUsageCard } from './components/skills-usage-card'
import { TokenMixHourCard } from './components/token-mix-hour-card'
import { ActiveModelKpi } from './components/active-model-kpi'
import { AttentionMarquee } from './components/attention-marquee'
import { WidgetShell } from './components/widget-shell'
import { EditModePanel } from './components/edit-mode-panel'
import { useDashboardLayout } from './lib/use-dashboard-layout'
@@ -38,7 +39,20 @@ import { cn } from '@/lib/utils'
import { openHamburgerMenu } from '@/components/mobile-hamburger-menu'
import { applyTheme, useSettingsStore } from '@/hooks/use-settings'
import { HugeiconsIcon } from '@hugeicons/react'
import { Moon02Icon, Sun02Icon } from '@hugeicons/core-free-icons'
import {
Moon02Icon,
Sun02Icon,
BubbleChatAddIcon,
ConsoleIcon,
PuzzleIcon,
Edit02Icon,
CheckmarkCircle02Icon,
Settings02Icon,
} from '@hugeicons/core-free-icons'
// `IconSvgObject` isn't exported from @hugeicons/react; reuse the
// inferred type from a real icon import for prop typing.
type HugeIcon = typeof Settings02Icon
// ── Helpers ──────────────────────────────────────────────────────
@@ -470,7 +484,7 @@ function SecondaryAction({
disabled,
}: {
label: string
icon: string
icon: HugeIcon
onClick: () => void
disabled?: boolean
}) {
@@ -479,13 +493,20 @@ function SecondaryAction({
type="button"
onClick={onClick}
disabled={disabled}
className="flex items-center gap-1.5 rounded-lg border px-2.5 py-2 text-xs font-medium transition-colors hover:bg-[var(--theme-card)]/80 disabled:cursor-not-allowed disabled:opacity-50"
className="group inline-flex items-center gap-1.5 rounded-lg border px-3 py-2 text-xs font-semibold uppercase tracking-[0.05em] transition-all hover:scale-[1.015] hover:bg-[var(--theme-card)]/70 hover:text-[var(--theme-text)] active:scale-[0.99] disabled:cursor-not-allowed disabled:opacity-50"
style={{
borderColor: 'var(--theme-border)',
color: 'var(--theme-muted)',
background:
'linear-gradient(135deg, color-mix(in srgb, var(--theme-card) 80%, transparent), transparent)',
}}
>
<span aria-hidden>{icon}</span>
<HugeiconsIcon
icon={icon}
size={14}
strokeWidth={1.6}
className="transition-colors group-hover:text-[var(--theme-accent)]"
/>
<span>{label}</span>
</button>
)
@@ -783,8 +804,10 @@ export function DashboardScreen() {
const overviewQuery = useQuery<DashboardOverview>({
queryKey: ['dashboard', 'overview', period],
queryFn: async () => {
// achievements=5 (instead of 3) gives the Achievements rail
// card enough vertical mass to fill the gap below Top Models.
const res = await fetch(
`/api/dashboard/overview?days=${period}`,
`/api/dashboard/overview?days=${period}&achievements=5`,
)
if (!res.ok) throw new Error(`overview ${res.status}`)
return (await res.json()) as DashboardOverview
@@ -882,24 +905,36 @@ export function DashboardScreen() {
params: { sessionKey: 'new' },
})
}
className="flex items-center gap-2 rounded-lg border px-3 py-2 text-sm font-semibold transition-all hover:scale-[1.02] active:scale-[0.99]"
className="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg px-3.5 py-2 text-sm font-semibold uppercase tracking-[0.05em] transition-all hover:scale-[1.02] active:scale-[0.99]"
style={{
background: `linear-gradient(135deg, ${palette.accent}22, ${palette.accentSecondary}22)`,
borderColor: 'color-mix(in srgb, var(--theme-accent) 40%, transparent)',
color: 'var(--theme-text)',
background: `linear-gradient(135deg, ${palette.accent}, ${palette.accentSecondary})`,
color: 'var(--theme-on-accent, white)',
boxShadow: `0 6px 18px -8px ${palette.accent}aa, inset 0 1px 0 0 rgba(255,255,255,0.18)`,
}}
>
<span aria-hidden>💬</span>
<span
aria-hidden
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100"
style={{
background:
'linear-gradient(135deg, rgba(255,255,255,0.15), transparent 60%)',
}}
/>
<HugeiconsIcon
icon={BubbleChatAddIcon}
size={16}
strokeWidth={1.8}
/>
<span>New Chat</span>
</button>
<SecondaryAction
label="Terminal"
icon="💻"
icon={ConsoleIcon}
onClick={() => navigate({ to: '/terminal' })}
/>
<SecondaryAction
label="Skills"
icon="🧩"
icon={PuzzleIcon}
onClick={() => navigate({ to: '/skills' })}
disabled={!skillsAvailable}
/>
@@ -911,46 +946,62 @@ export function DashboardScreen() {
aria-label={layout.editMode ? 'Done editing layout' : 'Edit layout'}
title={layout.editMode ? 'Done editing layout' : 'Edit layout'}
onClick={layout.toggleEdit}
className="flex size-9 items-center justify-center rounded-lg border transition-colors hover:bg-[var(--theme-card)]/80"
className="inline-flex size-9 items-center justify-center rounded-lg border transition-all hover:scale-[1.05] hover:bg-[var(--theme-card)]/70"
style={{
borderColor: layout.editMode
? 'var(--theme-accent)'
: 'var(--theme-border)',
background: layout.editMode
? 'color-mix(in srgb, var(--theme-accent) 14%, transparent)'
: 'transparent',
: 'linear-gradient(135deg, color-mix(in srgb, var(--theme-card) 80%, transparent), transparent)',
color: layout.editMode
? 'var(--theme-accent)'
: 'var(--theme-muted)',
}}
>
{layout.editMode ? '✓' : '✏️'}
<HugeiconsIcon
icon={layout.editMode ? CheckmarkCircle02Icon : Edit02Icon}
size={15}
strokeWidth={1.7}
/>
</button>
<button
type="button"
aria-label="Settings"
title="Settings"
onClick={() => navigate({ to: '/settings', search: {} })}
className="flex size-9 items-center justify-center rounded-lg border transition-colors hover:bg-[var(--theme-card)]/80"
className="inline-flex size-9 items-center justify-center rounded-lg border transition-all hover:scale-[1.05] hover:bg-[var(--theme-card)]/70 hover:text-[var(--theme-text)]"
style={{
borderColor: 'var(--theme-border)',
color: 'var(--theme-muted)',
background:
'linear-gradient(135deg, color-mix(in srgb, var(--theme-card) 80%, transparent), transparent)',
}}
>
<HugeiconsIcon
icon={Settings02Icon}
size={15}
strokeWidth={1.7}
/>
</button>
</div>
</div>
{/* ── Ops strip (gateway + version drift + platforms + cron pulse).
Now also hosts the right-to-left Attention marquee per
Eric's iteration-006 ask, replacing the standalone
AttentionCard in the side rail. ── */}
{/* ── Attention marquee ──
Iteration 008: lifted *out* of the OpsStrip into its own
dedicated row above it. Fixed Eric's 'feels cluttered'
concern by giving the ticker its own visual chamber
(warning gradient, separated border) so it doesn't blend
into the gateway/version/cron line below it. */}
{(overview?.incidents?.length ?? 0) > 0 ? (
<AttentionMarquee overview={overview ?? null} />
) : null}
{/* ── Ops strip (gateway + version drift + platforms + cron pulse). ── */}
<OpsStrip
status={overview?.status ?? null}
cron={overview?.cron ?? null}
platforms={overview?.platforms ?? []}
overview={overview ?? null}
/>
{/* ── Hero Metrics: 3 analytics tiles + Active Model KPI in slot 4 ── */}
@@ -1033,8 +1084,11 @@ export function DashboardScreen() {
{/* Side rail. Achievements is now first (sits beside Top Models
visually since the rail is right of the chart row + sessions),
then Skills, then the rhythm card. Mix & rhythm is the unique
chart in this column — keeping it. */}
<div className="flex flex-col gap-3 lg:col-span-4">
chart in this column — keeping it.
`min-h-full` + the trailing `flex-1` rhythm card together
stretch the rail to match Sessions Intelligence height so
we don't get the dangling gap Eric flagged in iter 007. */}
<div className="flex min-h-full flex-col gap-3 lg:col-span-4">
<WidgetShell id="achievements" layout={layout}>
<AchievementsCard
achievements={overview?.achievements ?? null}
@@ -1047,12 +1101,18 @@ export function DashboardScreen() {
onOpen={() => navigate({ to: '/skills' })}
/>
</WidgetShell>
<WidgetShell id="mix_rhythm" layout={layout}>
<TokenMixHourCard
analytics={overview?.analytics ?? null}
sessions={sessionRows}
/>
</WidgetShell>
{/* `flex-1` here pushes the rhythm card to consume any
remaining vertical space so the rail's bottom aligns
with Sessions Intelligence. The card itself uses
h-full + flex-1 to honor the stretch. */}
<div className="flex min-h-0 flex-1 flex-col">
<WidgetShell id="mix_rhythm" layout={layout}>
<TokenMixHourCard
analytics={overview?.analytics ?? null}
sessions={sessionRows}
/>
</WidgetShell>
</div>
</div>
</div>
</div>