mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Introduce a light/dark MODE axis orthogonal to the existing accent themes. Neutral surface tokens now live in html[data-mode="light|dark"] blocks; accent tokens (--primary*, --ring, --chart-1, --sidebar-primary*) stay in html[data-theme] blocks, so any accent works in either mode. - globals.css: split neutrals (mode) from accent (theme); add light palette; dark values unchanged (dark stays the default). - lib/themes.ts: Mode type, DEFAULT_MODE=dark, MODE_STORAGE_KEY, isMode. - use-theme.tsx: provider owns mode/setMode/toggleMode + cross-tab sync. - layout.tsx: boot script applies persisted mode before first paint (no flash); colorScheme "dark light". - mode-toggle.tsx (header sun/moon) + themed-toaster.tsx (mode-aware). - appearance-panel.tsx: light/dark control + updated copy. - Tokenize hardcoded neutral colors (slate/white/gray) -> semantic tokens across the app so every surface flips with mode. Flow canvas and message-bubble timestamps made mode-aware too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
135 lines
3.3 KiB
TypeScript
135 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* Reusable field components shared across every per-node form.
|
|
*
|
|
* `NodeKeySelect` — picks a node from the flow's node list, rendered
|
|
* with the source node's icon so the dropdown reads as
|
|
* "destination = ◇ menu" rather than an opaque slug.
|
|
*
|
|
* `NextNodeRow` — wraps NodeKeySelect with a label; the most common
|
|
* per-node form row ("after this node, advance to…").
|
|
*
|
|
* `TextRow` — wraps Input or Textarea behind a label. Pure UI sugar
|
|
* to keep per-node forms uncluttered.
|
|
*
|
|
* Lives in src/components/flows/forms/ so both the list view's
|
|
* collapsed-card editor and the canvas view's side-panel editor
|
|
* (introduced in this PR) mount the exact same form components.
|
|
*/
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { cn } from "@/lib/utils";
|
|
import { NODE_META, type BuilderNode } from "../shared";
|
|
|
|
export function TextRow({
|
|
label,
|
|
value,
|
|
onChange,
|
|
rows = 1,
|
|
}: {
|
|
label: string;
|
|
value: string;
|
|
onChange: (v: string) => void;
|
|
rows?: number;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<label className="mb-1 block text-xs text-muted-foreground">{label}</label>
|
|
{rows > 1 ? (
|
|
<Textarea
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
rows={rows}
|
|
className="bg-muted"
|
|
/>
|
|
) : (
|
|
<Input
|
|
value={value}
|
|
onChange={(e) => onChange(e.target.value)}
|
|
className="bg-muted"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function NextNodeRow({
|
|
value,
|
|
allNodes,
|
|
currentKey,
|
|
onChange,
|
|
label,
|
|
}: {
|
|
value: string;
|
|
allNodes: BuilderNode[];
|
|
currentKey: string;
|
|
onChange: (v: string) => void;
|
|
label: string;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<label className="mb-1 block text-xs text-muted-foreground">{label}</label>
|
|
<NodeKeySelect
|
|
value={value || null}
|
|
nodes={allNodes}
|
|
excludeKey={currentKey}
|
|
onChange={(v) => onChange(v ?? "")}
|
|
placeholder="Pick a next node…"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function NodeKeySelect({
|
|
value,
|
|
nodes,
|
|
excludeKey,
|
|
onChange,
|
|
placeholder,
|
|
className,
|
|
}: {
|
|
value: string | null;
|
|
nodes: BuilderNode[];
|
|
excludeKey?: string;
|
|
onChange: (v: string | null) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
}) {
|
|
const options = nodes.filter((n) => n.node_key !== excludeKey);
|
|
return (
|
|
<Select
|
|
value={value ?? "__none__"}
|
|
onValueChange={(v) => onChange(v === "__none__" ? null : v)}
|
|
>
|
|
<SelectTrigger className={cn("bg-muted", className)}>
|
|
<SelectValue placeholder={placeholder ?? "—"} />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="__none__">— None —</SelectItem>
|
|
{options.map((n) => {
|
|
const Icon = NODE_META[n.node_type].icon;
|
|
return (
|
|
<SelectItem key={n.node_key} value={n.node_key}>
|
|
<span className="inline-flex items-center gap-1.5">
|
|
<Icon
|
|
className={cn("h-3 w-3", NODE_META[n.node_type].color)}
|
|
/>
|
|
{n.node_key}
|
|
</span>
|
|
</SelectItem>
|
|
);
|
|
})}
|
|
</SelectContent>
|
|
</Select>
|
|
);
|
|
}
|