+
)}
@@ -380,7 +384,7 @@ export function TemplatePalette() {
key={t.id}
onClick={() => handleDeploy(t)}
disabled={isDeploying}
- className="w-full text-left bg-zinc-800/40 hover:bg-zinc-800/70 border border-zinc-700/40 hover:border-zinc-600/50 rounded-xl p-3 transition-all disabled:opacity-50 group"
+ className="w-full text-left bg-zinc-800/40 hover:bg-zinc-800/70 border border-zinc-700/40 hover:border-zinc-600/50 rounded-xl p-3 transition-all disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-zinc-800/40 disabled:hover:border-zinc-700/40 group focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70"
>
diff --git a/canvas/src/components/WorkspaceNode.tsx b/canvas/src/components/WorkspaceNode.tsx
index 5d4a6d39..ad469de6 100644
--- a/canvas/src/components/WorkspaceNode.tsx
+++ b/canvas/src/components/WorkspaceNode.tsx
@@ -5,6 +5,7 @@ import { Handle, Position, type NodeProps, type Node } from "@xyflow/react";
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
import { showToast } from "@/components/Toaster";
import { Tooltip } from "@/components/Tooltip";
+import { STATUS_CONFIG, TIER_CONFIG } from "@/lib/design-tokens";
import { useShallow } from "zustand/react/shallow";
/** Stable selector: returns children, grandchild flag, and descendant count for a node */
@@ -27,15 +28,6 @@ function useHierarchyInfo(parentId: string) {
return { children, hasGrandchildren, descendantCount };
}
-const STATUS_CONFIG: Record = {
- online: { dot: "bg-emerald-400", glow: "shadow-emerald-400/50", label: "Online", bar: "from-emerald-500/20 to-transparent" },
- offline: { dot: "bg-zinc-500", glow: "", label: "Offline", bar: "from-zinc-600/10 to-transparent" },
- paused: { dot: "bg-indigo-400", glow: "", label: "Paused", bar: "from-indigo-500/10 to-transparent" },
- degraded: { dot: "bg-amber-400", glow: "shadow-amber-400/50", label: "Degraded", bar: "from-amber-500/20 to-transparent" },
- failed: { dot: "bg-red-400", glow: "shadow-red-400/50", label: "Failed", bar: "from-red-500/20 to-transparent" },
- provisioning: { dot: "bg-sky-400 motion-safe:animate-pulse", glow: "shadow-sky-400/50", label: "Starting", bar: "from-sky-500/20 to-transparent" },
-};
-
/** Eject/extract arrow icon — visually distinct from delete ✕ */
function EjectIcon() {
return (
@@ -46,13 +38,6 @@ function EjectIcon() {
);
}
-const TIER_CONFIG: Record = {
- 1: { label: "T1", color: "text-zinc-500 bg-zinc-800/80" },
- 2: { label: "T2", color: "text-sky-400 bg-sky-950/50" },
- 3: { label: "T3", color: "text-violet-400 bg-violet-950/50" },
- 4: { label: "T4", color: "text-amber-400 bg-amber-950/50" },
-};
-
export function WorkspaceNode({ id, data }: NodeProps>) {
const statusCfg = STATUS_CONFIG[data.status] || STATUS_CONFIG.offline;
const tierCfg = TIER_CONFIG[data.tier] || { label: `T${data.tier}`, color: "text-zinc-500 bg-zinc-800" };
@@ -123,6 +108,7 @@ export function WorkspaceNode({ id, data }: NodeProps>)
: "bg-zinc-900/90 border border-zinc-700/80 hover:border-zinc-500/60 shadow-lg shadow-black/30 hover:shadow-xl hover:shadow-black/40"
}
backdrop-blur-sm
+ focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/70 focus-visible:ring-offset-1 focus-visible:ring-offset-zinc-950
`}
>
{/* Status gradient bar at top */}
diff --git a/canvas/src/lib/design-tokens.ts b/canvas/src/lib/design-tokens.ts
new file mode 100644
index 00000000..f50e9d43
--- /dev/null
+++ b/canvas/src/lib/design-tokens.ts
@@ -0,0 +1,22 @@
+export const STATUS_CONFIG: Record = {
+ online: { dot: "bg-emerald-400", glow: "shadow-emerald-400/50", label: "Online", bar: "from-emerald-500/20 to-transparent" },
+ offline: { dot: "bg-zinc-500", glow: "", label: "Offline", bar: "from-zinc-600/10 to-transparent" },
+ paused: { dot: "bg-indigo-400", glow: "", label: "Paused", bar: "from-indigo-500/10 to-transparent" },
+ degraded: { dot: "bg-amber-400", glow: "shadow-amber-400/50", label: "Degraded", bar: "from-amber-500/20 to-transparent" },
+ failed: { dot: "bg-red-400", glow: "shadow-red-400/50", label: "Failed", bar: "from-red-500/20 to-transparent" },
+ provisioning: { dot: "bg-sky-400 motion-safe:animate-pulse", glow: "shadow-sky-400/50", label: "Starting", bar: "from-sky-500/20 to-transparent" },
+};
+
+export const TIER_CONFIG: Record = {
+ 1: { label: "T1", color: "text-zinc-500 bg-zinc-800/80" },
+ 2: { label: "T2", color: "text-sky-400 bg-sky-950/50" },
+ 3: { label: "T3", color: "text-violet-400 bg-violet-950/50" },
+ 4: { label: "T4", color: "text-amber-400 bg-amber-950/50" },
+};
+
+export const TIER_COLORS: Record = {
+ 1: "text-zinc-400 border-zinc-700/60",
+ 2: "text-sky-400 border-sky-500/30",
+ 3: "text-violet-400 border-violet-500/30",
+ 4: "text-amber-400 border-amber-500/30",
+};