Skip to content
Merged
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
642 changes: 642 additions & 0 deletions public/dashboard/cost.js

Large diffs are not rendered by default.

160 changes: 151 additions & 9 deletions public/dashboard/dashboard.css
Original file line number Diff line number Diff line change
Expand Up @@ -2089,17 +2089,159 @@ body {
line-height: 1.5;
}

/* ---- .dash-chart: generic SVG chart primitive. Used by Cost for the
stacked bar; Evolution reuses it as a sparkline. Series fills come
from [data-series-idx] below so dark/light themes track automatically. */
.dash-chart {
position: relative;
padding: var(--space-4);
border: 1px solid var(--color-base-300);
border-radius: var(--radius-md);
background: var(--color-base-200);
margin-bottom: var(--space-5);
}
.dash-chart-header {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: var(--space-3);
margin-bottom: var(--space-3);
}
.dash-chart-title { font-size: 13px; font-weight: 600; margin: 0; }
.dash-chart-scroll { overflow-x: auto; overflow-y: hidden; }
.dash-chart-svg {
display: block;
width: 100%;
/* Floor at 540px so narrow viewports trigger the .dash-chart-scroll
horizontal scroll instead of squashing the bars and tick labels via
preserveAspectRatio="none". */
min-width: 540px;
height: auto;
font-family: 'JetBrains Mono', ui-monospace, monospace;
}
.dash-chart-axis { stroke: color-mix(in oklab, var(--color-base-content) 22%, transparent); stroke-width: 1; }
.dash-chart-gridline {
stroke: color-mix(in oklab, var(--color-base-content) 10%, transparent);
stroke-width: 1;
stroke-dasharray: 2 3;
}
.dash-chart-tick-label { fill: color-mix(in oklab, var(--color-base-content) 55%, transparent); font-size: 10px; }
.dash-chart-bar { cursor: pointer; transition: filter var(--motion-fast) var(--ease-out); }
.dash-chart-bar:hover { filter: brightness(1.15); }
.dash-chart-tooltip {
position: absolute;
background: var(--color-base-content);
color: var(--color-base-100);
font-family: Inter, system-ui, sans-serif;
font-size: 11.5px;
line-height: 1.45;
padding: 8px 10px;
border-radius: var(--radius-sm);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2);
pointer-events: none;
opacity: 0;
transform: translate(-50%, calc(-100% - 8px));
transition: opacity var(--motion-fast) var(--ease-out);
white-space: nowrap;
z-index: 5;
max-width: 280px;
}
.dash-chart-tooltip[data-visible="true"] { opacity: 1; }
.dash-chart-tooltip-title { font-weight: 600; margin: 0 0 4px; font-size: 11px; letter-spacing: 0.02em; }
.dash-chart-tooltip-row { display: flex; align-items: center; gap: 6px; margin: 2px 0; }
.dash-chart-tooltip-swatch, .dash-breakdown-swatch {
width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0;
}
.dash-chart-tooltip-swatch { width: 8px; height: 8px; }
.dash-chart-tooltip-total {
margin-top: 4px; padding-top: 4px; font-weight: 600;
border-top: 1px solid color-mix(in oklab, var(--color-base-100) 25%, transparent);
}
.dash-chart-empty {
padding: var(--space-8) var(--space-5);
text-align: center;
color: color-mix(in oklab, var(--color-base-content) 55%, transparent);
font-size: 12.5px;
}
.dash-chart-skeleton {
height: 240px;
border-radius: var(--radius-sm);
background: linear-gradient(90deg,
var(--color-base-300) 25%,
color-mix(in oklab, var(--color-base-300) 50%, transparent) 50%,
var(--color-base-300) 75%);
background-size: 200% 100%;
animation: dash-shimmer 1.5s infinite;
}
.dash-breakdown-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-4);
margin-bottom: var(--space-5);
}
@media (max-width: 860px) {
.dash-breakdown-grid { grid-template-columns: 1fr; }
}
.dash-breakdown-swatch {
display: inline-block;
margin-right: 8px;
vertical-align: middle;
}

/* Segmented control for compact on/off toggles (e.g. Day | Week). */
.dash-segmented {
display: inline-flex;
background: var(--color-base-100);
border: 1px solid var(--color-base-300);
border-radius: var(--radius-sm);
padding: 2px;
gap: 2px;
}
.dash-segmented button {
font: 500 12px Inter, system-ui, sans-serif;
color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
background: transparent;
border: 0;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
transition: background-color var(--motion-fast), color var(--motion-fast);
}
.dash-segmented button:hover { color: var(--color-base-content); }
.dash-segmented button[aria-pressed="true"] {
background: var(--color-base-200);
color: var(--color-base-content);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}
.dash-segmented button:focus-visible {
outline: none;
box-shadow: 0 0 0 3px color-mix(in oklab, var(--color-primary) 25%, transparent);
}

/* Series palette: shared by SVG bars, tooltip dots, breakdown swatches. */
[data-series-idx="0"].dash-chart-bar { fill: var(--color-primary); }
[data-series-idx="1"].dash-chart-bar { fill: var(--color-info); }
[data-series-idx="2"].dash-chart-bar { fill: var(--color-success); }
[data-series-idx="3"].dash-chart-bar { fill: var(--color-warning); }
[data-series-idx="4"].dash-chart-bar { fill: #a855f7; }
[data-series-idx="5"].dash-chart-bar { fill: #ec4899; }
[data-series-idx="6"].dash-chart-bar { fill: #0891b2; }
[data-series-idx="7"].dash-chart-bar { fill: #d97706; }
[data-series-idx="0"].dash-chart-tooltip-swatch, [data-series-idx="0"].dash-breakdown-swatch { background: var(--color-primary); }
[data-series-idx="1"].dash-chart-tooltip-swatch, [data-series-idx="1"].dash-breakdown-swatch { background: var(--color-info); }
[data-series-idx="2"].dash-chart-tooltip-swatch, [data-series-idx="2"].dash-breakdown-swatch { background: var(--color-success); }
[data-series-idx="3"].dash-chart-tooltip-swatch, [data-series-idx="3"].dash-breakdown-swatch { background: var(--color-warning); }
[data-series-idx="4"].dash-chart-tooltip-swatch, [data-series-idx="4"].dash-breakdown-swatch { background: #a855f7; }
[data-series-idx="5"].dash-chart-tooltip-swatch, [data-series-idx="5"].dash-breakdown-swatch { background: #ec4899; }
[data-series-idx="6"].dash-chart-tooltip-swatch, [data-series-idx="6"].dash-breakdown-swatch { background: #0891b2; }
[data-series-idx="7"].dash-chart-tooltip-swatch, [data-series-idx="7"].dash-breakdown-swatch { background: #d97706; }

/* Respect reduced motion: snap drawer in, skip shimmer. */
@media (prefers-reduced-motion: reduce) {
.dash-drawer {
animation: none;
}
.dash-drawer-backdrop {
animation: none;
}
.dash-drawer, .dash-drawer-backdrop { animation: none; }
.dash-metric-card.dash-metric-skeleton .dash-metric-label,
.dash-metric-card.dash-metric-skeleton .dash-metric-value,
.dash-table-skeleton-pill {
animation: none;
}
.dash-table-skeleton-pill,
.dash-chart-skeleton { animation: none; }
.dash-chart-bar, .dash-chart-tooltip { transition: none; }
}
4 changes: 2 additions & 2 deletions public/dashboard/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,8 @@
var name = parsed.route;
deactivateAllRoutes();

var liveRoutes = ["skills", "memory-files", "plugins", "subagents", "hooks", "settings", "sessions"];
var comingSoon = ["cost", "scheduler", "evolution", "memory"];
var liveRoutes = ["skills", "memory-files", "plugins", "subagents", "hooks", "settings", "sessions", "cost"];
var comingSoon = ["scheduler", "evolution", "memory"];

if (liveRoutes.indexOf(name) >= 0 && routes[name]) {
var containerId = "route-" + name;
Expand Down
11 changes: 6 additions & 5 deletions public/dashboard/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,14 @@
<svg class="dash-sidebar-icon" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M20.25 14.15v4.098a2.25 2.25 0 0 1-2.25 2.25h-12a2.25 2.25 0 0 1-2.25-2.25V5.625a2.25 2.25 0 0 1 2.25-2.25h8.25M15.75 9l3.75-3.75m0 0L23.25 9m-3.75-3.75v9"/></svg>
<span>Sessions</span>
</a>
<a href="#/cost" class="dash-sidebar-item" data-route="cost">
<svg class="dash-sidebar-icon" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941"/></svg>
<span>Cost</span>
</a>
</nav>

<div class="dash-sidebar-eyebrow" style="margin-top:var(--space-5);">Coming soon</div>
<nav class="dash-sidebar-nav">
<a href="#/cost" class="dash-sidebar-item dash-sidebar-item-soon" data-route="cost">
<svg class="dash-sidebar-icon" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18 9 11.25l4.306 4.306a11.95 11.95 0 0 1 5.814-5.518l2.74-1.22m0 0-5.94-2.281m5.94 2.28-2.28 5.941"/></svg>
<span>Cost</span>
<span class="dash-sidebar-soon-pill">soon</span>
</a>
<a href="#/scheduler" class="dash-sidebar-item dash-sidebar-item-soon" data-route="scheduler">
<svg class="dash-sidebar-icon" fill="none" viewBox="0 0 24 24" stroke-width="1.6" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/></svg>
<span>Scheduler</span>
Expand Down Expand Up @@ -104,6 +103,7 @@
<div id="route-hooks" class="dash-route" hidden></div>
<div id="route-settings" class="dash-route" hidden></div>
<div id="route-sessions" class="dash-route" hidden></div>
<div id="route-cost" class="dash-route" hidden></div>
<div id="route-soon" class="dash-route" hidden></div>
</main>

Expand All @@ -119,6 +119,7 @@
<script src="/ui/dashboard/hooks.js"></script>
<script src="/ui/dashboard/settings.js"></script>
<script src="/ui/dashboard/sessions.js"></script>
<script src="/ui/dashboard/cost.js"></script>
<script>window.PhantomDashboard.init();</script>
</body>
</html>
Loading
Loading