fix(FN-2204): restore tokenized styling in agents views
- Replace inline agent state badge and card colors with state-specific CSS classes across list, board, tree, and org chart views - Add reusable AgentEmptyState with create-agent CTA and wire it into all empty agent view modes - Polish agents controls and metrics presentation in AgentsView/AgentMetricsBar with corresponding stylesheet token updates - Extend dashboard tests for agent CSS classes, metrics bar behavior, list view rendering, and mobile view coverage - Add a patch changeset for @gsxdsm/fusion describing the agent view UX improvements
This commit is contained in:
5
.changeset/agents-list-view-ux-pass.md
Normal file
5
.changeset/agents-list-view-ux-pass.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@gsxdsm/fusion": patch
|
||||
---
|
||||
|
||||
Improve the dashboard agents list views with shared empty-state actions, token-based state styling, and clearer board/tree/org-chart presentation.
|
||||
11
.changeset/config.json
Normal file
11
.changeset/config.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
|
||||
"changelog": "@changesets/cli/changelog",
|
||||
"commit": false,
|
||||
"fixed": [],
|
||||
"linked": [],
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": []
|
||||
}
|
||||
@@ -7,15 +7,28 @@ const stylesContent = fs.readFileSync(stylesPath, "utf-8");
|
||||
|
||||
// Agent component file paths to verify inline <style> blocks are removed
|
||||
const agentsViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentsView.tsx"), "utf-8");
|
||||
const agentEmptyStateContent = fs.readFileSync(path.join(__dirname, "../components/AgentEmptyState.tsx"), "utf-8");
|
||||
const agentDetailViewContent = fs.readFileSync(path.join(__dirname, "../components/AgentDetailView.tsx"), "utf-8");
|
||||
const activeAgentsPanelContent = fs.readFileSync(path.join(__dirname, "../components/ActiveAgentsPanel.tsx"), "utf-8");
|
||||
const newAgentDialogContent = fs.readFileSync(path.join(__dirname, "../components/NewAgentDialog.tsx"), "utf-8");
|
||||
|
||||
/** Check that styles.css contains a CSS class definition for the given selector */
|
||||
function extractRuleBlock(selector: string): string {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = stylesContent.match(new RegExp(`${escaped}\\s*\\{([^}]*)\\}`));
|
||||
return match?.[1] ?? "";
|
||||
}
|
||||
|
||||
function hasClass(cls: string): boolean {
|
||||
const escaped = cls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
// Match standalone class or class within a grouped selector list
|
||||
return new RegExp(`${escaped}\\s*(,|\\{)`).test(stylesContent);
|
||||
// Match standalone class, grouped selector, or compound selector (e.g. `.foo svg`)
|
||||
return new RegExp(`${escaped}(?=[\\s,{:.#>+~])`).test(stylesContent);
|
||||
}
|
||||
|
||||
function extractSection(startMarker: string, endMarker: string): string {
|
||||
const start = stylesContent.indexOf(startMarker);
|
||||
const end = stylesContent.indexOf(endMarker, start + startMarker.length);
|
||||
return start >= 0 && end >= 0 ? stylesContent.slice(start, end) : "";
|
||||
}
|
||||
|
||||
describe("Agent CSS classes", () => {
|
||||
@@ -53,6 +66,10 @@ describe("Agent CSS classes", () => {
|
||||
it("should define AgentMetricsBar CSS classes", () => {
|
||||
expect(hasClass(".agent-metrics-bar")).toBe(true);
|
||||
expect(hasClass(".agent-metric-card")).toBe(true);
|
||||
expect(hasClass(".agent-metric-card--active")).toBe(true);
|
||||
expect(hasClass(".agent-metric-card--tasks")).toBe(true);
|
||||
expect(hasClass(".agent-metric-card--success")).toBe(true);
|
||||
expect(hasClass(".agent-metric-card--runs")).toBe(true);
|
||||
expect(hasClass(".agent-metric-info")).toBe(true);
|
||||
expect(hasClass(".agent-metric-value")).toBe(true);
|
||||
expect(hasClass(".agent-metric-label")).toBe(true);
|
||||
@@ -66,13 +83,26 @@ describe("Agent CSS classes", () => {
|
||||
expect(hasClass(".agents-view-controls")).toBe(true);
|
||||
expect(hasClass(".agents-view-content")).toBe(true);
|
||||
expect(hasClass(".agent-controls")).toBe(true);
|
||||
expect(hasClass(".agent-controls-filters")).toBe(true);
|
||||
expect(hasClass(".agent-state-filter")).toBe(true);
|
||||
expect(hasClass(".agent-state-filter-select")).toBe(true);
|
||||
expect(hasClass(".agent-board")).toBe(true);
|
||||
expect(hasClass(".agent-board-card")).toBe(true);
|
||||
expect(hasClass(".agent-board-card--idle")).toBe(true);
|
||||
expect(hasClass(".agent-board-card--active")).toBe(true);
|
||||
expect(hasClass(".agent-board-card--running")).toBe(true);
|
||||
expect(hasClass(".agent-board-card--paused")).toBe(true);
|
||||
expect(hasClass(".agent-board-card--error")).toBe(true);
|
||||
expect(hasClass(".agent-board-card--terminated")).toBe(true);
|
||||
expect(hasClass(".agent-board-header")).toBe(true);
|
||||
expect(hasClass(".agent-board-icon")).toBe(true);
|
||||
expect(hasClass(".agent-board-badge")).toBe(true);
|
||||
expect(hasClass(".agent-badge--idle")).toBe(true);
|
||||
expect(hasClass(".agent-badge--active")).toBe(true);
|
||||
expect(hasClass(".agent-badge--running")).toBe(true);
|
||||
expect(hasClass(".agent-badge--paused")).toBe(true);
|
||||
expect(hasClass(".agent-badge--error")).toBe(true);
|
||||
expect(hasClass(".agent-badge--terminated")).toBe(true);
|
||||
expect(hasClass(".agent-board-health")).toBe(true);
|
||||
expect(hasClass(".agent-board-name")).toBe(true);
|
||||
expect(hasClass(".agent-board-id")).toBe(true);
|
||||
@@ -80,6 +110,12 @@ describe("Agent CSS classes", () => {
|
||||
expect(hasClass(".agent-board-actions")).toBe(true);
|
||||
expect(hasClass(".agent-list")).toBe(true);
|
||||
expect(hasClass(".agent-card")).toBe(true);
|
||||
expect(hasClass(".agent-card--idle")).toBe(true);
|
||||
expect(hasClass(".agent-card--active")).toBe(true);
|
||||
expect(hasClass(".agent-card--running")).toBe(true);
|
||||
expect(hasClass(".agent-card--paused")).toBe(true);
|
||||
expect(hasClass(".agent-card--error")).toBe(true);
|
||||
expect(hasClass(".agent-card--terminated")).toBe(true);
|
||||
expect(hasClass(".agent-card-header")).toBe(true);
|
||||
expect(hasClass(".agent-card-body")).toBe(true);
|
||||
expect(hasClass(".agent-card-actions")).toBe(true);
|
||||
@@ -99,6 +135,48 @@ describe("Agent CSS classes", () => {
|
||||
expect(hasClass(".spin")).toBe(true);
|
||||
});
|
||||
|
||||
it("should visually group the filter controls", () => {
|
||||
const filtersBlock = extractRuleBlock(".agent-controls-filters");
|
||||
expect(filtersBlock).toContain("padding: var(--space-xs) var(--space-sm)");
|
||||
expect(filtersBlock).toContain("background: var(--surface)");
|
||||
expect(filtersBlock).toContain("border: 1px solid var(--border)");
|
||||
expect(filtersBlock).toContain("border-radius: var(--radius-md)");
|
||||
});
|
||||
|
||||
it("should use design tokens for the updated tree view spacing and transitions", () => {
|
||||
const treeViewBlock = extractRuleBlock(".agent-tree__view");
|
||||
expect(treeViewBlock).toContain("gap: var(--space-xs)");
|
||||
expect(treeViewBlock).toContain("padding: var(--space-sm) 0");
|
||||
|
||||
const treeNodeBlock = extractRuleBlock(".agent-tree__node");
|
||||
expect(treeNodeBlock).toContain("gap: var(--space-sm)");
|
||||
expect(treeNodeBlock).toContain("padding: var(--space-sm) var(--space-md)");
|
||||
expect(treeNodeBlock).toContain("border-radius: var(--radius-sm)");
|
||||
expect(treeNodeBlock).toContain("transition: background-color var(--transition-fast)");
|
||||
expect(treeNodeBlock).not.toMatch(/gap:\s*4px|padding:\s*8px 12px|border-radius:\s*4px|0\.15s\s+ease/);
|
||||
|
||||
expect(extractRuleBlock(".agent-tree__indent--1")).toContain("padding-left: var(--space-xl)");
|
||||
expect(extractRuleBlock(".agent-tree__indent--2")).toContain("padding-left: calc(var(--space-xl) * 2)");
|
||||
expect(extractRuleBlock(".agent-tree__indent--3")).toContain("padding-left: calc(var(--space-xl) * 3)");
|
||||
expect(extractRuleBlock(".agent-tree__indent--4")).toContain("padding-left: calc(var(--space-xl) * 4)");
|
||||
});
|
||||
|
||||
it("should use dashboard tokens in the updated org chart styles", () => {
|
||||
const orgChartSection = extractSection("/* === FN-1167: Agent Org Chart + Chain of Command === */", "/* === Agent Dialog Mobile Responsive === */");
|
||||
expect(orgChartSection).toContain("gap: var(--space-xl)");
|
||||
expect(orgChartSection).toContain("padding: var(--space-lg)");
|
||||
expect(orgChartSection).toContain("min-height: calc(var(--space-xl) * 9 + var(--space-xs))");
|
||||
expect(orgChartSection).toContain("border: 1px solid var(--border)");
|
||||
expect(orgChartSection).toContain("color: var(--text)");
|
||||
expect(orgChartSection).toContain("color: var(--text-muted)");
|
||||
expect(orgChartSection).toContain("border-radius: var(--radius-pill)");
|
||||
expect(orgChartSection).toContain("transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast)");
|
||||
expect(orgChartSection).not.toContain("var(--border-color)");
|
||||
expect(orgChartSection).not.toContain("var(--text-primary)");
|
||||
expect(orgChartSection).not.toContain("var(--text-secondary)");
|
||||
expect(orgChartSection).not.toMatch(/1\.5rem|0\.75rem|0\.72rem|0\.78rem|0\.65rem|120ms\s+ease|220px|10px/);
|
||||
});
|
||||
|
||||
// Verify AgentDetailView classes
|
||||
it("should define AgentDetailView CSS classes", () => {
|
||||
expect(hasClass(".agent-detail-overlay")).toBe(true);
|
||||
@@ -211,11 +289,23 @@ describe("Agent CSS classes", () => {
|
||||
expect(hasClass(".agent-dialog-loading")).toBe(true);
|
||||
});
|
||||
|
||||
it("should define shared AgentEmptyState component primitives", () => {
|
||||
expect(hasClass(".agent-empty-state__icon")).toBe(true);
|
||||
expect(hasClass(".agent-empty-state__title")).toBe(true);
|
||||
expect(hasClass(".agent-empty-state__description")).toBe(true);
|
||||
expect(agentEmptyStateContent).toContain("Create Agent");
|
||||
});
|
||||
|
||||
// Verify no inline <style> blocks remain in agent components
|
||||
it("should not have inline <style> blocks in AgentsView", () => {
|
||||
expect(agentsViewContent).not.toContain("<style>");
|
||||
});
|
||||
|
||||
it("should only keep runtime health color inline styles in AgentsView", () => {
|
||||
const inlineStyleCount = (agentsViewContent.match(/style=\{\{/g) || []).length;
|
||||
expect(inlineStyleCount).toBe(4);
|
||||
});
|
||||
|
||||
it("should not have inline <style> blocks in AgentDetailView", () => {
|
||||
expect(agentDetailViewContent).not.toContain("<style>");
|
||||
});
|
||||
|
||||
28
packages/dashboard/app/components/AgentEmptyState.tsx
Normal file
28
packages/dashboard/app/components/AgentEmptyState.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Bot } from "lucide-react";
|
||||
|
||||
interface AgentEmptyStateProps {
|
||||
title?: string;
|
||||
description?: string;
|
||||
ctaLabel?: string;
|
||||
onCtaClick?: () => void;
|
||||
}
|
||||
|
||||
export function AgentEmptyState({
|
||||
title = "No agents found",
|
||||
description = "Create an agent to get started",
|
||||
ctaLabel = "Create Agent",
|
||||
onCtaClick,
|
||||
}: AgentEmptyStateProps) {
|
||||
return (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} className="agent-empty-state__icon" />
|
||||
<p className="agent-empty-state__title">{title}</p>
|
||||
<p className="agent-empty-state__description text-secondary">{description}</p>
|
||||
{onCtaClick ? (
|
||||
<button type="button" className="btn btn--primary" onClick={onCtaClick}>
|
||||
{ctaLabel}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,27 +5,33 @@ interface AgentMetricsBarProps {
|
||||
stats: AgentStats | null;
|
||||
}
|
||||
|
||||
const METRIC_CARDS = [
|
||||
{ icon: Activity, label: "Active Agents", valueKey: "activeCount", className: "agent-metric-card--active" },
|
||||
{ icon: ListTodo, label: "Assigned Tasks", valueKey: "assignedTaskCount", className: "agent-metric-card--tasks" },
|
||||
{ icon: CheckCircle, label: "Success Rate", valueKey: "successRate", className: "agent-metric-card--success" },
|
||||
{ icon: Zap, label: "Total Runs", valueKey: "completedRuns", className: "agent-metric-card--runs" },
|
||||
] as const;
|
||||
|
||||
export function AgentMetricsBar({ stats }: AgentMetricsBarProps) {
|
||||
if (!stats) return null;
|
||||
|
||||
const cards = [
|
||||
{ icon: Activity, label: "Active Agents", value: stats.activeCount, color: "var(--state-active-text)" },
|
||||
{ icon: ListTodo, label: "Assigned Tasks", value: stats.assignedTaskCount, color: "var(--in-progress)" },
|
||||
{ icon: CheckCircle, label: "Success Rate", value: `${Math.round(stats.successRate * 100)}%`, color: "var(--color-success, #3fb950)" },
|
||||
{ icon: Zap, label: "Total Runs", value: stats.completedRuns, color: "var(--in-progress)" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="agent-metrics-bar">
|
||||
{cards.map(card => (
|
||||
<div key={card.label} className="agent-metric-card">
|
||||
<card.icon size={18} style={{ color: card.color }} />
|
||||
<div className="agent-metric-info">
|
||||
<span className="agent-metric-value">{card.value}</span>
|
||||
<span className="agent-metric-label">{card.label}</span>
|
||||
{METRIC_CARDS.map((card) => {
|
||||
const value = card.valueKey === "successRate"
|
||||
? `${Math.round(stats.successRate * 100)}%`
|
||||
: stats[card.valueKey];
|
||||
|
||||
return (
|
||||
<div key={card.label} className={`agent-metric-card ${card.className}`}>
|
||||
<card.icon size={18} />
|
||||
<div className="agent-metric-info">
|
||||
<span className="agent-metric-value">{value}</span>
|
||||
<span className="agent-metric-label">{card.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { fetchAgents, updateAgent, updateAgentState, deleteAgent, startAgentRun,
|
||||
import { AgentDetailView } from "./AgentDetailView";
|
||||
import { ActiveAgentsPanel } from "./ActiveAgentsPanel";
|
||||
import { AgentMetricsBar } from "./AgentMetricsBar";
|
||||
import { AgentEmptyState } from "./AgentEmptyState";
|
||||
import { useAgents } from "../hooks/useAgents";
|
||||
import { subscribeSse } from "../sse-bus";
|
||||
import { useAgentHierarchy } from "../hooks/useAgentHierarchy";
|
||||
@@ -37,14 +38,41 @@ const AGENT_ROLES: { value: AgentCapability; label: string; icon: string }[] = [
|
||||
];
|
||||
|
||||
|
||||
const STATE_COLORS: Record<AgentState, { bg: string; text: string; border: string }> = {
|
||||
idle: { bg: "var(--state-idle-bg)", text: "var(--state-idle-text)", border: "var(--state-idle-border)" },
|
||||
active: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" },
|
||||
running: { bg: "var(--state-active-bg)", text: "var(--state-active-text)", border: "var(--state-active-border)" },
|
||||
paused: { bg: "var(--state-paused-bg)", text: "var(--state-paused-text)", border: "var(--state-paused-border)" },
|
||||
error: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
||||
terminated: { bg: "var(--state-error-bg)", text: "var(--state-error-text)", border: "var(--state-error-border)" },
|
||||
};
|
||||
function getStateBadgeClass(state: AgentState): string {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return "agent-badge--running";
|
||||
case "active":
|
||||
return "agent-badge--active";
|
||||
case "paused":
|
||||
return "agent-badge--paused";
|
||||
case "error":
|
||||
return "agent-badge--error";
|
||||
case "terminated":
|
||||
return "agent-badge--terminated";
|
||||
case "idle":
|
||||
default:
|
||||
return "agent-badge--idle";
|
||||
}
|
||||
}
|
||||
|
||||
function getStateCardClass(prefix: "agent-board-card" | "agent-card", state: AgentState): string {
|
||||
switch (state) {
|
||||
case "running":
|
||||
return `${prefix}--running`;
|
||||
case "active":
|
||||
return `${prefix}--active`;
|
||||
case "paused":
|
||||
return `${prefix}--paused`;
|
||||
case "error":
|
||||
return `${prefix}--error`;
|
||||
case "terminated":
|
||||
return `${prefix}--terminated`;
|
||||
case "idle":
|
||||
default:
|
||||
return `${prefix}--idle`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Recursive tree node component for agent hierarchy */
|
||||
function AgentTreeNode({
|
||||
@@ -70,7 +98,7 @@ function AgentTreeNode({
|
||||
const childCount = getChildCount(agent.id);
|
||||
const expanded = isExpanded(agent.id);
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -99,12 +127,7 @@ function AgentTreeNode({
|
||||
<span className="agent-tree__icon">{getRoleIcon(agent.role)}</span>
|
||||
<span className="agent-tree__name">{agent.name}</span>
|
||||
<span
|
||||
className="agent-tree__badge"
|
||||
style={{
|
||||
background: stateStyle.bg,
|
||||
color: stateStyle.text,
|
||||
border: `1px solid ${stateStyle.border}`,
|
||||
}}
|
||||
className={`agent-tree__badge ${stateBadgeClass}`}
|
||||
>
|
||||
{agent.state}
|
||||
</span>
|
||||
@@ -162,7 +185,7 @@ function OrgChartNode({
|
||||
}) {
|
||||
const { agent, children } = node;
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||
|
||||
return (
|
||||
<div className={`org-chart-node${children.length > 0 ? " org-chart-node--has-children" : ""}`}>
|
||||
@@ -179,12 +202,7 @@ function OrgChartNode({
|
||||
</div>
|
||||
<div className="org-chart-node__meta">
|
||||
<span
|
||||
className="org-chart-node__badge"
|
||||
style={{
|
||||
background: stateStyle.bg,
|
||||
color: stateStyle.text,
|
||||
border: `1px solid ${stateStyle.border}`,
|
||||
}}
|
||||
className={`org-chart-node__badge ${stateBadgeClass}`}
|
||||
>
|
||||
{agent.state}
|
||||
</span>
|
||||
@@ -539,7 +557,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<div className="agents-view-content">
|
||||
{/* Filter and Create Bar */}
|
||||
<div className="agent-controls">
|
||||
<div className="agent-state-filter">
|
||||
<div className="agent-controls-filters">
|
||||
<div className="agent-state-filter">
|
||||
<Filter size={14} />
|
||||
<select
|
||||
className="agent-state-filter-select"
|
||||
@@ -557,7 +576,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<label className="checkbox-label agent-system-filter">
|
||||
<label className="checkbox-label agent-system-filter">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showSystemAgents}
|
||||
@@ -565,7 +584,8 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
aria-label="Show system agents"
|
||||
/>
|
||||
Show system agents
|
||||
</label>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="agent-controls-actions">
|
||||
<button
|
||||
@@ -609,11 +629,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
{agentView === "tree" ? (
|
||||
<div className="agent-tree__view">
|
||||
{displayAgents.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
<p className="text-secondary">Create an agent to get started</p>
|
||||
</div>
|
||||
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
|
||||
) : (
|
||||
hierarchy.rootNodes.map((node) => (
|
||||
<AgentTreeNode
|
||||
@@ -638,11 +654,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<span>Loading org chart...</span>
|
||||
</div>
|
||||
) : displayOrgTree.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
<p className="text-secondary">Create an agent to get started</p>
|
||||
</div>
|
||||
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
|
||||
) : (
|
||||
displayOrgTree.map((node) => (
|
||||
<OrgChartNode
|
||||
@@ -659,18 +671,15 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
) : (
|
||||
<div className={agentView === "board" ? "agent-board" : "agent-list"}>
|
||||
{displayAgents.length === 0 ? (
|
||||
<div className="agent-empty">
|
||||
<Bot size={48} opacity={0.3} />
|
||||
<p>No agents found</p>
|
||||
<p className="text-secondary">Create an agent to get started</p>
|
||||
</div>
|
||||
<AgentEmptyState onCtaClick={() => setIsCreating(true)} />
|
||||
) : agentView === "board" ? (
|
||||
// Board view: compact grid layout
|
||||
displayAgents.map(agent => {
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||
const stateCardClass = getStateCardClass("agent-board-card", agent.state);
|
||||
return (
|
||||
<div key={agent.id} className="agent-board-card" style={{ borderColor: stateStyle.border }}>
|
||||
<div key={agent.id} className={`agent-board-card ${stateCardClass}`}>
|
||||
<div
|
||||
className="agent-board-clickable"
|
||||
onClick={() => setSelectedAgentId(agent.id)}
|
||||
@@ -681,12 +690,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
<div className="agent-board-header">
|
||||
<span className="agent-board-icon">{getRoleIcon(agent.role)}</span>
|
||||
<span
|
||||
className="agent-board-badge"
|
||||
style={{
|
||||
background: stateStyle.bg,
|
||||
color: stateStyle.text,
|
||||
border: `1px solid ${stateStyle.border}`,
|
||||
}}
|
||||
className={`agent-board-badge ${stateBadgeClass}`}
|
||||
>
|
||||
{agent.state}
|
||||
</span>
|
||||
@@ -847,12 +851,13 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
// List view: detailed card layout
|
||||
displayAgents.map(agent => {
|
||||
const health = getHealthStatus(agent);
|
||||
const stateStyle = STATE_COLORS[agent.state];
|
||||
const stateBadgeClass = getStateBadgeClass(agent.state);
|
||||
const stateCardClass = getStateCardClass("agent-card", agent.state);
|
||||
const configuredIntervalMs = resolveHeartbeatIntervalMs(agent.runtimeConfig?.heartbeatIntervalMs);
|
||||
const heartbeatOptions = getHeartbeatIntervalOptions(configuredIntervalMs);
|
||||
const isUpdatingHeartbeat = updatingHeartbeatAgentId === agent.id;
|
||||
return (
|
||||
<div key={agent.id} className="agent-card" style={{ borderLeftColor: stateStyle.border }}>
|
||||
<div key={agent.id} className={`agent-card ${stateCardClass}`}>
|
||||
<div className="agent-card-header">
|
||||
<div
|
||||
className="agent-info agent-info--clickable"
|
||||
@@ -905,12 +910,7 @@ export function AgentsView({ addToast, projectId }: AgentsViewProps) {
|
||||
</div>
|
||||
<div className="agent-badges">
|
||||
<span
|
||||
className="badge"
|
||||
style={{
|
||||
background: stateStyle.bg,
|
||||
color: stateStyle.text,
|
||||
border: `1px solid ${stateStyle.border}`,
|
||||
}}
|
||||
className={`badge ${stateBadgeClass}`}
|
||||
>
|
||||
{agent.state}
|
||||
</span>
|
||||
|
||||
@@ -108,4 +108,16 @@ describe("AgentMetricsBar", () => {
|
||||
expect(screen.getByTestId("check-circle-icon")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("zap-icon")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("applies metric variant classes without inline icon styles", () => {
|
||||
const { container } = render(<AgentMetricsBar stats={makeStats()} />);
|
||||
|
||||
expect(container.querySelector(".agent-metric-card--active")).toBeInTheDocument();
|
||||
expect(container.querySelector(".agent-metric-card--tasks")).toBeInTheDocument();
|
||||
expect(container.querySelector(".agent-metric-card--success")).toBeInTheDocument();
|
||||
expect(container.querySelector(".agent-metric-card--runs")).toBeInTheDocument();
|
||||
|
||||
const metricsBarMarkup = container.querySelector(".agent-metrics-bar")?.innerHTML ?? "";
|
||||
expect(metricsBarMarkup).not.toContain("style=");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,19 @@ describe("AgentsView", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No agents found")).toBeTruthy();
|
||||
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Create Agent" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it("opens the create dialog from the empty state CTA", async () => {
|
||||
mockFetchAgents.mockResolvedValue([]);
|
||||
render(<AgentsView addToast={mockAddToast} />);
|
||||
|
||||
const cta = await screen.findByRole("button", { name: "Create Agent" });
|
||||
fireEvent.click(cta);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole("dialog", { name: "Create new agent" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -449,6 +462,7 @@ describe("AgentsView", () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("No agents found")).toBeTruthy();
|
||||
expect(screen.getByText("Create an agent to get started")).toBeTruthy();
|
||||
expect(screen.getByRole("button", { name: "Create Agent" })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ vi.mock("../../api", () => ({
|
||||
deleteAgent: vi.fn(),
|
||||
startAgentRun: vi.fn(),
|
||||
fetchModels: vi.fn(() => Promise.resolve({ models: [] })),
|
||||
fetchOrgTree: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
@@ -53,6 +54,7 @@ import {
|
||||
updateAgentState,
|
||||
deleteAgent,
|
||||
startAgentRun,
|
||||
fetchOrgTree,
|
||||
} from "../../api";
|
||||
|
||||
const mockAgents: Agent[] = [
|
||||
@@ -105,6 +107,7 @@ describe("AgentsView mobile adaptations", () => {
|
||||
endedAt: null,
|
||||
status: "active",
|
||||
});
|
||||
vi.mocked(fetchOrgTree).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it("renders board view grid and board cards", async () => {
|
||||
@@ -136,6 +139,7 @@ describe("AgentsView mobile adaptations", () => {
|
||||
await waitFor(() => expect(screen.getByText("Agents")).toBeTruthy());
|
||||
|
||||
expect(container.querySelector(".agent-controls")).toBeTruthy();
|
||||
expect(container.querySelector(".agent-controls-filters")).toBeTruthy();
|
||||
expect(container.querySelector(".agent-state-filter")).toBeTruthy();
|
||||
expect(container.querySelector(".agent-controls-actions")).toBeTruthy();
|
||||
});
|
||||
@@ -195,9 +199,24 @@ describe("agents-view mobile CSS", () => {
|
||||
expect(block).toContain("flex-wrap: wrap");
|
||||
});
|
||||
|
||||
it("stacks grouped filter controls on mobile", () => {
|
||||
expect(mobileMediaBlock).toContain(".agent-controls-filters");
|
||||
const block = extractRuleBlock(mobileMediaBlock, ".agent-controls-filters");
|
||||
expect(block).toContain("flex-direction: column");
|
||||
expect(block).toContain("width: 100%");
|
||||
});
|
||||
|
||||
it("defines .agents-view-title with flex-wrap on mobile", () => {
|
||||
expect(mobileMediaBlock).toContain(".agents-view-title");
|
||||
const block = extractRuleBlock(mobileMediaBlock, ".agents-view-title");
|
||||
expect(block).toContain("flex-wrap: wrap");
|
||||
});
|
||||
|
||||
it("defines mobile org chart sizing rules", () => {
|
||||
expect(mobileMediaBlock).toContain(".org-chart-node");
|
||||
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node")).toContain("min-width: calc(var(--space-2xl) * 5)");
|
||||
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node-card")).toContain("padding: var(--space-sm)");
|
||||
expect(extractRuleBlock(mobileMediaBlock, ".agent-org-chart")).toContain("gap: var(--space-sm)");
|
||||
expect(extractRuleBlock(mobileMediaBlock, ".org-chart-node__badge")).toContain("font-size: calc(var(--space-sm) + var(--space-xs) * 0.625)");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -872,7 +872,7 @@ body {
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast);
|
||||
transition: background var(--transition-fast), color var(--transition-fast), border-radius var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.view-toggle-btn:hover {
|
||||
@@ -887,6 +887,7 @@ body {
|
||||
.view-toggle-btn.active {
|
||||
background: var(--todo);
|
||||
color: var(--bg);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.view-toggle-btn.active:hover {
|
||||
@@ -25731,9 +25732,21 @@ html .column.drag-over * {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transition: background var(--transition-fast), transform var(--transition-fast), box-shadow var(--transition-fast);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.agent-metric-card:hover {
|
||||
background: var(--card-hover);
|
||||
transform: translateY(calc(var(--space-xs) * -0.25));
|
||||
}
|
||||
|
||||
.agent-metric-card--active svg { color: var(--state-active-text); }
|
||||
.agent-metric-card--tasks svg { color: var(--in-progress); }
|
||||
.agent-metric-card--success svg { color: var(--color-success); }
|
||||
.agent-metric-card--runs svg { color: var(--in-progress); }
|
||||
|
||||
.agent-metric-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -25799,18 +25812,30 @@ html .column.drag-over * {
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
margin-bottom: var(--space-lg);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent-controls-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.agent-state-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs));
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
transition: border-color var(--transition-fast), color var(--transition-fast);
|
||||
transition: border-color var(--transition-fast), color var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-state-filter:hover {
|
||||
@@ -25841,8 +25866,11 @@ html .column.drag-over * {
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
gap: var(--space-sm);
|
||||
user-select: none;
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: color var(--transition-fast), background var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-system-filter:hover {
|
||||
@@ -25870,18 +25898,18 @@ html .column.drag-over * {
|
||||
.agent-tree__view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 8px 0;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-sm) 0;
|
||||
}
|
||||
|
||||
.agent-tree__node {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.15s ease;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.agent-tree__node:hover {
|
||||
@@ -25889,21 +25917,21 @@ html .column.drag-over * {
|
||||
}
|
||||
|
||||
.agent-tree__indent--0 { padding-left: 0; }
|
||||
.agent-tree__indent--1 { padding-left: 24px; }
|
||||
.agent-tree__indent--2 { padding-left: 48px; }
|
||||
.agent-tree__indent--3 { padding-left: 72px; }
|
||||
.agent-tree__indent--4 { padding-left: 96px; }
|
||||
.agent-tree__indent--1 { padding-left: var(--space-xl); }
|
||||
.agent-tree__indent--2 { padding-left: calc(var(--space-xl) * 2); }
|
||||
.agent-tree__indent--3 { padding-left: calc(var(--space-xl) * 3); }
|
||||
.agent-tree__indent--4 { padding-left: calc(var(--space-xl) * 4); }
|
||||
|
||||
.agent-tree__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: calc(var(--space-md) * 2);
|
||||
height: calc(var(--space-md) * 2);
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: var(--text-secondary);
|
||||
transition: background-color 0.15s ease, color 0.15s ease;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
@@ -25912,7 +25940,7 @@ html .column.drag-over * {
|
||||
|
||||
.agent-tree__toggle:hover {
|
||||
background: var(--card-hover);
|
||||
color: var(--text-primary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.agent-tree__toggle--leaf {
|
||||
@@ -25928,7 +25956,7 @@ html .column.drag-over * {
|
||||
.agent-tree__content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: var(--space-sm);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -25969,14 +25997,28 @@ html .column.drag-over * {
|
||||
}
|
||||
|
||||
.agent-is-child {
|
||||
background: rgba(124, 92, 191, 0.05);
|
||||
background: color-mix(in srgb, var(--accent) 5%, transparent);
|
||||
border-left: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.agent-tree__children {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
gap: var(--space-xs);
|
||||
margin-left: calc(var(--space-md) - var(--space-xs));
|
||||
padding-left: var(--space-md);
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.agent-tree__children::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1px;
|
||||
top: 0;
|
||||
bottom: 50%;
|
||||
width: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.agent-board {
|
||||
@@ -25989,7 +26031,7 @@ html .column.drag-over * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
padding: var(--space-md);
|
||||
padding: var(--space-lg);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-top-width: 3px;
|
||||
@@ -26051,8 +26093,8 @@ html .column.drag-over * {
|
||||
|
||||
.agent-board-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
gap: var(--space-xs);
|
||||
margin-top: var(--space-xs);
|
||||
padding-top: var(--space-sm);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
@@ -26215,10 +26257,66 @@ html .column.drag-over * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 48px;
|
||||
gap: var(--space-sm);
|
||||
padding: calc(var(--space-2xl) + var(--space-lg));
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agent-empty-state__icon {
|
||||
color: var(--text-dim);
|
||||
}
|
||||
|
||||
.agent-empty-state__title,
|
||||
.agent-empty-state__description {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.agent-empty .btn {
|
||||
margin-top: var(--space-sm);
|
||||
}
|
||||
|
||||
/* Agent state badge variants */
|
||||
.agent-badge--idle {
|
||||
background: var(--state-idle-bg);
|
||||
color: var(--state-idle-text);
|
||||
border: 1px solid var(--state-idle-border);
|
||||
}
|
||||
|
||||
.agent-badge--active,
|
||||
.agent-badge--running {
|
||||
background: var(--state-active-bg);
|
||||
color: var(--state-active-text);
|
||||
border: 1px solid var(--state-active-border);
|
||||
}
|
||||
|
||||
.agent-badge--paused {
|
||||
background: var(--state-paused-bg);
|
||||
color: var(--state-paused-text);
|
||||
border: 1px solid var(--state-paused-border);
|
||||
}
|
||||
|
||||
.agent-badge--error,
|
||||
.agent-badge--terminated {
|
||||
background: var(--state-error-bg);
|
||||
color: var(--state-error-text);
|
||||
border: 1px solid var(--state-error-border);
|
||||
}
|
||||
|
||||
.agent-board-card--idle { border-top-color: var(--state-idle-border); }
|
||||
.agent-board-card--active,
|
||||
.agent-board-card--running { border-top-color: var(--state-active-border); }
|
||||
.agent-board-card--paused { border-top-color: var(--state-paused-border); }
|
||||
.agent-board-card--error,
|
||||
.agent-board-card--terminated { border-top-color: var(--state-error-border); }
|
||||
|
||||
.agent-card--idle { border-left-color: var(--state-idle-border); }
|
||||
.agent-card--active,
|
||||
.agent-card--running { border-left-color: var(--state-active-border); }
|
||||
.agent-card--paused { border-left-color: var(--state-paused-border); }
|
||||
.agent-card--error,
|
||||
.agent-card--terminated { border-left-color: var(--state-error-border); }
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@@ -28033,7 +28131,7 @@ html .column.drag-over * {
|
||||
|
||||
.agent-board-actions .btn,
|
||||
.agent-card-actions .btn {
|
||||
min-height: 36px;
|
||||
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
.agent-controls {
|
||||
@@ -28042,6 +28140,13 @@ html .column.drag-over * {
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.agent-controls-filters {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
.agent-controls-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -28050,12 +28155,13 @@ html .column.drag-over * {
|
||||
|
||||
.agent-controls-actions .btn {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
.agent-state-filter {
|
||||
.agent-state-filter,
|
||||
.agent-system-filter {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
min-height: calc(var(--space-2xl) + var(--space-xs));
|
||||
}
|
||||
|
||||
.agent-state-filter-select {
|
||||
@@ -28211,14 +28317,14 @@ html .column.drag-over * {
|
||||
min-width: 36px;
|
||||
}
|
||||
|
||||
.agent-tree__indent--1 { padding-left: 16px; }
|
||||
.agent-tree__indent--2 { padding-left: 32px; }
|
||||
.agent-tree__indent--3 { padding-left: 48px; }
|
||||
.agent-tree__indent--4 { padding-left: 64px; }
|
||||
.agent-tree__indent--1 { padding-left: var(--space-lg); }
|
||||
.agent-tree__indent--2 { padding-left: calc(var(--space-lg) * 2); }
|
||||
.agent-tree__indent--3 { padding-left: calc(var(--space-lg) * 3); }
|
||||
.agent-tree__indent--4 { padding-left: calc(var(--space-lg) * 4); }
|
||||
|
||||
.agent-tree__toggle {
|
||||
min-width: 36px;
|
||||
min-height: 36px;
|
||||
min-width: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||
min-height: calc(var(--space-lg) * 2 + var(--space-xs));
|
||||
}
|
||||
|
||||
/* Performance Tab mobile */
|
||||
@@ -33229,19 +33335,19 @@ html .column.drag-over * {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 1.5rem;
|
||||
padding: 1rem;
|
||||
gap: var(--space-xl);
|
||||
padding: var(--space-lg);
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
min-height: 220px;
|
||||
min-height: calc(var(--space-xl) * 9 + var(--space-xs));
|
||||
}
|
||||
|
||||
.agent-org-chart__loading,
|
||||
.chain-of-command-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
gap: var(--space-sm);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.org-chart-node {
|
||||
@@ -33249,19 +33355,19 @@ html .column.drag-over * {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 220px;
|
||||
min-width: calc(var(--space-xl) * 9 + var(--space-xs));
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.org-chart-node-card {
|
||||
width: 100%;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
padding: 0.75rem;
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--space-md);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
transition: border-color 120ms ease, background-color 120ms ease, transform 120ms ease;
|
||||
transition: border-color var(--transition-fast), background-color var(--transition-fast), transform var(--transition-fast);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -33269,19 +33375,19 @@ html .column.drag-over * {
|
||||
.org-chart-node-card:focus-visible {
|
||||
border-color: var(--accent);
|
||||
background: var(--card-hover);
|
||||
transform: translateY(-1px);
|
||||
transform: translateY(calc(var(--space-xs) * -0.25));
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.org-chart-node__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
gap: var(--space-sm);
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.org-chart-node__icon {
|
||||
font-size: 1rem;
|
||||
font-size: var(--space-lg);
|
||||
}
|
||||
|
||||
.org-chart-node__name {
|
||||
@@ -33293,23 +33399,23 @@ html .column.drag-over * {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.org-chart-node__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.72rem;
|
||||
padding: calc(var(--space-xs) * 0.5) var(--space-sm);
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.org-chart-node__health {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.75rem;
|
||||
gap: var(--space-xs);
|
||||
font-size: var(--space-md);
|
||||
}
|
||||
|
||||
.org-chart-node--has-children > .org-chart-node-card::after {
|
||||
@@ -33317,10 +33423,10 @@ html .column.drag-over * {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: -16px;
|
||||
bottom: calc(var(--space-lg) * -1);
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: var(--border-color);
|
||||
height: var(--space-lg);
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.org-chart-children {
|
||||
@@ -33328,60 +33434,60 @@ html .column.drag-over * {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
padding-top: 1.5rem;
|
||||
margin-top: 0.25rem;
|
||||
gap: var(--space-lg);
|
||||
padding-top: var(--space-xl);
|
||||
margin-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.org-chart-children::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 24px;
|
||||
right: 24px;
|
||||
left: var(--space-xl);
|
||||
right: var(--space-xl);
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.org-chart-children > .org-chart-node::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -24px;
|
||||
top: calc(var(--space-xl) * -1);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: var(--border-color);
|
||||
height: var(--space-xl);
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
.chain-of-command-path {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35rem 0.5rem;
|
||||
gap: var(--space-xs) var(--space-sm);
|
||||
}
|
||||
|
||||
.chain-of-command-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.chain-of-command-node {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: var(--space-xs) calc(var(--space-sm) + var(--space-xs) * 0.5);
|
||||
border-radius: var(--radius-pill);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chain-of-command-node:hover:not(:disabled) {
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chain-of-command-node:disabled {
|
||||
@@ -33391,7 +33497,7 @@ html .column.drag-over * {
|
||||
|
||||
.chain-of-command-node--current {
|
||||
border-color: var(--accent);
|
||||
color: var(--text-primary);
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -33401,11 +33507,26 @@ html .column.drag-over * {
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.agent-org-chart {
|
||||
padding-bottom: 1.5rem;
|
||||
padding: var(--space-sm);
|
||||
padding-bottom: calc(var(--space-lg) + var(--space-sm));
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.org-chart-node {
|
||||
min-width: 200px;
|
||||
min-width: calc(var(--space-2xl) * 5);
|
||||
}
|
||||
|
||||
.org-chart-node-card {
|
||||
padding: var(--space-sm);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.org-chart-node__name {
|
||||
font-size: var(--space-md);
|
||||
}
|
||||
|
||||
.org-chart-node__badge {
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.625);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user