fix(canvas): add htmlFor/id pairs to all bare labels in ConfigTab and DetailsTab

Wire WCAG 1.3.1 label associations: 6 bare <label>+control pairs in
ConfigTab (Description, Tier, Runtime, Effort, Task Budget, Backend) now
use stable useId() IDs with matching htmlFor/id. Field helper in
DetailsTab updated to generate its own fieldId via useId() and inject it
into the child element via cloneElement, so every Name/Role/Tier field in
edit mode is correctly associated without requiring call-site changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Molecule AI Frontend Engineer 2026-04-17 21:15:52 +00:00
parent a8fcff947d
commit fb17f430b7
2 changed files with 25 additions and 10 deletions

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { useState, useEffect, useCallback, useRef, useId } from "react";
import { api } from "@/lib/api";
import { useCanvasStore } from "@/store/canvas";
import { type ConfigData, DEFAULT_CONFIG, TextInput, NumberInput, Toggle, TagList, Section } from "./config/form-inputs";
@ -170,6 +170,14 @@ export function ConfigTab({ workspaceId }: Props) {
}
};
// Stable IDs for bare label↔control pairs (WCAG 1.3.1)
const descriptionId = useId();
const tierId = useId();
const runtimeId = useId();
const effortId = useId();
const taskBudgetId = useId();
const sandboxBackendId = useId();
const isDirty = rawMode ? rawDraft !== originalYaml : toYaml(config) !== originalYaml;
if (loading) {
@ -214,8 +222,9 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="General">
<TextInput label="Name" value={config.name} onChange={(v) => update("name", v)} />
<div>
<label className="text-[10px] text-zinc-500 block mb-1">Description</label>
<label htmlFor={descriptionId} className="text-[10px] text-zinc-500 block mb-1">Description</label>
<textarea
id={descriptionId}
value={config.description}
onChange={(e) => update("description", e.target.value)}
rows={3}
@ -225,8 +234,9 @@ export function ConfigTab({ workspaceId }: Props) {
<div className="grid grid-cols-2 gap-3">
<TextInput label="Version" value={config.version} onChange={(v) => update("version", v)} mono />
<div>
<label className="text-[10px] text-zinc-500 block mb-1">Tier</label>
<label htmlFor={tierId} className="text-[10px] text-zinc-500 block mb-1">Tier</label>
<select
id={tierId}
value={config.tier}
onChange={(e) => update("tier", parseInt(e.target.value, 10))}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
@ -242,8 +252,9 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="Runtime">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-[10px] text-zinc-500 block mb-1">Runtime</label>
<label htmlFor={runtimeId} className="text-[10px] text-zinc-500 block mb-1">Runtime</label>
<select
id={runtimeId}
value={config.runtime || ""}
onChange={(e) => update("runtime", e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
@ -273,11 +284,12 @@ export function ConfigTab({ workspaceId }: Props) {
(config.runtime_config?.model || config.model || "").toLowerCase().includes("anthropic")) && (
<Section title="Claude Settings" defaultOpen={false}>
<div>
<label className="text-[10px] text-zinc-500 block mb-1">
<label htmlFor={effortId} className="text-[10px] text-zinc-500 block mb-1">
Effort
<span className="ml-1 text-zinc-600">(output_config.effort Opus 4.7+)</span>
</label>
<select
id={effortId}
value={config.effort || ""}
onChange={(e) => update("effort", e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"
@ -292,11 +304,12 @@ export function ConfigTab({ workspaceId }: Props) {
</select>
</div>
<div>
<label className="text-[10px] text-zinc-500 block mb-1">
<label htmlFor={taskBudgetId} className="text-[10px] text-zinc-500 block mb-1">
Task Budget (tokens)
<span className="ml-1 text-zinc-600">(output_config.task_budget.total 0 = unset)</span>
</label>
<input
id={taskBudgetId}
type="number"
min={0}
step={1000}
@ -334,8 +347,9 @@ export function ConfigTab({ workspaceId }: Props) {
<Section title="Sandbox" defaultOpen={false}>
<div>
<label className="text-[10px] text-zinc-500 block mb-1">Backend</label>
<label htmlFor={sandboxBackendId} className="text-[10px] text-zinc-500 block mb-1">Backend</label>
<select
id={sandboxBackendId}
value={config.sandbox?.backend || "docker"}
onChange={(e) => updateNested("sandbox" as keyof ConfigData, "backend", e.target.value)}
className="w-full bg-zinc-800 border border-zinc-700 rounded px-2 py-1 text-xs text-zinc-200 focus:outline-none focus:border-blue-500"

View File

@ -1,6 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useState, useEffect, useCallback, useId, cloneElement, type ReactElement } from "react";
import { api } from "@/lib/api";
import { useCanvasStore, type WorkspaceNodeData } from "@/store/canvas";
import { StatusDot } from "../StatusDot";
@ -292,10 +292,11 @@ function Section({ title, children }: { title: string; children: React.ReactNode
}
function Field({ label, children }: { label: string; children: React.ReactNode }) {
const fieldId = useId();
return (
<div>
<label className="text-[10px] text-zinc-500 block mb-0.5">{label}</label>
{children}
<label htmlFor={fieldId} className="text-[10px] text-zinc-500 block mb-0.5">{label}</label>
{cloneElement(children as ReactElement, { id: fieldId })}
</div>
);
}