Skip to content

🖌️ Dashboard Theme Guide

This guide covers the visual system for the Vue 3 + Vite dashboard app (frontend/). If you’re adding tabs, components, or touching styles, read this first.

All dashboard colors are defined in frontend/src/assets/main.css. Use these variables everywhere — never hardcode hex values in components.

VariableHexRole
--bg-base#0a0f0cPage background. The darkest surface.
--bg-card#141e17Card and panel backgrounds. Raised surface.
--bg-deep#060a07Inset areas, deeper than base. Used sparingly.
--border#1e2e22Default border for cards, dividers, inputs.
--text-1#e8ede9Primary text. Headings, important labels.
--text-2#bec8c0Secondary text. Descriptions, body copy.
--text-3#8a9a8dTertiary text. Timestamps, hints, placeholders.
--text-4#5a6e5eQuaternary text. Disabled states, very subtle labels.
--accent#00e5a0Brand green. Active states, primary buttons, links.
--accent-lt#6efcd0Light mint green. Hover accents, badge text.

Reference CSS variables via inline styles or :style bindings. Don’t rely on Tailwind color classes for brand colors.

<!-- Do this -->
<p style="color: var(--text-2)">Description text</p>
<span :style="{ color: 'var(--accent)' }">Active</span>
<!-- Don't do this -->
<p class="text-green-400">Description text</p>

The .card class is the workhorse container:

.card {
background: var(--bg-card); /* #141e17 */
border: 1px solid var(--border); /* #1e2e22 */
border-radius: 12px;
padding: 16px;
}

Usage:

<div class="card">
<h3 style="color: var(--text-1)">Card Title</h3>
<p style="color: var(--text-2)">Card content goes here.</p>
</div>

Compact number displays used in overview panels:

.stat-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 10px;
padding: 12px 16px;
text-align: center;
}
.stat-card h3 { font-size: 24px; font-weight: 700; }
.stat-card p { font-size: 12px; color: var(--text-3); }

Usage:

<div class="stat-card">
<h3 style="color: var(--accent)">12</h3>
<p>Active Devices</p>
</div>

Three button variants:

/* Default: dark bg, subtle border, accent hover */
.btn {
padding: 6px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
border: 1px solid var(--border);
background: var(--bg-card);
color: var(--text-1);
}
.btn:hover { border-color: var(--accent); }
/* Primary: solid green */
.btn-primary {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.btn-primary:hover { opacity: 0.9; }
/* Small variant */
.btn-sm { padding: 4px 10px; font-size: 12px; }

Usage:

<button class="btn">Secondary Action</button>
<button class="btn btn-primary">Run Skill</button>
<button class="btn btn-sm">Small</button>

Tabs are defined in frontend/src/App.vue as a flat array. The active tab gets an emerald-tinted style.

<button
:class="activeTab === tab.id
? 'border-emerald-500/70 text-emerald-500/80 bg-emerald-500/8'
: 'border-transparent text-slate-500 hover:text-slate-300'">
{{ tab.label }}
</button>

Active tabs show a green bottom border, green text, and a faint green background. Inactive tabs are slate gray with a lighter hover state.

Some tabs are gated behind a premium toggle stored in localStorage:

const PREMIUM_TABS = new Set([
'influencers', 'analytics', 'strategies',
'content', 'generate', 'agent', 'metrics'
])
const showPremium = ref(localStorage.getItem('droidbot-premium') === 'true')
const visibleTabs = computed(() =>
showPremium.value ? tabs : tabs.filter(t => !PREMIUM_TABS.has(t.id))
)
  1. Add the tab entry to the tabs array in App.vue:
const tabs = [
// ... existing tabs
{ id: 'mytab', label: '🔮 My Tab' },
]
  1. Create the view component at frontend/src/views/MyTabView.vue.

  2. Import and register in App.vue:

import MyTabView from '@/views/MyTabView.vue'
  1. Add the conditional render in the template:
<MyTabView v-else-if="activeTab === 'mytab'" />
  1. If the tab should be premium-only, add its ID to PREMIUM_TABS.

The dashboard header shows a 36px ghost mascot image that reflects the current system state. It polls two endpoints every 10 seconds.

const ghostState = ref<'disconnected' | 'idle' | 'working' | 'error'>('disconnected')
const GHOST_IMAGES: Record<string, string> = {
disconnected: '/mascot/33-disconnected.png',
idle: '/mascot/27-idle.png',
working: '/mascot/28-working.png',
error: '/mascot/30-error.png',
}
StateImageCondition
disconnected33-disconnected.pngNo devices returned from /api/phone/devices, or fetch fails entirely.
idle27-idle.pngAt least one device connected, no jobs running in /api/scheduler/queue.
working28-working.pngAt least one device connected AND a job has status === 'running' in the queue.
error30-error.png/api/phone/devices returns a non-OK response.

The ghost image sits in the header next to the “Ghost in the Droid” title:

<img
:src="GHOST_IMAGES[ghostState]"
alt=""
style="width:36px;height:36px;object-fit:contain;transition:opacity 0.3s"
/>

Polling starts on mount and clears on unmount. The 10-second interval is hardcoded — don’t make it faster or the server logs fill up.

When a tab has no data to show, display a centered ghost illustration with a short message. This gives the page personality instead of a blank void.

<div style="display:flex;flex-direction:column;align-items:center;
justify-content:center;padding:5rem 2rem;text-align:center">
<img
src="/mascot/27-idle.png"
alt="Ghost sleeping"
style="width:120px;height:120px;object-fit:contain;
margin-bottom:1.5rem;opacity:0.8"
/>
<h2 style="font-size:1.5rem;font-weight:700;color:var(--text-1);
margin-bottom:0.5rem">
The ghost is sleeping on this one.
</h2>
<p style="color:var(--text-3);max-width:400px;font-size:0.9rem;
line-height:1.6">
Feature description or status update goes here.
</p>
</div>
  • Ghost image: 120px wide, opacity: 0.8 so it doesn’t dominate.
  • Pick a ghost that fits the context. Idle ghost for “coming soon” features. Error ghost for failure states.
  • Heading: short, personality-forward. “The ghost is sleeping on this one.” Not “No data available.”
  • Description: one or two sentences explaining what the tab will do or what went wrong.
  • Optionally include a hint card below the description with a link to relevant docs or a tracking reference.

The dashboard and docs site share the same green-tinted gray palette. The mapping:

Dashboard VariableDocs EquivalentShared Hex
--text-1--sl-color-gray-1#e8ede9
--text-2--sl-color-gray-2#bec8c0
--text-3--sl-color-gray-3#8a9a8d
--accent--sl-color-accent#00e5a0
--accent-lt--sl-color-accent-high#6efcd0

If you change a color in main.css, open the docs site side-by-side and check that the two still feel like one product. The accent green #00e5a0 is the single source of truth — if that ever changes (it shouldn’t), it must change in both frontend/src/assets/main.css and site/src/styles/custom.css simultaneously.

The dashboard uses Tailwind CSS 4 via a single import at the top of main.css:

@import "tailwindcss";
  • Use Tailwind utilities for layout: flex, gap-3, px-6, py-4, grid, rounded-lg, etc.
  • Use CSS variables for colors: Don’t use Tailwind’s built-in color classes (text-green-400, bg-slate-800) for brand colors. Use style="color: var(--accent)" instead. This keeps the palette centralized and avoids drift.
  • Exception: The tab active state uses emerald-500/70 and slate-500 Tailwind classes because they’re close enough to the brand palette for that specific UI element. Don’t expand this pattern to other components.
  • Responsive: Use Tailwind breakpoints (sm:, md:, lg:) for layout shifts. The dashboard is primarily a desktop tool, but tabs should wrap gracefully on smaller screens (flex-wrap is already on the tab bar).

Example: Combining Tailwind Layout with CSS Variables

Section titled “Example: Combining Tailwind Layout with CSS Variables”
<div class="flex items-center gap-3 px-4 py-3 rounded-lg"
style="background: var(--bg-card); border: 1px solid var(--border)">
<span class="text-sm font-semibold" style="color: var(--text-1)">
Device Connected
</span>
<span class="text-xs" style="color: var(--accent)">Online</span>
</div>