wacrm/src/components/flows/forms/fields.tsx
Arnas Donauskas 4aa329b527
feat(theme): add light/dark mode toggle (#243)
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>
2026-06-16 17:29:46 +03:00

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>
);
}