diff --git a/frontend/src/widgets/FreetextWidget.test.tsx b/frontend/src/widgets/FreetextWidget.test.tsx
new file mode 100644
index 00000000..64db4be6
--- /dev/null
+++ b/frontend/src/widgets/FreetextWidget.test.tsx
@@ -0,0 +1,10 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { FreetextWidget } from "./FreetextWidget";
+test("submits typed text", async () => {
+ const onRespond = vi.fn();
+ render();
+ await userEvent.type(screen.getByRole("textbox"), "ciao");
+ await userEvent.click(screen.getByRole("button", { name: /invia/i }));
+ expect(onRespond).toHaveBeenCalledWith({ id: "u1", kind: "freetext", text: "ciao" });
+});
diff --git a/frontend/src/widgets/FreetextWidget.tsx b/frontend/src/widgets/FreetextWidget.tsx
new file mode 100644
index 00000000..178c4844
--- /dev/null
+++ b/frontend/src/widgets/FreetextWidget.tsx
@@ -0,0 +1,12 @@
+import { useState } from "react";
+import type { WidgetProps } from "./types";
+export function FreetextWidget({ descriptor, onRespond }: WidgetProps) {
+ const [text, setText] = useState("");
+ return (
+
+ {descriptor.title &&
{descriptor.title}
}
+
+ );
+}
diff --git a/frontend/src/widgets/InfoWidget.tsx b/frontend/src/widgets/InfoWidget.tsx
new file mode 100644
index 00000000..7f2f2dc8
--- /dev/null
+++ b/frontend/src/widgets/InfoWidget.tsx
@@ -0,0 +1,5 @@
+import type { WidgetProps } from "./types";
+export function InfoWidget({ descriptor }: WidgetProps) {
+ const color = descriptor.level === "error" ? "text-red-600" : descriptor.level === "warning" ? "text-amber-600" : "text-foreground";
+ return {descriptor.text ?? descriptor.title}
;
+}
diff --git a/frontend/src/widgets/ReservedControls.tsx b/frontend/src/widgets/ReservedControls.tsx
new file mode 100644
index 00000000..cdbfc740
--- /dev/null
+++ b/frontend/src/widgets/ReservedControls.tsx
@@ -0,0 +1,7 @@
+const LABELS: Record = { back: "Torna indietro", exit: "Esci", other: "Altro — specifica" };
+export function ReservedControls({ reserved, onControl }: { reserved?: string[]; onControl: (c: string) => void }) {
+ if (!reserved?.length) return null;
+ return {reserved.map((c) => (
+
+ ))}
;
+}
diff --git a/frontend/src/widgets/SelectWidget.test.tsx b/frontend/src/widgets/SelectWidget.test.tsx
new file mode 100644
index 00000000..f3c807f7
--- /dev/null
+++ b/frontend/src/widgets/SelectWidget.test.tsx
@@ -0,0 +1,17 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { SelectWidget } from "./SelectWidget";
+
+test("picking an option responds with its id", async () => {
+ const onRespond = vi.fn();
+ render();
+ expect(screen.getByText(/consigliato/i)).toBeInTheDocument();
+ await userEvent.click(screen.getByRole("button", { name: /A/ }));
+ expect(onRespond).toHaveBeenCalledWith({ id: "u1", kind: "select", choices: ["a"] });
+});
+test("a reserved control responds with control, not a choice", async () => {
+ const onRespond = vi.fn();
+ render();
+ await userEvent.click(screen.getByRole("button", { name: /indietro/i }));
+ expect(onRespond).toHaveBeenCalledWith({ id: "u1", control: "back" });
+});
diff --git a/frontend/src/widgets/SelectWidget.tsx b/frontend/src/widgets/SelectWidget.tsx
new file mode 100644
index 00000000..7bad3167
--- /dev/null
+++ b/frontend/src/widgets/SelectWidget.tsx
@@ -0,0 +1,19 @@
+import type { WidgetProps } from "./types";
+import { ReservedControls } from "./ReservedControls";
+export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
+ return (
+
+ {descriptor.title &&
{descriptor.title}
}
+ {descriptor.intro &&
{descriptor.intro}
}
+
+ {descriptor.options?.map((o) => (
+
+ ))}
+
+
onRespond({ id: descriptor.id, control: c })} />
+
+ );
+}
diff --git a/frontend/src/widgets/index.ts b/frontend/src/widgets/index.ts
new file mode 100644
index 00000000..63937d7a
--- /dev/null
+++ b/frontend/src/widgets/index.ts
@@ -0,0 +1,6 @@
+import { register } from "./registry";
+import { SelectWidget } from "./SelectWidget";
+import { InfoWidget } from "./InfoWidget";
+import { FreetextWidget } from "./FreetextWidget";
+register("select", SelectWidget); register("info", InfoWidget); register("freetext", FreetextWidget);
+export { resolve } from "./registry";