+
+ {/* ---- left: back · icon · name · status · edited ---- */}
-
-
-
-
-
- setState((s) => ({ ...s, name: e.target.value }))
- }
- placeholder="Flow name"
- className="max-w-md bg-card text-lg font-semibold"
- />
-
- {dirty && (
-
-
- Edited
-
- )}
-
-
+
+
+
+
setState((s) => ({ ...s, name: e.target.value }))}
+ placeholder="Flow name"
+ spellCheck={false}
+ aria-label="Flow name"
+ className="min-w-[120px] max-w-[340px] rounded-lg border border-transparent bg-transparent px-2 py-1 text-lg font-bold leading-tight tracking-tight text-foreground outline-none transition-colors hover:bg-muted focus:border-primary focus:bg-transparent focus:shadow-[0_0_0_3px_var(--primary-soft)]"
+ />
+
+ {dirty && (
+
+
+ Edited
+
+ )}
+
+ {/* ---- right: runs · delete · activate · save ---- */}
+
-
setState((s) => ({ ...s, description: e.target.value }))
}
- placeholder="Optional description (internal — customers don't see this)"
- className="bg-card text-sm"
+ placeholder="Add a short description (internal — customers don't see this)"
+ aria-label="Flow description"
+ className="w-full max-w-[78ch] rounded-md border border-transparent bg-transparent px-2 py-1 text-[13px] text-muted-foreground outline-none transition-colors placeholder:text-muted-foreground/60 hover:bg-muted/50 focus:border-primary focus:bg-transparent focus:text-foreground"
/>
);
}
-function StatusBadge({ status }: { status: BuilderState["status"] }) {
- const cls = {
- draft: "border-border bg-muted text-muted-foreground",
- active: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
- archived: "border-border bg-muted/50 text-muted-foreground",
+function StatusChip({ status }: { status: BuilderState["status"] }) {
+ const cfg = {
+ draft: {
+ // Neutral, not amber — amber is reserved for the adjacent
+ // "Edited" dirty signal, so the two don't read as the same alert.
+ cls: "border-border bg-muted text-muted-foreground",
+ label: "Draft",
+ },
+ active: {
+ cls: "border-emerald-600/40 bg-emerald-500/10 text-emerald-300",
+ label: "Active",
+ },
+ archived: {
+ cls: "border-border bg-muted/50 text-muted-foreground",
+ label: "Archived",
+ },
}[status];
return (
-
- {status.charAt(0).toUpperCase() + status.slice(1)}
-
+
+
+ {cfg.label}
+
);
}
diff --git a/src/components/flows/shared.tsx b/src/components/flows/shared.tsx
index 1b2a097..3befa71 100644
--- a/src/components/flows/shared.tsx
+++ b/src/components/flows/shared.tsx
@@ -30,6 +30,8 @@ import {
Workflow,
} from "lucide-react";
+import { cn } from "@/lib/utils";
+
// ============================================================
// Node-type union — single source of truth for every place the UI
// enumerates types (add menu, type pickers, switch statements). Kept
@@ -67,52 +69,159 @@ export interface BuilderNode {
export const NODE_META: Record<
NodeType,
- { label: string; icon: typeof Workflow; color: string }
+ { label: string; icon: typeof Workflow; color: string; blurb: string }
> = {
- start: { label: "Start", icon: PlayCircle, color: "text-emerald-400" },
+ start: {
+ label: "Start",
+ icon: PlayCircle,
+ color: "text-emerald-400",
+ blurb: "Entry point of the flow",
+ },
send_message: {
label: "Send message",
icon: MessageCircle,
color: "text-sky-400",
+ blurb: "Sends a WhatsApp text message",
},
send_buttons: {
label: "Send buttons",
icon: ListChecks,
color: "text-primary",
+ blurb: "Sends quick-reply buttons",
},
send_list: {
label: "Send list",
icon: ListPlus,
color: "text-indigo-400",
+ blurb: "Sends a tappable list of options",
},
send_media: {
label: "Send media",
icon: Paperclip,
color: "text-cyan-400",
+ blurb: "Sends an image, video, or document",
},
collect_input: {
label: "Collect input",
icon: Inbox,
color: "text-teal-400",
+ blurb: "Asks a question, saves the reply",
},
condition: {
label: "If / else",
icon: GitFork,
color: "text-fuchsia-400",
+ blurb: "Branches on a rule",
},
set_tag: {
label: "Tag contact",
icon: Tag,
color: "text-pink-400",
+ blurb: "Adds or removes a contact tag",
},
handoff: {
label: "Handoff to agent",
icon: UserPlus,
color: "text-amber-400",
+ blurb: "Hands the conversation to a human",
+ },
+ end: {
+ label: "End",
+ icon: Flag,
+ color: "text-muted-foreground",
+ blurb: "Ends the flow",
},
- end: { label: "End", icon: Flag, color: "text-muted-foreground" },
};
+// ============================================================
+// Per-node-type color system.
+//
+// Each node type gets its own hue so the canvas reads at a glance —
+// what KIND of step is this. Kept as raw oklch (not Tailwind classes)
+// so a node card can tint its icon chip, type label, selection ring,
+// and edge ports from one source, the way the Flow Builder design
+// handoff does. Hues sit in the same oklch family as the app tokens
+// in globals.css; they don't replace --primary (the accent), they
+// complement it. `nodeColors()` derives the soft/ring/text variants.
+// ============================================================
+
+const NODE_HUE: Record
= {
+ start: { l: 0.62, c: 0.13, h: 162 }, // emerald — the start, echoes WhatsApp green
+ send_message: { l: 0.6, c: 0.18, h: 293 }, // violet — the workhorse
+ send_buttons: { l: 0.62, c: 0.16, h: 254 }, // cobalt
+ send_list: { l: 0.62, c: 0.15, h: 277 }, // indigo
+ send_media: { l: 0.65, c: 0.12, h: 210 }, // sky
+ collect_input: { l: 0.65, c: 0.1, h: 185 }, // teal — capture
+ condition: { l: 0.72, c: 0.15, h: 65 }, // amber — a fork in the road
+ set_tag: { l: 0.65, c: 0.15, h: 350 }, // pink
+ handoff: { l: 0.65, c: 0.17, h: 16 }, // rose — hands off
+ end: { l: 0.55, c: 0.01, h: 260 }, // neutral grey — terminal
+};
+
+export interface NodeColors {
+ /** Full-strength hue — icon glyph, selection ring, port fill. */
+ solid: string;
+ /** ~14% tint — icon chip background, soft fills. */
+ soft: string;
+ /** ~45% tint — hover border / focus ring. */
+ ring: string;
+ /** Hue for the uppercase type label, kept readable in BOTH modes. */
+ text: string;
+}
+
+export function nodeColors(type: NodeType): NodeColors {
+ const t = NODE_HUE[type];
+ const solid = `oklch(${t.l} ${t.c} ${t.h})`;
+ return {
+ solid,
+ soft: `oklch(${t.l} ${t.c} ${t.h} / 0.14)`,
+ ring: `oklch(${t.l} ${t.c} ${t.h} / 0.45)`,
+ // Blend the hue toward the live --foreground token so the label
+ // holds contrast in BOTH modes: in dark mode --foreground is
+ // near-white (the label lightens to read on the dark card), in
+ // light mode it's near-black (the label darkens to read on the
+ // white card). The old fixed-light value only worked on dark.
+ text: `color-mix(in oklch, ${solid}, var(--foreground) 38%)`,
+ };
+}
+
+// ============================================================
+// Shared node icon chip — the per-type colored glyph badge used in
+// the canvas node card, list-view card, inspector header, and the
+// add-step menu. One component so a styling change (radius, contrast,
+// hover) lands in every place at once and the `nodeColors()` lookup
+// lives in exactly one spot.
+// ============================================================
+
+export function NodeIconChip({
+ type,
+ size = 24,
+ iconSize = 14,
+ className,
+}: {
+ type: NodeType;
+ /** Chip side length in px. */
+ size?: number;
+ /** Glyph side length in px. */
+ iconSize?: number;
+ className?: string;
+}) {
+ const meta = NODE_META[type];
+ const c = nodeColors(type);
+ const Icon = meta.icon;
+ return (
+
+
+
+ );
+}
+
// ============================================================
// Pure editing helpers — used by forms in both views.
// ============================================================