Files
exo/dashboard/src/app.css
Alex Cheema f370452d7e Better onboarding UX (#1533)
## Summary
- **Complete onboarding wizard**: 7-step flow guiding new users from
Welcome → Your Devices (topology) → Add More Devices (animation) →
Choose Model → Download → Load → Chat
- **Native macOS integration**: NSPopover welcome callout anchored to
menu bar icon on first launch, polished DMG installer with
drag-to-Applications arrow
- **Dashboard UX polish**: auto-download on model select, toast
notifications, connection banner, skeleton loading, download progress in
header, recommended model tags, sidebar hidden in home state for cleaner
first impression
- **Settings & menu bar overhaul**: native Settings window with Advanced
tab, onboarding reset, chat sidebar toggle

## Test plan
- [ ] Fresh install: verify onboarding wizard appears and flows Welcome
→ Topology → Animation → Model → Download → Load → Chat
- [ ] Verify topology shows real device data in onboarding step 2
- [ ] Verify selecting a model in the main dashboard picker
auto-triggers download
- [ ] Verify chat sidebar is hidden on home view, appears when chat is
active
- [ ] Verify DMG installer has white background with curved arrow
- [ ] Verify NSPopover appears anchored to menu bar icon on first launch

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ryuichi Leo Takashige <leo@exolabs.net>
2026-02-23 11:27:28 +00:00

388 lines
8.3 KiB
CSS

@import 'tailwindcss';
@import 'tw-animate-css';
@custom-variant dark (&:is(.dark *));
:root {
/* EXO Brand Colors - Command Center Theme (neutral dark greys) */
--exo-black: oklch(0.12 0 0);
--exo-dark-gray: oklch(0.16 0 0);
--exo-medium-gray: oklch(0.22 0 0);
--exo-light-gray: oklch(0.6 0 0);
--exo-yellow: oklch(0.85 0.18 85);
--exo-yellow-darker: oklch(0.7 0.16 85);
--exo-yellow-glow: oklch(0.9 0.2 85);
/* Gotham-inspired accent colors */
--exo-grid: oklch(0.25 0 0);
--exo-scanline: oklch(0.15 0 0);
--exo-glow-yellow: 0 0 20px oklch(0.85 0.18 85 / 0.3);
--exo-glow-yellow-strong: 0 0 40px oklch(0.85 0.18 85 / 0.5);
/* Theme Variables */
--radius: 0.375rem;
--background: var(--exo-black);
--foreground: oklch(0.9 0 0);
--card: var(--exo-dark-gray);
--card-foreground: oklch(0.9 0 0);
--popover: var(--exo-dark-gray);
--popover-foreground: oklch(0.9 0 0);
--primary: var(--exo-yellow);
--primary-foreground: var(--exo-black);
--secondary: var(--exo-medium-gray);
--secondary-foreground: oklch(0.9 0 0);
--muted: var(--exo-medium-gray);
--muted-foreground: var(--exo-light-gray);
--accent: var(--exo-medium-gray);
--accent-foreground: oklch(0.9 0 0);
--destructive: oklch(0.6 0.25 25);
--border: oklch(0.22 0 0);
--input: oklch(0.22 0 0);
--ring: var(--exo-yellow);
}
@theme inline {
--radius-sm: calc(var(--radius) - 2px);
--radius-md: var(--radius);
--radius-lg: calc(var(--radius) + 2px);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
/* Custom EXO colors */
--color-exo-yellow: var(--exo-yellow);
--color-exo-yellow-darker: var(--exo-yellow-darker);
--color-exo-black: var(--exo-black);
--color-exo-dark-gray: var(--exo-dark-gray);
--color-exo-medium-gray: var(--exo-medium-gray);
--color-exo-light-gray: var(--exo-light-gray);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
html, body {
@apply bg-background text-foreground;
font-family: 'SF Mono', 'Fira Code', 'Monaco', 'Consolas', 'Liberation Mono', monospace;
letter-spacing: 0.02em;
}
}
@layer utilities {
.scrollbar-hide {
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
scrollbar-width: none;
}
/* CRT Scanline effect */
.scanlines {
position: relative;
&::before {
content: '';
position: absolute;
inset: 0;
background: repeating-linear-gradient(
0deg,
transparent,
transparent 2px,
oklch(0 0 0 / 0.03) 2px,
oklch(0 0 0 / 0.03) 4px
);
pointer-events: none;
z-index: 100;
}
}
/* Command panel styling */
.command-panel {
background: linear-gradient(
180deg,
oklch(0.16 0 0 / 0.95) 0%,
oklch(0.12 0 0 / 0.98) 100%
);
border: 1px solid oklch(0.25 0 0);
box-shadow:
inset 0 1px 0 oklch(1 0 0 / 0.03),
0 4px 20px oklch(0 0 0 / 0.5);
}
/* Glow text */
.glow-text {
text-shadow:
0 0 10px oklch(0.85 0.18 85 / 0.5),
0 0 20px oklch(0.85 0.18 85 / 0.3),
0 0 40px oklch(0.85 0.18 85 / 0.1);
}
/* Status indicator pulse */
.status-pulse {
animation: statusPulse 2s ease-in-out infinite;
}
/* Grid background */
.grid-bg {
background-image:
linear-gradient(oklch(0.2 0 0 / 0.3) 1px, transparent 1px),
linear-gradient(90deg, oklch(0.2 0 0 / 0.3) 1px, transparent 1px);
background-size: 40px 40px;
}
}
/* Animations */
@keyframes flowAnimation {
from {
stroke-dashoffset: 0;
}
to {
stroke-dashoffset: -16;
}
}
@keyframes statusPulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
@keyframes radarSweep {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes glowPulse {
0%, 100% {
box-shadow: 0 0 5px oklch(0.85 0.18 85 / 0.3), 0 0 10px oklch(0.85 0.18 85 / 0.1);
}
50% {
box-shadow: 0 0 15px oklch(0.85 0.18 85 / 0.5), 0 0 30px oklch(0.85 0.18 85 / 0.2);
}
}
@keyframes dataPulse {
0%, 100% {
opacity: 0.6;
}
50% {
opacity: 1;
}
}
.graph-link {
stroke: oklch(0.85 0.18 85 / 0.4);
stroke-width: 1.5px;
stroke-dasharray: 8, 8;
animation: flowAnimation 1s linear infinite;
filter: drop-shadow(0 0 3px oklch(0.85 0.18 85 / 0.5));
}
/* Onboarding step 2: connection line between devices */
.onboarding-connection-line {
stroke: oklch(0.85 0.18 85 / 0.5);
stroke-width: 1.5px;
stroke-dasharray: 6, 6;
animation: flowAnimation 1s linear infinite;
filter: drop-shadow(0 0 4px oklch(0.85 0.18 85 / 0.4));
}
/* Onboarding step 4: red connection line for disconnect */
.onboarding-connection-line-red {
stroke: rgba(220, 38, 38, 0.7);
stroke-width: 1.5px;
stroke-dasharray: 6, 6;
filter: drop-shadow(0 0 2px rgba(220, 38, 38, 0.3));
}
.graph-link-active {
stroke: oklch(0.85 0.18 85 / 0.8);
stroke-width: 2px;
filter: drop-shadow(0 0 6px oklch(0.85 0.18 85 / 0.8));
}
/* CRT Screen effect for topology */
.crt-screen {
position: relative;
border-radius: 50%;
background: radial-gradient(
ellipse at center,
oklch(0.16 0 0) 0%,
oklch(0.12 0 0) 50%,
oklch(0.09 0 0) 100%
);
box-shadow:
inset 0 0 100px oklch(0 0 0 / 0.5),
0 0 50px oklch(0.85 0.18 85 / 0.1);
}
/* Data readout styling */
.data-readout {
font-family: 'SF Mono', 'Fira Code', monospace;
font-size: 11px;
letter-spacing: 0.05em;
text-transform: uppercase;
}
/* Terminal cursor blink */
.cursor-blink {
animation: cursorBlink 1s step-end infinite;
}
@keyframes cursorBlink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Custom scrollbar for command center */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: oklch(0.1 0 0);
}
::-webkit-scrollbar-thumb {
background: oklch(0.3 0 0);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: oklch(0.85 0.18 85 / 0.5);
}
/* Remove focus outline/border for inputs */
input:focus, textarea:focus {
outline: none;
box-shadow: none;
}
/* Shooting Stars Animation */
.shooting-stars {
position: fixed;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
}
.shooting-star {
position: absolute;
width: 3px;
height: 3px;
background: oklch(0.85 0.18 85 / 1);
border-radius: 50%;
box-shadow: 0 0 6px oklch(0.85 0.18 85 / 0.8);
animation: shootingStar var(--duration, 3s) linear infinite;
animation-delay: var(--delay, 0s);
opacity: 0;
}
.shooting-star::before {
content: '';
position: absolute;
width: 80px;
height: 2px;
background: linear-gradient(90deg, oklch(0.85 0.18 85 / 0), oklch(0.85 0.18 85 / 0.6));
transform: rotate(45deg);
transform-origin: right center;
top: 0;
right: 2px;
}
@keyframes shootingStar {
0% {
opacity: 0;
transform: translate(0, 0);
}
0.5% {
opacity: 1;
}
2.5% {
opacity: 0.8;
transform: translate(300px, 300px);
}
3.5% {
opacity: 0;
transform: translate(400px, 400px);
}
100% {
opacity: 0;
transform: translate(400px, 400px);
}
}
/* Onboarding smooth fade-in */
@keyframes onb-fade-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
/* Opacity-only fade-in — no transform, safe for containers with fixed-position children */
@keyframes onb-fade-opacity {
from { opacity: 0; }
to { opacity: 1; }
}
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
.shooting-star,
.shooting-star::before {
animation: none !important;
opacity: 0 !important;
}
.graph-link {
animation: none;
}
.status-pulse {
animation: none;
}
.cursor-blink {
animation: none;
}
.onboarding-connection-line {
animation: none;
}
.onboarding-connection-line-red {
animation: none;
}
[style*="onb-fade-in"],
[style*="onb-fade-opacity"] {
animation: none !important;
opacity: 1 !important;
}
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
}
}