` | - | Native input change handler. |
+| `type` | `QuestionnaireInputType` | `"text"` | A text-entry type such as `"email"`, `"number"`, or `"date"`. |
+| `render` | `ReactElement \| (props, state) => ReactElement` | ` ` | Custom render target with input state. |
+
+The render state contains `disabled`, `filled`, and `invalid`.
+
+### Questionnaire.Error
+
+The validation message. It is hidden until its item fails validation.
+
+| Prop | Type | Default | Description |
+| ---------- | ------------------------------------------------ | ------------------ | ---------------------------------------- |
+| `children` | `React.ReactNode` | contextual message | Custom validation message. |
+| `render` | `ReactElement \| (props, state) => ReactElement` | `` | Custom render target with invalid state. |
+
+### Navigation actions
+
+`Previous`, `Skip`, `Next`, and `Submit` render native buttons and stay mounted
+while their visibility changes.
+
+| Part | Default type | Visible when | Disabled when |
+| ---------- | ------------ | ------------------------ | ------------------------- |
+| `Previous` | `"button"` | Not on the first item. | Consumer sets `disabled`. |
+| `Skip` | `"button"` | Active item is optional. | Consumer sets `disabled`. |
+| `Next` | `"button"` | Not on the last item. | Consumer sets `disabled`. |
+| `Submit` | `"submit"` | On the last item. | Consumer sets `disabled`. |
+
+Each accepts native button props and:
+
+| Prop | Type | Description |
+| -------- | ------------------------------------------------ | ------------------------------------------- |
+| `render` | `ReactElement \| (props, state) => ReactElement` | Custom render target with navigation state. |
+
+The render state contains `visible`, `disabled`, `shortcut`, and the active
+item's `status`.
diff --git a/apps/v4/examples/__components__/aria.tsx b/apps/v4/examples/__components__/aria.tsx
index 3ce167c7d64..e15dd9b9745 100644
--- a/apps/v4/examples/__components__/aria.tsx
+++ b/apps/v4/examples/__components__/aria.tsx
@@ -2730,6 +2730,118 @@ export const Components: Record = {
) || "progress-rtl"
return { default: mod.default || mod[exportName] }
}),
+ "questionnaire-animated": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-animated")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-animated"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-card": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-card")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-card"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-conditional": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-conditional")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-conditional"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-controlled": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-controlled")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-controlled"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-demo": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-demo")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-demo"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-dialog": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-dialog")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-dialog"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-freeform": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-freeform")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-freeform"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-multiple": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-multiple")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-multiple"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-navigation-state": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-navigation-state")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-navigation-state"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-progress": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-progress")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-progress"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-resume": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-resume")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-resume"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-shortcuts": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-shortcuts")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-shortcuts"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-skip": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-skip")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-skip"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-validation": React.lazy(async () => {
+ const mod = await import("@/examples/aria/questionnaire-validation")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-validation"
+ return { default: mod.default || mod[exportName] }
+ }),
"radio-fields": React.lazy(async () => {
const mod = await import("@/examples/aria/radio-fields")
const exportName =
diff --git a/apps/v4/examples/__components__/base.tsx b/apps/v4/examples/__components__/base.tsx
index 04fd37d0efe..6b9297a3145 100644
--- a/apps/v4/examples/__components__/base.tsx
+++ b/apps/v4/examples/__components__/base.tsx
@@ -2850,6 +2850,118 @@ export const Components: Record = {
) || "progress-rtl"
return { default: mod.default || mod[exportName] }
}),
+ "questionnaire-animated": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-animated")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-animated"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-card": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-card")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-card"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-conditional": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-conditional")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-conditional"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-controlled": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-controlled")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-controlled"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-demo": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-demo")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-demo"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-dialog": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-dialog")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-dialog"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-freeform": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-freeform")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-freeform"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-multiple": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-multiple")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-multiple"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-navigation-state": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-navigation-state")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-navigation-state"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-progress": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-progress")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-progress"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-resume": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-resume")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-resume"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-shortcuts": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-shortcuts")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-shortcuts"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-skip": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-skip")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-skip"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-validation": React.lazy(async () => {
+ const mod = await import("@/examples/base/questionnaire-validation")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-validation"
+ return { default: mod.default || mod[exportName] }
+ }),
"radio-fields": React.lazy(async () => {
const mod = await import("@/examples/base/radio-fields")
const exportName =
diff --git a/apps/v4/examples/__components__/index.tsx b/apps/v4/examples/__components__/index.tsx
index 7f4d9f6aa28..8a6d81d22c9 100644
--- a/apps/v4/examples/__components__/index.tsx
+++ b/apps/v4/examples/__components__/index.tsx
@@ -370,6 +370,20 @@ const shards: Record<
"progress-demo",
"progress-label",
"progress-rtl",
+ "questionnaire-animated",
+ "questionnaire-card",
+ "questionnaire-conditional",
+ "questionnaire-controlled",
+ "questionnaire-demo",
+ "questionnaire-dialog",
+ "questionnaire-freeform",
+ "questionnaire-multiple",
+ "questionnaire-navigation-state",
+ "questionnaire-progress",
+ "questionnaire-resume",
+ "questionnaire-shortcuts",
+ "questionnaire-skip",
+ "questionnaire-validation",
"radio-fields",
"radio-group-choice-card",
"radio-group-demo",
@@ -859,6 +873,20 @@ const shards: Record<
"progress-demo",
"progress-label",
"progress-rtl",
+ "questionnaire-animated",
+ "questionnaire-card",
+ "questionnaire-conditional",
+ "questionnaire-controlled",
+ "questionnaire-demo",
+ "questionnaire-dialog",
+ "questionnaire-freeform",
+ "questionnaire-multiple",
+ "questionnaire-navigation-state",
+ "questionnaire-progress",
+ "questionnaire-resume",
+ "questionnaire-shortcuts",
+ "questionnaire-skip",
+ "questionnaire-validation",
"radio-fields",
"radio-group-choice-card",
"radio-group-demo",
@@ -1358,6 +1386,20 @@ const shards: Record<
"progress-demo",
"progress-label",
"progress-rtl",
+ "questionnaire-animated",
+ "questionnaire-card",
+ "questionnaire-conditional",
+ "questionnaire-controlled",
+ "questionnaire-demo",
+ "questionnaire-dialog",
+ "questionnaire-freeform",
+ "questionnaire-multiple",
+ "questionnaire-navigation-state",
+ "questionnaire-progress",
+ "questionnaire-resume",
+ "questionnaire-shortcuts",
+ "questionnaire-skip",
+ "questionnaire-validation",
"radio-fields",
"radio-group-choice-card",
"radio-group-demo",
diff --git a/apps/v4/examples/__components__/radix.tsx b/apps/v4/examples/__components__/radix.tsx
index efe499c90d3..5ee04b60c12 100644
--- a/apps/v4/examples/__components__/radix.tsx
+++ b/apps/v4/examples/__components__/radix.tsx
@@ -2810,6 +2810,118 @@ export const Components: Record = {
) || "progress-rtl"
return { default: mod.default || mod[exportName] }
}),
+ "questionnaire-animated": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-animated")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-animated"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-card": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-card")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-card"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-conditional": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-conditional")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-conditional"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-controlled": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-controlled")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-controlled"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-demo": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-demo")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-demo"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-dialog": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-dialog")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-dialog"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-freeform": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-freeform")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-freeform"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-multiple": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-multiple")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-multiple"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-navigation-state": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-navigation-state")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-navigation-state"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-progress": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-progress")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-progress"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-resume": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-resume")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-resume"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-shortcuts": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-shortcuts")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-shortcuts"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-skip": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-skip")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-skip"
+ return { default: mod.default || mod[exportName] }
+ }),
+ "questionnaire-validation": React.lazy(async () => {
+ const mod = await import("@/examples/radix/questionnaire-validation")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-validation"
+ return { default: mod.default || mod[exportName] }
+ }),
"radio-fields": React.lazy(async () => {
const mod = await import("@/examples/radix/radio-fields")
const exportName =
diff --git a/apps/v4/examples/__index__.tsx b/apps/v4/examples/__index__.tsx
index 6b1378c784f..4cba5a10155 100644
--- a/apps/v4/examples/__index__.tsx
+++ b/apps/v4/examples/__index__.tsx
@@ -1425,6 +1425,62 @@ export const ExamplesIndex: Record> = {
name: "progress-rtl",
filePath: "examples/base/progress-rtl.tsx",
},
+ "questionnaire-animated": {
+ name: "questionnaire-animated",
+ filePath: "examples/base/questionnaire-animated.tsx",
+ },
+ "questionnaire-card": {
+ name: "questionnaire-card",
+ filePath: "examples/base/questionnaire-card.tsx",
+ },
+ "questionnaire-conditional": {
+ name: "questionnaire-conditional",
+ filePath: "examples/base/questionnaire-conditional.tsx",
+ },
+ "questionnaire-controlled": {
+ name: "questionnaire-controlled",
+ filePath: "examples/base/questionnaire-controlled.tsx",
+ },
+ "questionnaire-demo": {
+ name: "questionnaire-demo",
+ filePath: "examples/base/questionnaire-demo.tsx",
+ },
+ "questionnaire-dialog": {
+ name: "questionnaire-dialog",
+ filePath: "examples/base/questionnaire-dialog.tsx",
+ },
+ "questionnaire-freeform": {
+ name: "questionnaire-freeform",
+ filePath: "examples/base/questionnaire-freeform.tsx",
+ },
+ "questionnaire-multiple": {
+ name: "questionnaire-multiple",
+ filePath: "examples/base/questionnaire-multiple.tsx",
+ },
+ "questionnaire-navigation-state": {
+ name: "questionnaire-navigation-state",
+ filePath: "examples/base/questionnaire-navigation-state.tsx",
+ },
+ "questionnaire-progress": {
+ name: "questionnaire-progress",
+ filePath: "examples/base/questionnaire-progress.tsx",
+ },
+ "questionnaire-resume": {
+ name: "questionnaire-resume",
+ filePath: "examples/base/questionnaire-resume.tsx",
+ },
+ "questionnaire-shortcuts": {
+ name: "questionnaire-shortcuts",
+ filePath: "examples/base/questionnaire-shortcuts.tsx",
+ },
+ "questionnaire-skip": {
+ name: "questionnaire-skip",
+ filePath: "examples/base/questionnaire-skip.tsx",
+ },
+ "questionnaire-validation": {
+ name: "questionnaire-validation",
+ filePath: "examples/base/questionnaire-validation.tsx",
+ },
"radio-fields": {
name: "radio-fields",
filePath: "examples/base/radio-fields.tsx",
@@ -3363,6 +3419,62 @@ export const ExamplesIndex: Record> = {
name: "progress-rtl",
filePath: "examples/aria/progress-rtl.tsx",
},
+ "questionnaire-animated": {
+ name: "questionnaire-animated",
+ filePath: "examples/aria/questionnaire-animated.tsx",
+ },
+ "questionnaire-card": {
+ name: "questionnaire-card",
+ filePath: "examples/aria/questionnaire-card.tsx",
+ },
+ "questionnaire-conditional": {
+ name: "questionnaire-conditional",
+ filePath: "examples/aria/questionnaire-conditional.tsx",
+ },
+ "questionnaire-controlled": {
+ name: "questionnaire-controlled",
+ filePath: "examples/aria/questionnaire-controlled.tsx",
+ },
+ "questionnaire-demo": {
+ name: "questionnaire-demo",
+ filePath: "examples/aria/questionnaire-demo.tsx",
+ },
+ "questionnaire-dialog": {
+ name: "questionnaire-dialog",
+ filePath: "examples/aria/questionnaire-dialog.tsx",
+ },
+ "questionnaire-freeform": {
+ name: "questionnaire-freeform",
+ filePath: "examples/aria/questionnaire-freeform.tsx",
+ },
+ "questionnaire-multiple": {
+ name: "questionnaire-multiple",
+ filePath: "examples/aria/questionnaire-multiple.tsx",
+ },
+ "questionnaire-navigation-state": {
+ name: "questionnaire-navigation-state",
+ filePath: "examples/aria/questionnaire-navigation-state.tsx",
+ },
+ "questionnaire-progress": {
+ name: "questionnaire-progress",
+ filePath: "examples/aria/questionnaire-progress.tsx",
+ },
+ "questionnaire-resume": {
+ name: "questionnaire-resume",
+ filePath: "examples/aria/questionnaire-resume.tsx",
+ },
+ "questionnaire-shortcuts": {
+ name: "questionnaire-shortcuts",
+ filePath: "examples/aria/questionnaire-shortcuts.tsx",
+ },
+ "questionnaire-skip": {
+ name: "questionnaire-skip",
+ filePath: "examples/aria/questionnaire-skip.tsx",
+ },
+ "questionnaire-validation": {
+ name: "questionnaire-validation",
+ filePath: "examples/aria/questionnaire-validation.tsx",
+ },
"radio-fields": {
name: "radio-fields",
filePath: "examples/aria/radio-fields.tsx",
@@ -5341,6 +5453,62 @@ export const ExamplesIndex: Record> = {
name: "progress-rtl",
filePath: "examples/radix/progress-rtl.tsx",
},
+ "questionnaire-animated": {
+ name: "questionnaire-animated",
+ filePath: "examples/radix/questionnaire-animated.tsx",
+ },
+ "questionnaire-card": {
+ name: "questionnaire-card",
+ filePath: "examples/radix/questionnaire-card.tsx",
+ },
+ "questionnaire-conditional": {
+ name: "questionnaire-conditional",
+ filePath: "examples/radix/questionnaire-conditional.tsx",
+ },
+ "questionnaire-controlled": {
+ name: "questionnaire-controlled",
+ filePath: "examples/radix/questionnaire-controlled.tsx",
+ },
+ "questionnaire-demo": {
+ name: "questionnaire-demo",
+ filePath: "examples/radix/questionnaire-demo.tsx",
+ },
+ "questionnaire-dialog": {
+ name: "questionnaire-dialog",
+ filePath: "examples/radix/questionnaire-dialog.tsx",
+ },
+ "questionnaire-freeform": {
+ name: "questionnaire-freeform",
+ filePath: "examples/radix/questionnaire-freeform.tsx",
+ },
+ "questionnaire-multiple": {
+ name: "questionnaire-multiple",
+ filePath: "examples/radix/questionnaire-multiple.tsx",
+ },
+ "questionnaire-navigation-state": {
+ name: "questionnaire-navigation-state",
+ filePath: "examples/radix/questionnaire-navigation-state.tsx",
+ },
+ "questionnaire-progress": {
+ name: "questionnaire-progress",
+ filePath: "examples/radix/questionnaire-progress.tsx",
+ },
+ "questionnaire-resume": {
+ name: "questionnaire-resume",
+ filePath: "examples/radix/questionnaire-resume.tsx",
+ },
+ "questionnaire-shortcuts": {
+ name: "questionnaire-shortcuts",
+ filePath: "examples/radix/questionnaire-shortcuts.tsx",
+ },
+ "questionnaire-skip": {
+ name: "questionnaire-skip",
+ filePath: "examples/radix/questionnaire-skip.tsx",
+ },
+ "questionnaire-validation": {
+ name: "questionnaire-validation",
+ filePath: "examples/radix/questionnaire-validation.tsx",
+ },
"radio-fields": {
name: "radio-fields",
filePath: "examples/radix/radio-fields.tsx",
diff --git a/apps/v4/examples/aria/questionnaire-animated.tsx b/apps/v4/examples/aria/questionnaire-animated.tsx
new file mode 100644
index 00000000000..8bb03279dbf
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-animated.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "task", required: true },
+ { name: "review", required: true },
+ { name: "delivery", required: true },
+] as const
+
+const itemClassName =
+ "data-active:animate-in data-active:fade-in-0 data-active:slide-in-from-bottom-2 data-active:duration-300 motion-reduce:animate-none"
+
+export function QuestionnaireAnimated() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent workflow saved", {
+ description: `Task: ${formData.get("task") ?? "None"} · Review: ${formData.get("review") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ What should the agent do?
+
+ Choose the task for this run.
+
+
+
+ Implement the requested change
+
+
+ Debug the current behavior
+
+
+ Review the implementation
+
+
+
+
+
+
+
+ How should the work be reviewed?
+
+
+ Select the verification depth.
+
+
+
+ Targeted checks
+
+
+ Complete test suite
+
+
+ Tests and manual QA
+
+
+
+
+
+
+
+ How should the result be delivered?
+
+
+ Choose the final handoff format.
+
+
+
+ Concise summary
+
+
+ Summary and changed files
+
+
+ Detailed review handoff
+
+
+
+
+
+
+
+ Next
+ Save workflow
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-card.tsx b/apps/v4/examples/aria/questionnaire-card.tsx
new file mode 100644
index 00000000000..c2460520a0f
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-card.tsx
@@ -0,0 +1,136 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/styles/aria-nova/ui/card"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [{ value: "fix" }, { value: "refactor" }, { value: "docs" }],
+ name: "task",
+ required: true,
+ },
+ {
+ choices: [{ value: "summary" }, { value: "files" }, { value: "review" }],
+ name: "output",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireCard() {
+ const taskTitleId = React.useId()
+ const outputTitleId = React.useId()
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent task created", {
+ description: `Task: ${formData.get("task") ?? "None"} · Handoff: ${formData.get("output") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ }>
+ What should the agent work on?
+
+ }>
+ Choose the task that should be handled next.
+
+
+
+
+
+
+
+
+ Fix the failing tests
+
+
+ Refactor the data layer
+
+
+ Update the integration guide
+
+
+
+
+
+
+
+
+ }>
+ What should the final handoff include?
+
+ }>
+ Pick the level of detail needed for review.
+
+
+
+
+
+
+
+
+ Summary only
+
+
+ Summary and changed files
+
+
+ Full review handoff
+
+
+
+
+
+
+
+
+
+ Next
+ Create task
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-conditional.tsx b/apps/v4/examples/aria/questionnaire-conditional.tsx
new file mode 100644
index 00000000000..7c3f64db8a3
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-conditional.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+export function QuestionnaireConditional() {
+ const [runtime, setRuntime] = React.useState("local")
+ const items = React.useMemo(
+ () => [
+ { name: "runtime", required: true },
+ {
+ disabled: runtime !== "cloud",
+ name: "environment",
+ required: true,
+ },
+ { name: "approval", required: true },
+ ],
+ [runtime]
+ )
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Execution plan saved", {
+ description: `Runtime: ${formData.get("runtime") ?? "None"} · Environment: ${formData.get("environment") ?? "Not applicable"} · Approval: ${formData.get("approval") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ Where should the agent run?
+
+ Cloud runs add an environment question to this flow.
+
+
+ setRuntime("local")}
+ >
+ Local workspace
+
+ setRuntime("cloud")}
+ >
+ Cloud workspace
+
+
+
+
+
+
+
+ Which cloud environment should it use?
+
+
+ Preview
+ Staging
+
+ Isolated sandbox
+
+
+
+
+
+
+
+ When should the agent request approval?
+
+
+
+ Before writing files
+
+
+ Before running commands
+
+
+ Only for sensitive actions
+
+
+
+
+
+
+
+ Next
+ Save execution plan
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-controlled.tsx b/apps/v4/examples/aria/questionnaire-controlled.tsx
new file mode 100644
index 00000000000..d5224fee305
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-controlled.tsx
@@ -0,0 +1,127 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "checks", required: true },
+ { name: "output", required: true },
+] as const
+
+const itemLabels: Record = {
+ scope: "Change scope",
+ checks: "Verification",
+ output: "Final output",
+}
+
+export function QuestionnaireControlled() {
+ const [item, setItem] = React.useState("scope")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent workflow configured", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("checks") ?? "None"} · Output: ${formData.get("output") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+ Current checkpoint: {itemLabels[item]}
+
+
+
+
+
+
+ What may the agent change?
+
+ The host stores the active checkpoint while Questionnaire navigates.
+
+
+
+ Only the target component
+
+
+ Component and related tests
+
+
+ The complete feature area
+
+
+
+
+
+
+
+ Which verification level should it use?
+
+
+
+ Targeted tests
+
+
+ Package tests and typecheck
+
+
+ Full workspace verification
+
+
+
+
+
+
+
+ What should the agent return when finished?
+
+
+
+ Concise summary
+
+
+ Summary with changed files
+
+
+ Detailed implementation handoff
+
+
+
+
+
+
+
+ Next
+ Save workflow
+
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-demo.tsx b/apps/v4/examples/aria/questionnaire-demo.tsx
new file mode 100644
index 00000000000..6d83f6aaafa
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-demo.tsx
@@ -0,0 +1,150 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "tool-calls" },
+ { value: "approvals" },
+ { value: "handoffs" },
+ ],
+ name: "direction",
+ required: true,
+ },
+ {
+ choices: [
+ { value: "progress" },
+ { value: "decisions" },
+ { value: "risks" },
+ { value: "next-step" },
+ ],
+ name: "signals",
+ },
+ {
+ choices: [{ value: "now" }, { value: "next-cycle" }, { value: "backlog" }],
+ name: "timing",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireDemo() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ direction: formData.get("direction"),
+ signals: formData.getAll("signals"),
+ timing: formData.get("timing"),
+ }
+
+ toast("Agent plan saved", {
+ description: `Direction: ${answers.direction ?? "None"} · Progress signals: ${answers.signals.join(", ") || "None"} · Timing: ${answers.timing ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+
+ What should the agent build next?
+
+
+ Choose a direction or describe another task.
+
+
+
+ Tool call timeline
+
+ Show what the agent ran and what came back.
+
+
+
+ Approval checkpoints
+
+ Ask before sensitive or destructive actions.
+
+
+
+ Sub-agent handoffs
+
+ Make delegated work and results easier to follow.
+
+
+
+
+
+
+
+
+
+ What should every progress update include?
+
+
+ Select all that apply, or skip this question.
+
+
+ Progress
+ Decisions
+ Risks
+ Next step
+
+
+
+
+
+ When should work begin?
+
+ Choose when the agent should begin the work.
+
+
+ Start now
+
+ Next development cycle
+
+
+ Add it to the backlog
+
+
+
+
+
+
+
+
+ Next
+ Save plan
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-dialog.tsx b/apps/v4/examples/aria/questionnaire-dialog.tsx
new file mode 100644
index 00000000000..07d290c7783
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-dialog.tsx
@@ -0,0 +1,121 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import { Button } from "@/styles/aria-nova/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/styles/aria-nova/ui/dialog"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "tests", required: true },
+] as const
+
+export function QuestionnaireDialog() {
+ const [open, setOpen] = React.useState(false)
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ setOpen(false)
+ toast("Clarification sent", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("tests") ?? "None"}`,
+ })
+ }
+
+ return (
+
+ Open clarification
+
+
+
+
+
+ }>
+ Which files are in scope?
+
+ }>
+ Choose how broadly the agent can update the workspace.
+
+
+
+
+ Component only
+
+
+ Complete feature directory
+
+
+ Any related workspace file
+
+
+
+
+
+
+
+
+ }>
+ How much verification is needed?
+
+ }>
+ Choose the checks the agent should run before handoff.
+
+
+
+
+ Targeted tests
+
+
+ Package tests
+
+
+ Full workspace verification
+
+
+
+
+
+
+
+ Cancel
+
+
+
+ Next
+ Send answer
+
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-freeform.tsx b/apps/v4/examples/aria/questionnaire-freeform.tsx
new file mode 100644
index 00000000000..28ae7d8ad3b
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-freeform.tsx
@@ -0,0 +1,79 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "incremental" },
+ { value: "module" },
+ { value: "rewrite" },
+ ],
+ name: "approach",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireFreeform() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const approach = new FormData(event.currentTarget).get("approach")
+
+ toast("Approach selected", {
+ description: `Approach: ${approach ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+ How should the agent approach this refactor?
+
+
+ Choose a strategy or write a more specific instruction.
+
+
+
+ Make the smallest safe change
+
+
+ Refactor one module at a time
+
+
+ Replace the implementation completely
+
+
+
+
+
+
+
+ Use this approach
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-multiple.tsx b/apps/v4/examples/aria/questionnaire-multiple.tsx
new file mode 100644
index 00000000000..5c635f73546
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-multiple.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "source" },
+ { value: "tests" },
+ { value: "docs" },
+ { value: "history" },
+ ],
+ name: "context",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireMultiple() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const context = new FormData(event.currentTarget).getAll("context")
+
+ toast("Context selected", {
+ description: `Context: ${context.join(", ") || "None"}`,
+ })
+ }
+
+ return (
+
+
+
+ What context should the agent inspect?
+
+
+ Select every source that may affect the implementation.
+
+
+
+ Relevant source files
+
+
+ Existing tests
+
+
+ Architecture documentation
+
+
+ Recent commit history
+
+
+
+
+
+
+ Share context
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-navigation-state.tsx b/apps/v4/examples/aria/questionnaire-navigation-state.tsx
new file mode 100644
index 00000000000..ed06d122645
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-navigation-state.tsx
@@ -0,0 +1,119 @@
+"use client"
+
+import * as React from "react"
+import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "permission", required: true },
+ { name: "verification", required: true },
+] as const
+
+type ItemName = "permission" | "verification"
+
+export function QuestionnaireNavigationState() {
+ const [item, setItem] = React.useState("permission")
+ const [statuses, setStatuses] = React.useState<
+ Record
+ >({
+ permission: "unanswered",
+ verification: "unanswered",
+ })
+ const unanswered = statuses[item] === "unanswered"
+
+ function setStatus(name: ItemName, status: QuestionnaireItemStatus) {
+ setStatuses((current) => ({ ...current, [name]: status }))
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Permissions saved", {
+ description: `Permission: ${formData.get("permission") ?? "None"} · Verification: ${formData.get("verification") ?? "None"}`,
+ })
+ }
+
+ return (
+ setItem(nextItem as ItemName)}
+ onSubmit={handleSubmit}
+ >
+
+
+ setStatus("permission", status)}
+ >
+ What may the agent modify?
+
+ Next is intentionally disabled until an answer is selected.
+
+
+ Project files
+
+ Project files and tests
+
+
+ Files, tests, and configuration
+
+
+
+
+
+ setStatus("verification", status)}
+ >
+
+ What must pass before completion?
+
+
+ Tests
+
+ Tests and types
+
+
+ Tests, types, and visual QA
+
+
+
+
+
+
+
+
+ Next
+
+
+ Save permissions
+
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-progress.tsx b/apps/v4/examples/aria/questionnaire-progress.tsx
new file mode 100644
index 00000000000..61c66631a05
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-progress.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "strategy", required: true },
+ { name: "tests", required: true },
+ { name: "delivery", required: true },
+] as const
+
+export function QuestionnaireProgressExample() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Pull request plan ready", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Commits: ${formData.get("strategy") ?? "None"} · Tests: ${formData.get("tests") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
+ })
+ }
+
+ return (
+
+ (
+
+
+ {Array.from({ length: state.total }, (_, index) => (
+
+ ))}
+
+
+ Checkpoint {state.current} of {state.total}
+
+
+ )}
+ />
+
+
+ How large is the change?
+
+ Small patch
+
+ Feature-sized change
+
+
+ Cross-package change
+
+
+
+
+
+
+
+ How should commits be organized?
+
+
+
+ Single commit
+
+
+ Logical commits
+
+
+ Squash before review
+
+
+
+
+
+
+ Which tests should run?
+
+
+ Targeted tests
+
+
+ Package suite
+
+
+ Full workspace
+
+
+
+
+
+
+
+ How should the work be delivered?
+
+
+ Patch only
+
+ Committed locally
+
+
+ Push a review branch
+
+
+
+
+
+
+
+ Next
+ Finish plan
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-resume.tsx b/apps/v4/examples/aria/questionnaire-resume.tsx
new file mode 100644
index 00000000000..74eaa751639
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-resume.tsx
@@ -0,0 +1,115 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import { Button } from "@/styles/aria-nova/ui/button"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "change", required: true },
+ { name: "verification", required: true },
+ { name: "notes" },
+] as const
+
+export function QuestionnaireResume() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ change: formData.get("change"),
+ verification: formData.getAll("verification"),
+ notes: formData.get("notes"),
+ }
+
+ toast("Draft updated", {
+ description: `Migration: ${answers.change ?? "None"} · Verification: ${answers.verification.join(", ") || "None"} · Notes: ${answers.notes || "None"}`,
+ })
+ }
+
+ return (
+ toast("Saved answers restored")}
+ onSubmit={handleSubmit}
+ >
+
+
+
+ What kind of migration is this?
+
+ This answer was saved during the previous session.
+
+
+
+ Incremental migration
+
+
+ Single cutover
+
+
+
+
+
+
+
+ How should the migration be verified?
+
+
+ These checks were selected during the previous session.
+
+
+
+ Run migration tests
+
+
+ Run the typecheck
+
+
+ Perform a manual smoke test
+
+
+
+
+
+
+
+ Anything else the agent should remember?
+
+
+ This note was saved with the draft.
+
+
+
+
+
+
+ Reset changes
+
+
+ Next
+ Update draft
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-shortcuts.tsx b/apps/v4/examples/aria/questionnaire-shortcuts.tsx
new file mode 100644
index 00000000000..ce210beae14
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-shortcuts.tsx
@@ -0,0 +1,96 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ NativeSelect,
+ NativeSelectOption,
+} from "@/styles/aria-nova/ui/native-select"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [{ value: "inspect" }, { value: "tests" }, { value: "patch" }],
+ name: "action",
+ required: true,
+ },
+] as const
+
+type ShortcutMode = React.ComponentProps["shortcuts"]
+
+export function QuestionnaireShortcuts() {
+ const [shortcuts, setShortcuts] = React.useState("letters")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const action = new FormData(event.currentTarget).get("action")
+
+ toast("Next action selected", {
+ description: `Action: ${action ?? "None"} · Shortcuts: ${shortcuts ?? "none"}`,
+ })
+ }
+
+ return (
+
+ {
+ const value = event.target.value
+ setShortcuts(
+ value === "letters" || value === "numbers" ? value : undefined
+ )
+ }}
+ >
+ No shortcuts
+ Letters
+ Numbers
+
+
+
+
+
+ What should the agent do next?
+
+
+ Use the displayed shortcut or navigate with the keyboard.
+
+
+
+ Inspect the implementation
+
+
+ Run the relevant tests
+
+
+ Prepare the patch
+
+
+
+
+
+
+ Confirm action
+
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-skip.tsx b/apps/v4/examples/aria/questionnaire-skip.tsx
new file mode 100644
index 00000000000..a7be2edaa27
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-skip.tsx
@@ -0,0 +1,132 @@
+"use client"
+
+import * as React from "react"
+import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "task", required: true },
+ { name: "constraints" },
+ { name: "review", required: true },
+] as const
+
+export function QuestionnaireSkipExample() {
+ const [constraintStatus, setConstraintStatus] =
+ React.useState("unanswered")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ task: formData.get("task"),
+ constraints: formData.get("constraints"),
+ constraintStatus,
+ review: formData.get("review"),
+ }
+
+ toast("Agent brief submitted", {
+ description: `Task: ${answers.task ?? "None"} · Constraints: ${
+ answers.constraintStatus === "skipped"
+ ? "Skipped"
+ : (answers.constraints ?? "None")
+ } · Review: ${answers.review ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ What kind of change is this?
+
+ Choose the category that best describes the work.
+
+
+ New feature
+ Bug fix
+ Refactor
+
+
+
+
+
+
+ Are there any implementation constraints?
+
+
+ Answer if needed, or intentionally skip this question.
+
+
+
+ Do not add dependencies
+
+
+ Do not change the database
+
+
+ Preserve the public API
+
+
+
+
+
+
+
+ How should the work be reviewed?
+
+
+ Choose the checks the agent should complete before handoff.
+
+
+
+ Run the test suite
+
+
+ Review the final diff
+
+
+ Tests and diff review
+
+
+
+
+
+
+
+
+ Next
+ Submit brief
+
+
+ )
+}
diff --git a/apps/v4/examples/aria/questionnaire-validation.tsx b/apps/v4/examples/aria/questionnaire-validation.tsx
new file mode 100644
index 00000000000..f9d6e09b899
--- /dev/null
+++ b/apps/v4/examples/aria/questionnaire-validation.tsx
@@ -0,0 +1,203 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from "@/styles/aria-nova/ui/card"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/aria-nova/ui/questionnaire"
+
+const items = [
+ { name: "detail", required: true },
+ { name: "audience", required: true },
+] as const
+
+const questionnaireSchema = z
+ .object({
+ detail: z.enum(["summary", "complete"]),
+ audience: z.enum(["team", "public"]),
+ })
+ .superRefine((answers, context) => {
+ if (answers.audience === "public" && answers.detail === "summary") {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ message:
+ "Public answers need enough context. Choose a complete answer.",
+ path: ["detail"],
+ })
+ }
+ })
+
+type QuestionnaireItemName = keyof z.infer
+type QuestionnaireErrors = Partial>
+
+function ValidationProgress() {
+ return (
+ (
+
+ {state.current} / {state.total}
+
+ )}
+ />
+ )
+}
+
+export function QuestionnaireValidation() {
+ const [item, setItem] = React.useState("detail")
+ const [errors, setErrors] = React.useState({})
+
+ function clearError(name: QuestionnaireItemName) {
+ setErrors((currentErrors) => {
+ if (!currentErrors[name]) {
+ return currentErrors
+ }
+
+ const nextErrors = { ...currentErrors }
+ delete nextErrors[name]
+ return nextErrors
+ })
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const result = questionnaireSchema.safeParse(
+ Object.fromEntries(new FormData(event.currentTarget))
+ )
+
+ if (result.success) {
+ setErrors({})
+ toast("Agent response configured", {
+ description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`,
+ })
+ return
+ }
+
+ const nextErrors: QuestionnaireErrors = {}
+
+ for (const issue of result.error.issues) {
+ const name = issue.path[0]
+
+ if ((name === "detail" || name === "audience") && !nextErrors[name]) {
+ nextErrors[name] = issue.message
+ }
+ }
+
+ const firstInvalidItem = result.error.issues[0]?.path[0]
+
+ setErrors(nextErrors)
+
+ if (firstInvalidItem === "detail" || firstInvalidItem === "audience") {
+ setItem(firstInvalidItem)
+ }
+ }
+
+ return (
+
+
+
+
+
+ How much detail should the answer include?
+
+
+ Choose the response depth.
+
+
+
+
+
+
+
+ clearError("detail")}
+ >
+ Concise summary
+
+ clearError("detail")}
+ >
+ Complete answer
+
+
+ {errors.detail}
+
+
+
+
+
+ Who will read the answer?
+
+ Public answers require complete context.
+
+
+
+
+
+
+
+ clearError("audience")}
+ >
+ My team
+
+ clearError("audience")}
+ >
+ Public audience
+
+
+ {errors.audience}
+
+
+
+
+
+
+ Next
+ Validate answers
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-animated.tsx b/apps/v4/examples/base/questionnaire-animated.tsx
new file mode 100644
index 00000000000..aa9db672464
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-animated.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "task", required: true },
+ { name: "review", required: true },
+ { name: "delivery", required: true },
+] as const
+
+const itemClassName =
+ "data-active:animate-in data-active:fade-in-0 data-active:slide-in-from-bottom-2 data-active:duration-300 motion-reduce:animate-none"
+
+export function QuestionnaireAnimated() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent workflow saved", {
+ description: `Task: ${formData.get("task") ?? "None"} · Review: ${formData.get("review") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ What should the agent do?
+
+ Choose the task for this run.
+
+
+
+ Implement the requested change
+
+
+ Debug the current behavior
+
+
+ Review the implementation
+
+
+
+
+
+
+
+ How should the work be reviewed?
+
+
+ Select the verification depth.
+
+
+
+ Targeted checks
+
+
+ Complete test suite
+
+
+ Tests and manual QA
+
+
+
+
+
+
+
+ How should the result be delivered?
+
+
+ Choose the final handoff format.
+
+
+
+ Concise summary
+
+
+ Summary and changed files
+
+
+ Detailed review handoff
+
+
+
+
+
+
+
+ Next
+ Save workflow
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-card.tsx b/apps/v4/examples/base/questionnaire-card.tsx
new file mode 100644
index 00000000000..3e2d63afac0
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-card.tsx
@@ -0,0 +1,136 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/styles/base-nova/ui/card"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [{ value: "fix" }, { value: "refactor" }, { value: "docs" }],
+ name: "task",
+ required: true,
+ },
+ {
+ choices: [{ value: "summary" }, { value: "files" }, { value: "review" }],
+ name: "output",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireCard() {
+ const taskTitleId = React.useId()
+ const outputTitleId = React.useId()
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent task created", {
+ description: `Task: ${formData.get("task") ?? "None"} · Handoff: ${formData.get("output") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ }>
+ What should the agent work on?
+
+ }>
+ Choose the task that should be handled next.
+
+
+
+
+
+
+
+
+ Fix the failing tests
+
+
+ Refactor the data layer
+
+
+ Update the integration guide
+
+
+
+
+
+
+
+
+ }>
+ What should the final handoff include?
+
+ }>
+ Pick the level of detail needed for review.
+
+
+
+
+
+
+
+
+ Summary only
+
+
+ Summary and changed files
+
+
+ Full review handoff
+
+
+
+
+
+
+
+
+
+ Next
+ Create task
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-conditional.tsx b/apps/v4/examples/base/questionnaire-conditional.tsx
new file mode 100644
index 00000000000..29045f572c3
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-conditional.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+export function QuestionnaireConditional() {
+ const [runtime, setRuntime] = React.useState("local")
+ const items = React.useMemo(
+ () => [
+ { name: "runtime", required: true },
+ {
+ disabled: runtime !== "cloud",
+ name: "environment",
+ required: true,
+ },
+ { name: "approval", required: true },
+ ],
+ [runtime]
+ )
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Execution plan saved", {
+ description: `Runtime: ${formData.get("runtime") ?? "None"} · Environment: ${formData.get("environment") ?? "Not applicable"} · Approval: ${formData.get("approval") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ Where should the agent run?
+
+ Cloud runs add an environment question to this flow.
+
+
+ setRuntime("local")}
+ >
+ Local workspace
+
+ setRuntime("cloud")}
+ >
+ Cloud workspace
+
+
+
+
+
+
+
+ Which cloud environment should it use?
+
+
+ Preview
+ Staging
+
+ Isolated sandbox
+
+
+
+
+
+
+
+ When should the agent request approval?
+
+
+
+ Before writing files
+
+
+ Before running commands
+
+
+ Only for sensitive actions
+
+
+
+
+
+
+
+ Next
+ Save execution plan
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-controlled.tsx b/apps/v4/examples/base/questionnaire-controlled.tsx
new file mode 100644
index 00000000000..698a685de0f
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-controlled.tsx
@@ -0,0 +1,127 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "checks", required: true },
+ { name: "output", required: true },
+] as const
+
+const itemLabels: Record = {
+ scope: "Change scope",
+ checks: "Verification",
+ output: "Final output",
+}
+
+export function QuestionnaireControlled() {
+ const [item, setItem] = React.useState("scope")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent workflow configured", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("checks") ?? "None"} · Output: ${formData.get("output") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+ Current checkpoint: {itemLabels[item]}
+
+
+
+
+
+
+ What may the agent change?
+
+ The host stores the active checkpoint while Questionnaire navigates.
+
+
+
+ Only the target component
+
+
+ Component and related tests
+
+
+ The complete feature area
+
+
+
+
+
+
+
+ Which verification level should it use?
+
+
+
+ Targeted tests
+
+
+ Package tests and typecheck
+
+
+ Full workspace verification
+
+
+
+
+
+
+
+ What should the agent return when finished?
+
+
+
+ Concise summary
+
+
+ Summary with changed files
+
+
+ Detailed implementation handoff
+
+
+
+
+
+
+
+ Next
+ Save workflow
+
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-demo.tsx b/apps/v4/examples/base/questionnaire-demo.tsx
new file mode 100644
index 00000000000..9482a5894f3
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-demo.tsx
@@ -0,0 +1,142 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const questionnaireItems = [
+ {
+ choices: [
+ {
+ description: "Show what the agent ran and what came back.",
+ label: "Tool call timeline",
+ value: "tool-calls",
+ },
+ {
+ description: "Ask before sensitive or destructive actions.",
+ label: "Approval checkpoints",
+ value: "approvals",
+ },
+ {
+ description: "Make delegated work and results easier to follow.",
+ label: "Sub-agent handoffs",
+ value: "handoffs",
+ },
+ ],
+ description: "Choose a direction or describe another task.",
+ input: {
+ label: "Another agent feature",
+ placeholder: "Describe another feature…",
+ },
+ name: "direction",
+ required: true,
+ title: "What should the agent build next?",
+ },
+ {
+ choices: [
+ { label: "Progress", value: "progress" },
+ { label: "Decisions", value: "decisions" },
+ { label: "Risks", value: "risks" },
+ { label: "Next step", value: "next-step" },
+ ],
+ description: "Select all that apply, or skip this question.",
+ multiple: true,
+ name: "signals",
+ required: false,
+ title: "What should every progress update include?",
+ },
+ {
+ choices: [
+ { label: "Start now", value: "now" },
+ { label: "Next development cycle", value: "next-cycle" },
+ { label: "Add it to the backlog", value: "backlog" },
+ ],
+ description: "Choose when the agent should begin the work.",
+ name: "timing",
+ required: true,
+ title: "When should work begin?",
+ },
+] as const
+
+export function QuestionnaireDemo() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ direction: formData.get("direction"),
+ signals: formData.getAll("signals"),
+ timing: formData.get("timing"),
+ }
+
+ toast("Agent plan saved", {
+ description: `Direction: ${answers.direction ?? "None"} · Progress signals: ${answers.signals.join(", ") || "None"} · Timing: ${answers.timing ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+ {questionnaireItems.map((question) => (
+
+ {question.title}
+
+ {question.description}
+
+
+ {question.choices.map((choice) => (
+
+ {choice.label}
+ {"description" in choice ? (
+
+ {choice.description}
+
+ ) : null}
+
+ ))}
+ {"input" in question ? (
+
+ ) : null}
+
+
+
+ ))}
+
+
+
+ Next
+ Save plan
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-dialog.tsx b/apps/v4/examples/base/questionnaire-dialog.tsx
new file mode 100644
index 00000000000..71ecc845b72
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-dialog.tsx
@@ -0,0 +1,124 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import { Button } from "@/styles/base-nova/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/styles/base-nova/ui/dialog"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "tests", required: true },
+] as const
+
+export function QuestionnaireDialog() {
+ const [open, setOpen] = React.useState(false)
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ setOpen(false)
+ toast("Clarification sent", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("tests") ?? "None"}`,
+ })
+ }
+
+ return (
+
+ }>
+ Open clarification
+
+
+
+
+
+
+ }>
+ Which files are in scope?
+
+ }>
+ Choose how broadly the agent can update the workspace.
+
+
+
+
+ Component only
+
+
+ Complete feature directory
+
+
+ Any related workspace file
+
+
+
+
+
+
+
+
+ }>
+ How much verification is needed?
+
+ }>
+ Choose the checks the agent should run before handoff.
+
+
+
+
+ Targeted tests
+
+
+ Package tests
+
+
+ Full workspace verification
+
+
+
+
+
+
+ }>
+ Cancel
+
+
+
+ Next
+ Send answer
+
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-freeform.tsx b/apps/v4/examples/base/questionnaire-freeform.tsx
new file mode 100644
index 00000000000..29899c34c7a
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-freeform.tsx
@@ -0,0 +1,79 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "incremental" },
+ { value: "module" },
+ { value: "rewrite" },
+ ],
+ name: "approach",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireFreeform() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const approach = new FormData(event.currentTarget).get("approach")
+
+ toast("Approach selected", {
+ description: `Approach: ${approach ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+ How should the agent approach this refactor?
+
+
+ Choose a strategy or write a more specific instruction.
+
+
+
+ Make the smallest safe change
+
+
+ Refactor one module at a time
+
+
+ Replace the implementation completely
+
+
+
+
+
+
+
+ Use this approach
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-multiple.tsx b/apps/v4/examples/base/questionnaire-multiple.tsx
new file mode 100644
index 00000000000..b27c109fb5f
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-multiple.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "source" },
+ { value: "tests" },
+ { value: "docs" },
+ { value: "history" },
+ ],
+ name: "context",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireMultiple() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const context = new FormData(event.currentTarget).getAll("context")
+
+ toast("Context selected", {
+ description: `Context: ${context.join(", ") || "None"}`,
+ })
+ }
+
+ return (
+
+
+
+ What context should the agent inspect?
+
+
+ Select every source that may affect the implementation.
+
+
+
+ Relevant source files
+
+
+ Existing tests
+
+
+ Architecture documentation
+
+
+ Recent commit history
+
+
+
+
+
+
+ Share context
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-navigation-state.tsx b/apps/v4/examples/base/questionnaire-navigation-state.tsx
new file mode 100644
index 00000000000..8c503ea5405
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-navigation-state.tsx
@@ -0,0 +1,119 @@
+"use client"
+
+import * as React from "react"
+import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "permission", required: true },
+ { name: "verification", required: true },
+] as const
+
+type ItemName = "permission" | "verification"
+
+export function QuestionnaireNavigationState() {
+ const [item, setItem] = React.useState("permission")
+ const [statuses, setStatuses] = React.useState<
+ Record
+ >({
+ permission: "unanswered",
+ verification: "unanswered",
+ })
+ const unanswered = statuses[item] === "unanswered"
+
+ function setStatus(name: ItemName, status: QuestionnaireItemStatus) {
+ setStatuses((current) => ({ ...current, [name]: status }))
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Permissions saved", {
+ description: `Permission: ${formData.get("permission") ?? "None"} · Verification: ${formData.get("verification") ?? "None"}`,
+ })
+ }
+
+ return (
+ setItem(nextItem as ItemName)}
+ onSubmit={handleSubmit}
+ >
+
+
+ setStatus("permission", status)}
+ >
+ What may the agent modify?
+
+ Next is intentionally disabled until an answer is selected.
+
+
+ Project files
+
+ Project files and tests
+
+
+ Files, tests, and configuration
+
+
+
+
+
+ setStatus("verification", status)}
+ >
+
+ What must pass before completion?
+
+
+ Tests
+
+ Tests and types
+
+
+ Tests, types, and visual QA
+
+
+
+
+
+
+
+
+ Next
+
+
+ Save permissions
+
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-progress.tsx b/apps/v4/examples/base/questionnaire-progress.tsx
new file mode 100644
index 00000000000..19f65f77471
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-progress.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "strategy", required: true },
+ { name: "tests", required: true },
+ { name: "delivery", required: true },
+] as const
+
+export function QuestionnaireProgressExample() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Pull request plan ready", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Commits: ${formData.get("strategy") ?? "None"} · Tests: ${formData.get("tests") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
+ })
+ }
+
+ return (
+
+ (
+
+
+ {Array.from({ length: state.total }, (_, index) => (
+
+ ))}
+
+
+ Checkpoint {state.current} of {state.total}
+
+
+ )}
+ />
+
+
+ How large is the change?
+
+ Small patch
+
+ Feature-sized change
+
+
+ Cross-package change
+
+
+
+
+
+
+
+ How should commits be organized?
+
+
+
+ Single commit
+
+
+ Logical commits
+
+
+ Squash before review
+
+
+
+
+
+
+ Which tests should run?
+
+
+ Targeted tests
+
+
+ Package suite
+
+
+ Full workspace
+
+
+
+
+
+
+
+ How should the work be delivered?
+
+
+ Patch only
+
+ Committed locally
+
+
+ Push a review branch
+
+
+
+
+
+
+
+ Next
+ Finish plan
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-resume.tsx b/apps/v4/examples/base/questionnaire-resume.tsx
new file mode 100644
index 00000000000..ad67100cfc4
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-resume.tsx
@@ -0,0 +1,115 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import { Button } from "@/styles/base-nova/ui/button"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "change", required: true },
+ { name: "verification", required: true },
+ { name: "notes" },
+] as const
+
+export function QuestionnaireResume() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ change: formData.get("change"),
+ verification: formData.getAll("verification"),
+ notes: formData.get("notes"),
+ }
+
+ toast("Draft updated", {
+ description: `Migration: ${answers.change ?? "None"} · Verification: ${answers.verification.join(", ") || "None"} · Notes: ${answers.notes || "None"}`,
+ })
+ }
+
+ return (
+ toast("Saved answers restored")}
+ onSubmit={handleSubmit}
+ >
+
+
+
+ What kind of migration is this?
+
+ This answer was saved during the previous session.
+
+
+
+ Incremental migration
+
+
+ Single cutover
+
+
+
+
+
+
+
+ How should the migration be verified?
+
+
+ These checks were selected during the previous session.
+
+
+
+ Run migration tests
+
+
+ Run the typecheck
+
+
+ Perform a manual smoke test
+
+
+
+
+
+
+
+ Anything else the agent should remember?
+
+
+ This note was saved with the draft.
+
+
+
+
+
+
+ Reset changes
+
+
+ Next
+ Update draft
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-shortcuts.tsx b/apps/v4/examples/base/questionnaire-shortcuts.tsx
new file mode 100644
index 00000000000..395d3239380
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-shortcuts.tsx
@@ -0,0 +1,96 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ NativeSelect,
+ NativeSelectOption,
+} from "@/styles/base-nova/ui/native-select"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [{ value: "inspect" }, { value: "tests" }, { value: "patch" }],
+ name: "action",
+ required: true,
+ },
+] as const
+
+type ShortcutMode = React.ComponentProps["shortcuts"]
+
+export function QuestionnaireShortcuts() {
+ const [shortcuts, setShortcuts] = React.useState("letters")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const action = new FormData(event.currentTarget).get("action")
+
+ toast("Next action selected", {
+ description: `Action: ${action ?? "None"} · Shortcuts: ${shortcuts ?? "none"}`,
+ })
+ }
+
+ return (
+
+ {
+ const value = event.target.value
+ setShortcuts(
+ value === "letters" || value === "numbers" ? value : undefined
+ )
+ }}
+ >
+ No shortcuts
+ Letters
+ Numbers
+
+
+
+
+
+ What should the agent do next?
+
+
+ Use the displayed shortcut or navigate with the keyboard.
+
+
+
+ Inspect the implementation
+
+
+ Run the relevant tests
+
+
+ Prepare the patch
+
+
+
+
+
+
+ Confirm action
+
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-skip.tsx b/apps/v4/examples/base/questionnaire-skip.tsx
new file mode 100644
index 00000000000..0407ae4ccf1
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-skip.tsx
@@ -0,0 +1,132 @@
+"use client"
+
+import * as React from "react"
+import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "task", required: true },
+ { name: "constraints" },
+ { name: "review", required: true },
+] as const
+
+export function QuestionnaireSkipExample() {
+ const [constraintStatus, setConstraintStatus] =
+ React.useState("unanswered")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ task: formData.get("task"),
+ constraints: formData.get("constraints"),
+ constraintStatus,
+ review: formData.get("review"),
+ }
+
+ toast("Agent brief submitted", {
+ description: `Task: ${answers.task ?? "None"} · Constraints: ${
+ answers.constraintStatus === "skipped"
+ ? "Skipped"
+ : (answers.constraints ?? "None")
+ } · Review: ${answers.review ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ What kind of change is this?
+
+ Choose the category that best describes the work.
+
+
+ New feature
+ Bug fix
+ Refactor
+
+
+
+
+
+
+ Are there any implementation constraints?
+
+
+ Answer if needed, or intentionally skip this question.
+
+
+
+ Do not add dependencies
+
+
+ Do not change the database
+
+
+ Preserve the public API
+
+
+
+
+
+
+
+ How should the work be reviewed?
+
+
+ Choose the checks the agent should complete before handoff.
+
+
+
+ Run the test suite
+
+
+ Review the final diff
+
+
+ Tests and diff review
+
+
+
+
+
+
+
+
+ Next
+ Submit brief
+
+
+ )
+}
diff --git a/apps/v4/examples/base/questionnaire-validation.tsx b/apps/v4/examples/base/questionnaire-validation.tsx
new file mode 100644
index 00000000000..eb7321cd23a
--- /dev/null
+++ b/apps/v4/examples/base/questionnaire-validation.tsx
@@ -0,0 +1,203 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from "@/styles/base-nova/ui/card"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/base-nova/ui/questionnaire"
+
+const items = [
+ { name: "detail", required: true },
+ { name: "audience", required: true },
+] as const
+
+const questionnaireSchema = z
+ .object({
+ detail: z.enum(["summary", "complete"]),
+ audience: z.enum(["team", "public"]),
+ })
+ .superRefine((answers, context) => {
+ if (answers.audience === "public" && answers.detail === "summary") {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ message:
+ "Public answers need enough context. Choose a complete answer.",
+ path: ["detail"],
+ })
+ }
+ })
+
+type QuestionnaireItemName = keyof z.infer
+type QuestionnaireErrors = Partial>
+
+function ValidationProgress() {
+ return (
+ (
+
+ {state.current} / {state.total}
+
+ )}
+ />
+ )
+}
+
+export function QuestionnaireValidation() {
+ const [item, setItem] = React.useState("detail")
+ const [errors, setErrors] = React.useState({})
+
+ function clearError(name: QuestionnaireItemName) {
+ setErrors((currentErrors) => {
+ if (!currentErrors[name]) {
+ return currentErrors
+ }
+
+ const nextErrors = { ...currentErrors }
+ delete nextErrors[name]
+ return nextErrors
+ })
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const result = questionnaireSchema.safeParse(
+ Object.fromEntries(new FormData(event.currentTarget))
+ )
+
+ if (result.success) {
+ setErrors({})
+ toast("Agent response configured", {
+ description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`,
+ })
+ return
+ }
+
+ const nextErrors: QuestionnaireErrors = {}
+
+ for (const issue of result.error.issues) {
+ const name = issue.path[0]
+
+ if ((name === "detail" || name === "audience") && !nextErrors[name]) {
+ nextErrors[name] = issue.message
+ }
+ }
+
+ const firstInvalidItem = result.error.issues[0]?.path[0]
+
+ setErrors(nextErrors)
+
+ if (firstInvalidItem === "detail" || firstInvalidItem === "audience") {
+ setItem(firstInvalidItem)
+ }
+ }
+
+ return (
+
+
+
+
+
+ How much detail should the answer include?
+
+
+ Choose the response depth.
+
+
+
+
+
+
+
+ clearError("detail")}
+ >
+ Concise summary
+
+ clearError("detail")}
+ >
+ Complete answer
+
+
+ {errors.detail}
+
+
+
+
+
+ Who will read the answer?
+
+ Public answers require complete context.
+
+
+
+
+
+
+
+ clearError("audience")}
+ >
+ My team
+
+ clearError("audience")}
+ >
+ Public audience
+
+
+ {errors.audience}
+
+
+
+
+
+
+ Next
+ Validate answers
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-animated.tsx b/apps/v4/examples/radix/questionnaire-animated.tsx
new file mode 100644
index 00000000000..07a57351fc9
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-animated.tsx
@@ -0,0 +1,118 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "task", required: true },
+ { name: "review", required: true },
+ { name: "delivery", required: true },
+] as const
+
+const itemClassName =
+ "data-active:animate-in data-active:fade-in-0 data-active:slide-in-from-bottom-2 data-active:duration-300 motion-reduce:animate-none"
+
+export function QuestionnaireAnimated() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent workflow saved", {
+ description: `Task: ${formData.get("task") ?? "None"} · Review: ${formData.get("review") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ What should the agent do?
+
+ Choose the task for this run.
+
+
+
+ Implement the requested change
+
+
+ Debug the current behavior
+
+
+ Review the implementation
+
+
+
+
+
+
+
+ How should the work be reviewed?
+
+
+ Select the verification depth.
+
+
+
+ Targeted checks
+
+
+ Complete test suite
+
+
+ Tests and manual QA
+
+
+
+
+
+
+
+ How should the result be delivered?
+
+
+ Choose the final handoff format.
+
+
+
+ Concise summary
+
+
+ Summary and changed files
+
+
+ Detailed review handoff
+
+
+
+
+
+
+
+ Next
+ Save workflow
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-card.tsx b/apps/v4/examples/radix/questionnaire-card.tsx
new file mode 100644
index 00000000000..42032f65330
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-card.tsx
@@ -0,0 +1,136 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/styles/radix-nova/ui/card"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [{ value: "fix" }, { value: "refactor" }, { value: "docs" }],
+ name: "task",
+ required: true,
+ },
+ {
+ choices: [{ value: "summary" }, { value: "files" }, { value: "review" }],
+ name: "output",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireCard() {
+ const taskTitleId = React.useId()
+ const outputTitleId = React.useId()
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent task created", {
+ description: `Task: ${formData.get("task") ?? "None"} · Handoff: ${formData.get("output") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ }>
+ What should the agent work on?
+
+ }>
+ Choose the task that should be handled next.
+
+
+
+
+
+
+
+
+ Fix the failing tests
+
+
+ Refactor the data layer
+
+
+ Update the integration guide
+
+
+
+
+
+
+
+
+ }>
+ What should the final handoff include?
+
+ }>
+ Pick the level of detail needed for review.
+
+
+
+
+
+
+
+
+ Summary only
+
+
+ Summary and changed files
+
+
+ Full review handoff
+
+
+
+
+
+
+
+
+
+ Next
+ Create task
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-conditional.tsx b/apps/v4/examples/radix/questionnaire-conditional.tsx
new file mode 100644
index 00000000000..5b7b885fac9
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-conditional.tsx
@@ -0,0 +1,122 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+export function QuestionnaireConditional() {
+ const [runtime, setRuntime] = React.useState("local")
+ const items = React.useMemo(
+ () => [
+ { name: "runtime", required: true },
+ {
+ disabled: runtime !== "cloud",
+ name: "environment",
+ required: true,
+ },
+ { name: "approval", required: true },
+ ],
+ [runtime]
+ )
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Execution plan saved", {
+ description: `Runtime: ${formData.get("runtime") ?? "None"} · Environment: ${formData.get("environment") ?? "Not applicable"} · Approval: ${formData.get("approval") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ Where should the agent run?
+
+ Cloud runs add an environment question to this flow.
+
+
+ setRuntime("local")}
+ >
+ Local workspace
+
+ setRuntime("cloud")}
+ >
+ Cloud workspace
+
+
+
+
+
+
+
+ Which cloud environment should it use?
+
+
+ Preview
+ Staging
+
+ Isolated sandbox
+
+
+
+
+
+
+
+ When should the agent request approval?
+
+
+
+ Before writing files
+
+
+ Before running commands
+
+
+ Only for sensitive actions
+
+
+
+
+
+
+
+ Next
+ Save execution plan
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-controlled.tsx b/apps/v4/examples/radix/questionnaire-controlled.tsx
new file mode 100644
index 00000000000..f9b3a5dd263
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-controlled.tsx
@@ -0,0 +1,127 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "checks", required: true },
+ { name: "output", required: true },
+] as const
+
+const itemLabels: Record = {
+ scope: "Change scope",
+ checks: "Verification",
+ output: "Final output",
+}
+
+export function QuestionnaireControlled() {
+ const [item, setItem] = React.useState("scope")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Agent workflow configured", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("checks") ?? "None"} · Output: ${formData.get("output") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+ Current checkpoint: {itemLabels[item]}
+
+
+
+
+
+
+ What may the agent change?
+
+ The host stores the active checkpoint while Questionnaire navigates.
+
+
+
+ Only the target component
+
+
+ Component and related tests
+
+
+ The complete feature area
+
+
+
+
+
+
+
+ Which verification level should it use?
+
+
+
+ Targeted tests
+
+
+ Package tests and typecheck
+
+
+ Full workspace verification
+
+
+
+
+
+
+
+ What should the agent return when finished?
+
+
+
+ Concise summary
+
+
+ Summary with changed files
+
+
+ Detailed implementation handoff
+
+
+
+
+
+
+
+ Next
+ Save workflow
+
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-demo.tsx b/apps/v4/examples/radix/questionnaire-demo.tsx
new file mode 100644
index 00000000000..3f70b9a692e
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-demo.tsx
@@ -0,0 +1,150 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "tool-calls" },
+ { value: "approvals" },
+ { value: "handoffs" },
+ ],
+ name: "direction",
+ required: true,
+ },
+ {
+ choices: [
+ { value: "progress" },
+ { value: "decisions" },
+ { value: "risks" },
+ { value: "next-step" },
+ ],
+ name: "signals",
+ },
+ {
+ choices: [{ value: "now" }, { value: "next-cycle" }, { value: "backlog" }],
+ name: "timing",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireDemo() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ direction: formData.get("direction"),
+ signals: formData.getAll("signals"),
+ timing: formData.get("timing"),
+ }
+
+ toast("Agent plan saved", {
+ description: `Direction: ${answers.direction ?? "None"} · Progress signals: ${answers.signals.join(", ") || "None"} · Timing: ${answers.timing ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+
+ What should the agent build next?
+
+
+ Choose a direction or describe another task.
+
+
+
+ Tool call timeline
+
+ Show what the agent ran and what came back.
+
+
+
+ Approval checkpoints
+
+ Ask before sensitive or destructive actions.
+
+
+
+ Sub-agent handoffs
+
+ Make delegated work and results easier to follow.
+
+
+
+
+
+
+
+
+
+ What should every progress update include?
+
+
+ Select all that apply, or skip this question.
+
+
+ Progress
+ Decisions
+ Risks
+ Next step
+
+
+
+
+
+ When should work begin?
+
+ Choose when the agent should begin the work.
+
+
+ Start now
+
+ Next development cycle
+
+
+ Add it to the backlog
+
+
+
+
+
+
+
+
+ Next
+ Save plan
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-dialog.tsx b/apps/v4/examples/radix/questionnaire-dialog.tsx
new file mode 100644
index 00000000000..d0a72ffb11c
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-dialog.tsx
@@ -0,0 +1,126 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import { Button } from "@/styles/radix-nova/ui/button"
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/styles/radix-nova/ui/dialog"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "tests", required: true },
+] as const
+
+export function QuestionnaireDialog() {
+ const [open, setOpen] = React.useState(false)
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ setOpen(false)
+ toast("Clarification sent", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Verification: ${formData.get("tests") ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+ Open clarification
+
+
+
+
+
+
+ }>
+ Which files are in scope?
+
+ }>
+ Choose how broadly the agent can update the workspace.
+
+
+
+
+ Component only
+
+
+ Complete feature directory
+
+
+ Any related workspace file
+
+
+
+
+
+
+
+
+ }>
+ How much verification is needed?
+
+ }>
+ Choose the checks the agent should run before handoff.
+
+
+
+
+ Targeted tests
+
+
+ Package tests
+
+
+ Full workspace verification
+
+
+
+
+
+
+
+
+ Cancel
+
+
+
+
+ Next
+ Send answer
+
+
+
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-freeform.tsx b/apps/v4/examples/radix/questionnaire-freeform.tsx
new file mode 100644
index 00000000000..3eecab5cb81
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-freeform.tsx
@@ -0,0 +1,79 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "incremental" },
+ { value: "module" },
+ { value: "rewrite" },
+ ],
+ name: "approach",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireFreeform() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const approach = new FormData(event.currentTarget).get("approach")
+
+ toast("Approach selected", {
+ description: `Approach: ${approach ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+ How should the agent approach this refactor?
+
+
+ Choose a strategy or write a more specific instruction.
+
+
+
+ Make the smallest safe change
+
+
+ Refactor one module at a time
+
+
+ Replace the implementation completely
+
+
+
+
+
+
+
+ Use this approach
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-multiple.tsx b/apps/v4/examples/radix/questionnaire-multiple.tsx
new file mode 100644
index 00000000000..a33dff070d9
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-multiple.tsx
@@ -0,0 +1,78 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [
+ { value: "source" },
+ { value: "tests" },
+ { value: "docs" },
+ { value: "history" },
+ ],
+ name: "context",
+ required: true,
+ },
+] as const
+
+export function QuestionnaireMultiple() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const context = new FormData(event.currentTarget).getAll("context")
+
+ toast("Context selected", {
+ description: `Context: ${context.join(", ") || "None"}`,
+ })
+ }
+
+ return (
+
+
+
+ What context should the agent inspect?
+
+
+ Select every source that may affect the implementation.
+
+
+
+ Relevant source files
+
+
+ Existing tests
+
+
+ Architecture documentation
+
+
+ Recent commit history
+
+
+
+
+
+
+ Share context
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-navigation-state.tsx b/apps/v4/examples/radix/questionnaire-navigation-state.tsx
new file mode 100644
index 00000000000..2c681d45238
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-navigation-state.tsx
@@ -0,0 +1,119 @@
+"use client"
+
+import * as React from "react"
+import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "permission", required: true },
+ { name: "verification", required: true },
+] as const
+
+type ItemName = "permission" | "verification"
+
+export function QuestionnaireNavigationState() {
+ const [item, setItem] = React.useState("permission")
+ const [statuses, setStatuses] = React.useState<
+ Record
+ >({
+ permission: "unanswered",
+ verification: "unanswered",
+ })
+ const unanswered = statuses[item] === "unanswered"
+
+ function setStatus(name: ItemName, status: QuestionnaireItemStatus) {
+ setStatuses((current) => ({ ...current, [name]: status }))
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Permissions saved", {
+ description: `Permission: ${formData.get("permission") ?? "None"} · Verification: ${formData.get("verification") ?? "None"}`,
+ })
+ }
+
+ return (
+ setItem(nextItem as ItemName)}
+ onSubmit={handleSubmit}
+ >
+
+
+ setStatus("permission", status)}
+ >
+ What may the agent modify?
+
+ Next is intentionally disabled until an answer is selected.
+
+
+ Project files
+
+ Project files and tests
+
+
+ Files, tests, and configuration
+
+
+
+
+
+ setStatus("verification", status)}
+ >
+
+ What must pass before completion?
+
+
+ Tests
+
+ Tests and types
+
+
+ Tests, types, and visual QA
+
+
+
+
+
+
+
+
+ Next
+
+
+ Save permissions
+
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-progress.tsx b/apps/v4/examples/radix/questionnaire-progress.tsx
new file mode 100644
index 00000000000..3fa99481781
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-progress.tsx
@@ -0,0 +1,139 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "scope", required: true },
+ { name: "strategy", required: true },
+ { name: "tests", required: true },
+ { name: "delivery", required: true },
+] as const
+
+export function QuestionnaireProgressExample() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+
+ toast("Pull request plan ready", {
+ description: `Scope: ${formData.get("scope") ?? "None"} · Commits: ${formData.get("strategy") ?? "None"} · Tests: ${formData.get("tests") ?? "None"} · Delivery: ${formData.get("delivery") ?? "None"}`,
+ })
+ }
+
+ return (
+
+ (
+
+
+ {Array.from({ length: state.total }, (_, index) => (
+
+ ))}
+
+
+ Checkpoint {state.current} of {state.total}
+
+
+ )}
+ />
+
+
+ How large is the change?
+
+ Small patch
+
+ Feature-sized change
+
+
+ Cross-package change
+
+
+
+
+
+
+
+ How should commits be organized?
+
+
+
+ Single commit
+
+
+ Logical commits
+
+
+ Squash before review
+
+
+
+
+
+
+ Which tests should run?
+
+
+ Targeted tests
+
+
+ Package suite
+
+
+ Full workspace
+
+
+
+
+
+
+
+ How should the work be delivered?
+
+
+ Patch only
+
+ Committed locally
+
+
+ Push a review branch
+
+
+
+
+
+
+
+ Next
+ Finish plan
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-resume.tsx b/apps/v4/examples/radix/questionnaire-resume.tsx
new file mode 100644
index 00000000000..b371870b3ec
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-resume.tsx
@@ -0,0 +1,115 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import { Button } from "@/styles/radix-nova/ui/button"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "change", required: true },
+ { name: "verification", required: true },
+ { name: "notes" },
+] as const
+
+export function QuestionnaireResume() {
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ change: formData.get("change"),
+ verification: formData.getAll("verification"),
+ notes: formData.get("notes"),
+ }
+
+ toast("Draft updated", {
+ description: `Migration: ${answers.change ?? "None"} · Verification: ${answers.verification.join(", ") || "None"} · Notes: ${answers.notes || "None"}`,
+ })
+ }
+
+ return (
+ toast("Saved answers restored")}
+ onSubmit={handleSubmit}
+ >
+
+
+
+ What kind of migration is this?
+
+ This answer was saved during the previous session.
+
+
+
+ Incremental migration
+
+
+ Single cutover
+
+
+
+
+
+
+
+ How should the migration be verified?
+
+
+ These checks were selected during the previous session.
+
+
+
+ Run migration tests
+
+
+ Run the typecheck
+
+
+ Perform a manual smoke test
+
+
+
+
+
+
+
+ Anything else the agent should remember?
+
+
+ This note was saved with the draft.
+
+
+
+
+
+
+ Reset changes
+
+
+ Next
+ Update draft
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-shortcuts.tsx b/apps/v4/examples/radix/questionnaire-shortcuts.tsx
new file mode 100644
index 00000000000..d54ca7ddcbc
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-shortcuts.tsx
@@ -0,0 +1,96 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ NativeSelect,
+ NativeSelectOption,
+} from "@/styles/radix-nova/ui/native-select"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ {
+ choices: [{ value: "inspect" }, { value: "tests" }, { value: "patch" }],
+ name: "action",
+ required: true,
+ },
+] as const
+
+type ShortcutMode = React.ComponentProps["shortcuts"]
+
+export function QuestionnaireShortcuts() {
+ const [shortcuts, setShortcuts] = React.useState("letters")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const action = new FormData(event.currentTarget).get("action")
+
+ toast("Next action selected", {
+ description: `Action: ${action ?? "None"} · Shortcuts: ${shortcuts ?? "none"}`,
+ })
+ }
+
+ return (
+
+ {
+ const value = event.target.value
+ setShortcuts(
+ value === "letters" || value === "numbers" ? value : undefined
+ )
+ }}
+ >
+ No shortcuts
+ Letters
+ Numbers
+
+
+
+
+
+ What should the agent do next?
+
+
+ Use the displayed shortcut or navigate with the keyboard.
+
+
+
+ Inspect the implementation
+
+
+ Run the relevant tests
+
+
+ Prepare the patch
+
+
+
+
+
+
+ Confirm action
+
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-skip.tsx b/apps/v4/examples/radix/questionnaire-skip.tsx
new file mode 100644
index 00000000000..85fc63a6e36
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-skip.tsx
@@ -0,0 +1,132 @@
+"use client"
+
+import * as React from "react"
+import type { QuestionnaireItemStatus } from "@shadcn/react/questionnaire"
+import { toast } from "sonner"
+
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "task", required: true },
+ { name: "constraints" },
+ { name: "review", required: true },
+] as const
+
+export function QuestionnaireSkipExample() {
+ const [constraintStatus, setConstraintStatus] =
+ React.useState("unanswered")
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const answers = {
+ task: formData.get("task"),
+ constraints: formData.get("constraints"),
+ constraintStatus,
+ review: formData.get("review"),
+ }
+
+ toast("Agent brief submitted", {
+ description: `Task: ${answers.task ?? "None"} · Constraints: ${
+ answers.constraintStatus === "skipped"
+ ? "Skipped"
+ : (answers.constraints ?? "None")
+ } · Review: ${answers.review ?? "None"}`,
+ })
+ }
+
+ return (
+
+
+
+
+ What kind of change is this?
+
+ Choose the category that best describes the work.
+
+
+ New feature
+ Bug fix
+ Refactor
+
+
+
+
+
+
+ Are there any implementation constraints?
+
+
+ Answer if needed, or intentionally skip this question.
+
+
+
+ Do not add dependencies
+
+
+ Do not change the database
+
+
+ Preserve the public API
+
+
+
+
+
+
+
+ How should the work be reviewed?
+
+
+ Choose the checks the agent should complete before handoff.
+
+
+
+ Run the test suite
+
+
+ Review the final diff
+
+
+ Tests and diff review
+
+
+
+
+
+
+
+
+ Next
+ Submit brief
+
+
+ )
+}
diff --git a/apps/v4/examples/radix/questionnaire-validation.tsx b/apps/v4/examples/radix/questionnaire-validation.tsx
new file mode 100644
index 00000000000..0a789c6d4d8
--- /dev/null
+++ b/apps/v4/examples/radix/questionnaire-validation.tsx
@@ -0,0 +1,203 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+import { z } from "zod"
+
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardFooter,
+ CardHeader,
+} from "@/styles/radix-nova/ui/card"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/styles/radix-nova/ui/questionnaire"
+
+const items = [
+ { name: "detail", required: true },
+ { name: "audience", required: true },
+] as const
+
+const questionnaireSchema = z
+ .object({
+ detail: z.enum(["summary", "complete"]),
+ audience: z.enum(["team", "public"]),
+ })
+ .superRefine((answers, context) => {
+ if (answers.audience === "public" && answers.detail === "summary") {
+ context.addIssue({
+ code: z.ZodIssueCode.custom,
+ message:
+ "Public answers need enough context. Choose a complete answer.",
+ path: ["detail"],
+ })
+ }
+ })
+
+type QuestionnaireItemName = keyof z.infer
+type QuestionnaireErrors = Partial>
+
+function ValidationProgress() {
+ return (
+ (
+
+ {state.current} / {state.total}
+
+ )}
+ />
+ )
+}
+
+export function QuestionnaireValidation() {
+ const [item, setItem] = React.useState("detail")
+ const [errors, setErrors] = React.useState({})
+
+ function clearError(name: QuestionnaireItemName) {
+ setErrors((currentErrors) => {
+ if (!currentErrors[name]) {
+ return currentErrors
+ }
+
+ const nextErrors = { ...currentErrors }
+ delete nextErrors[name]
+ return nextErrors
+ })
+ }
+
+ function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const result = questionnaireSchema.safeParse(
+ Object.fromEntries(new FormData(event.currentTarget))
+ )
+
+ if (result.success) {
+ setErrors({})
+ toast("Agent response configured", {
+ description: `Detail: ${result.data.detail} · Audience: ${result.data.audience}`,
+ })
+ return
+ }
+
+ const nextErrors: QuestionnaireErrors = {}
+
+ for (const issue of result.error.issues) {
+ const name = issue.path[0]
+
+ if ((name === "detail" || name === "audience") && !nextErrors[name]) {
+ nextErrors[name] = issue.message
+ }
+ }
+
+ const firstInvalidItem = result.error.issues[0]?.path[0]
+
+ setErrors(nextErrors)
+
+ if (firstInvalidItem === "detail" || firstInvalidItem === "audience") {
+ setItem(firstInvalidItem)
+ }
+ }
+
+ return (
+
+
+
+
+
+ How much detail should the answer include?
+
+
+ Choose the response depth.
+
+
+
+
+
+
+
+ clearError("detail")}
+ >
+ Concise summary
+
+ clearError("detail")}
+ >
+ Complete answer
+
+
+ {errors.detail}
+
+
+
+
+
+ Who will read the answer?
+
+ Public answers require complete context.
+
+
+
+
+
+
+
+ clearError("audience")}
+ >
+ My team
+
+ clearError("audience")}
+ >
+ Public audience
+
+
+ {errors.audience}
+
+
+
+
+
+
+ Next
+ Validate answers
+
+
+
+
+ )
+}
diff --git a/apps/v4/lib/docs.ts b/apps/v4/lib/docs.ts
index 64d617b3dbb..b92ddf21846 100644
--- a/apps/v4/lib/docs.ts
+++ b/apps/v4/lib/docs.ts
@@ -1,22 +1,11 @@
export const PAGES_NEW = [
- "/docs/typeset",
- "/docs/utils/scroll-fade",
- "/docs/utils/shimmer",
- "/docs/components/radix/attachment",
- "/docs/components/base/attachment",
- "/docs/components/radix/bubble",
- "/docs/components/base/bubble",
- "/docs/components/radix/message-scroller",
- "/docs/components/base/message-scroller",
- "/docs/components/radix/marker",
- "/docs/components/base/marker",
- "/docs/components/radix/message",
- "/docs/components/base/message",
- "/docs/components/base/toast",
- "/docs/helpers/ai-sdk",
- "/docs/helpers/tanstack-ai",
- "/docs/react/message-scroller",
+ "/docs/changelog",
+ "/docs/changelog/2026-08-questionnaire",
"/docs/registry/dynamic-search",
+ "/docs/components/radix/questionnaire",
+ "/docs/components/base/questionnaire",
+ "/docs/components/aria/questionnaire",
+ "/docs/react/questionnaire",
]
export const PAGES_UPDATED = []
diff --git a/apps/v4/mdx-components.tsx b/apps/v4/mdx-components.tsx
index 86df36e5de8..402f320abbe 100644
--- a/apps/v4/mdx-components.tsx
+++ b/apps/v4/mdx-components.tsx
@@ -175,7 +175,7 @@ export const mdxComponents = {
// Typeset tables stay real tables and wrap to fit; wrap them to scroll
// wide ones horizontally instead.
table: (props: React.ComponentProps<"table">) => (
-
+
),
diff --git a/apps/v4/package.json b/apps/v4/package.json
index 48ed248e9e7..105e2ff8e0c 100644
--- a/apps/v4/package.json
+++ b/apps/v4/package.json
@@ -90,7 +90,7 @@
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"server-only": "^0.0.1",
- "shadcn": "4.16.1",
+ "shadcn": "4.16.2",
"shiki": "^3.23.0",
"sonner": "^2.0.0",
"streamdown": "^2.5.0",
diff --git a/apps/v4/public/r/index.json b/apps/v4/public/r/index.json
index 3f1a6ac6eb9..71cb0349bc3 100644
--- a/apps/v4/public/r/index.json
+++ b/apps/v4/public/r/index.json
@@ -1129,6 +1129,34 @@
}
}
},
+ {
+ "name": "questionnaire",
+ "type": "registry:ui",
+ "dependencies": ["@shadcn/react"],
+ "registryDependencies": ["button"],
+ "files": [
+ {
+ "path": "ui/questionnaire.tsx",
+ "type": "registry:ui"
+ }
+ ],
+ "meta": {
+ "links": {
+ "base": {
+ "docs": "https://ui.shadcn.com/docs/components/base/questionnaire",
+ "examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx"
+ },
+ "aria": {
+ "docs": "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ "examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx"
+ },
+ "radix": {
+ "docs": "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ "examples": "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx"
+ }
+ }
+ }
+ },
{
"name": "radio-group",
"type": "registry:ui",
diff --git a/apps/v4/registry/__components__/aria-luma.tsx b/apps/v4/registry/__components__/aria-luma.tsx
index 41f28490c01..71e74c891f8 100644
--- a/apps/v4/registry/__components__/aria-luma.tsx
+++ b/apps/v4/registry/__components__/aria-luma.tsx
@@ -446,6 +446,14 @@ export const Components: Record
= {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-luma/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-luma/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-lyra.tsx b/apps/v4/registry/__components__/aria-lyra.tsx
index 9cfa9c8b670..422ba3fc8d3 100644
--- a/apps/v4/registry/__components__/aria-lyra.tsx
+++ b/apps/v4/registry/__components__/aria-lyra.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-lyra/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-lyra/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-maia.tsx b/apps/v4/registry/__components__/aria-maia.tsx
index 07405401ea7..e20520649ff 100644
--- a/apps/v4/registry/__components__/aria-maia.tsx
+++ b/apps/v4/registry/__components__/aria-maia.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-maia/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-maia/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-mira.tsx b/apps/v4/registry/__components__/aria-mira.tsx
index d8d477a4d4a..be587de4c5d 100644
--- a/apps/v4/registry/__components__/aria-mira.tsx
+++ b/apps/v4/registry/__components__/aria-mira.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-mira/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-mira/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-nova.tsx b/apps/v4/registry/__components__/aria-nova.tsx
index 578673e2f95..612f5f59000 100644
--- a/apps/v4/registry/__components__/aria-nova.tsx
+++ b/apps/v4/registry/__components__/aria-nova.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-nova/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-nova/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-rhea.tsx b/apps/v4/registry/__components__/aria-rhea.tsx
index 5ef9de995aa..4c015d6d8ab 100644
--- a/apps/v4/registry/__components__/aria-rhea.tsx
+++ b/apps/v4/registry/__components__/aria-rhea.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-rhea/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-rhea/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-sera.tsx b/apps/v4/registry/__components__/aria-sera.tsx
index d11a19a000f..c4a6362c7d6 100644
--- a/apps/v4/registry/__components__/aria-sera.tsx
+++ b/apps/v4/registry/__components__/aria-sera.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-sera/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-sera/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/aria-vega.tsx b/apps/v4/registry/__components__/aria-vega.tsx
index 1c75ee10159..60a27c9b959 100644
--- a/apps/v4/registry/__components__/aria-vega.tsx
+++ b/apps/v4/registry/__components__/aria-vega.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/aria-vega/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/aria-vega/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-luma.tsx b/apps/v4/registry/__components__/base-luma.tsx
index c13e5779682..64d45b631e3 100644
--- a/apps/v4/registry/__components__/base-luma.tsx
+++ b/apps/v4/registry/__components__/base-luma.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-luma/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-luma/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-lyra.tsx b/apps/v4/registry/__components__/base-lyra.tsx
index b6595853496..9cee70661f8 100644
--- a/apps/v4/registry/__components__/base-lyra.tsx
+++ b/apps/v4/registry/__components__/base-lyra.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-lyra/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-lyra/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-maia.tsx b/apps/v4/registry/__components__/base-maia.tsx
index e5ace6cddd7..87b158ae51a 100644
--- a/apps/v4/registry/__components__/base-maia.tsx
+++ b/apps/v4/registry/__components__/base-maia.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-maia/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-maia/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-mira.tsx b/apps/v4/registry/__components__/base-mira.tsx
index e75813aea4a..b2051be19e0 100644
--- a/apps/v4/registry/__components__/base-mira.tsx
+++ b/apps/v4/registry/__components__/base-mira.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-mira/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-mira/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-nova.tsx b/apps/v4/registry/__components__/base-nova.tsx
index 465182155af..edeb7d554ca 100644
--- a/apps/v4/registry/__components__/base-nova.tsx
+++ b/apps/v4/registry/__components__/base-nova.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-nova/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-nova/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-rhea.tsx b/apps/v4/registry/__components__/base-rhea.tsx
index c8f447f0d13..f5175fed7ac 100644
--- a/apps/v4/registry/__components__/base-rhea.tsx
+++ b/apps/v4/registry/__components__/base-rhea.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-rhea/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-rhea/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-sera.tsx b/apps/v4/registry/__components__/base-sera.tsx
index c058aecd10b..e6f4c3bc534 100644
--- a/apps/v4/registry/__components__/base-sera.tsx
+++ b/apps/v4/registry/__components__/base-sera.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-sera/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-sera/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/base-vega.tsx b/apps/v4/registry/__components__/base-vega.tsx
index cf1c658eed0..89036373ca0 100644
--- a/apps/v4/registry/__components__/base-vega.tsx
+++ b/apps/v4/registry/__components__/base-vega.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/base-vega/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/base-vega/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/index.tsx b/apps/v4/registry/__components__/index.tsx
index e2b0ed13ae2..b4e0ef3df28 100644
--- a/apps/v4/registry/__components__/index.tsx
+++ b/apps/v4/registry/__components__/index.tsx
@@ -478,6 +478,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -543,6 +544,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -605,6 +607,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -671,6 +674,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -737,6 +741,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -803,6 +808,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -869,6 +875,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -935,6 +942,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1001,6 +1009,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1067,6 +1076,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1129,6 +1139,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1191,6 +1202,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1253,6 +1265,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1315,6 +1328,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1377,6 +1391,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1439,6 +1454,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1501,6 +1517,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1566,6 +1583,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1631,6 +1649,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1696,6 +1715,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1761,6 +1781,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1826,6 +1847,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1891,6 +1913,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
@@ -1956,6 +1979,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
]),
diff --git a/apps/v4/registry/__components__/radix-luma.tsx b/apps/v4/registry/__components__/radix-luma.tsx
index fb560606f34..0046683dfc6 100644
--- a/apps/v4/registry/__components__/radix-luma.tsx
+++ b/apps/v4/registry/__components__/radix-luma.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-luma/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-luma/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-lyra.tsx b/apps/v4/registry/__components__/radix-lyra.tsx
index 29eea35ab5c..bef7d57e8c5 100644
--- a/apps/v4/registry/__components__/radix-lyra.tsx
+++ b/apps/v4/registry/__components__/radix-lyra.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-lyra/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-lyra/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-maia.tsx b/apps/v4/registry/__components__/radix-maia.tsx
index 4dfb14f6a47..2eeaca57254 100644
--- a/apps/v4/registry/__components__/radix-maia.tsx
+++ b/apps/v4/registry/__components__/radix-maia.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-maia/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-maia/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-mira.tsx b/apps/v4/registry/__components__/radix-mira.tsx
index 88a092af562..d5b9ee5997e 100644
--- a/apps/v4/registry/__components__/radix-mira.tsx
+++ b/apps/v4/registry/__components__/radix-mira.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-mira/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-mira/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-nova.tsx b/apps/v4/registry/__components__/radix-nova.tsx
index 1937135bfe1..2264bb24488 100644
--- a/apps/v4/registry/__components__/radix-nova.tsx
+++ b/apps/v4/registry/__components__/radix-nova.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-nova/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-nova/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-rhea.tsx b/apps/v4/registry/__components__/radix-rhea.tsx
index aa83e42150c..5d9b7a0386e 100644
--- a/apps/v4/registry/__components__/radix-rhea.tsx
+++ b/apps/v4/registry/__components__/radix-rhea.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-rhea/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-rhea/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-sera.tsx b/apps/v4/registry/__components__/radix-sera.tsx
index dfd0a013c13..176f6a16bb1 100644
--- a/apps/v4/registry/__components__/radix-sera.tsx
+++ b/apps/v4/registry/__components__/radix-sera.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-sera/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-sera/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__components__/radix-vega.tsx b/apps/v4/registry/__components__/radix-vega.tsx
index 69be1c2e322..62dc14c6eb0 100644
--- a/apps/v4/registry/__components__/radix-vega.tsx
+++ b/apps/v4/registry/__components__/radix-vega.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/styles/radix-vega/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/styles/radix-vega/ui/marker")
const exportName =
diff --git a/apps/v4/registry/__index__.tsx b/apps/v4/registry/__index__.tsx
index c9d0f3e75f9..7f38046dd3f 100644
--- a/apps/v4/registry/__index__.tsx
+++ b/apps/v4/registry/__index__.tsx
@@ -8271,6 +8271,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-nova/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -9604,6 +9626,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-nova/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -10890,6 +10934,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-nova/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -12257,6 +12323,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-vega/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -13612,6 +13700,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-maia/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -14967,6 +15077,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-lyra/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -16322,6 +16454,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-mira/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -17677,6 +17831,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-luma/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -19032,6 +19208,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-sera/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -20387,6 +20585,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/base-rhea/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -21673,6 +21893,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-vega/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -22971,6 +23213,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-maia/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -24269,6 +24533,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-lyra/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -25567,6 +25853,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-mira/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -26865,6 +27173,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-luma/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -28163,6 +28493,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-sera/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -29461,6 +29813,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/aria-rhea/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -30806,6 +31180,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-vega/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -32139,6 +32535,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-maia/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -33472,6 +33890,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-lyra/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -34805,6 +35245,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-mira/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -36138,6 +36600,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-luma/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -37471,6 +37955,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-sera/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -38804,6 +39310,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "styles/radix-rhea/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
diff --git a/apps/v4/registry/bases/__components__/aria.tsx b/apps/v4/registry/bases/__components__/aria.tsx
index 60b10abccfc..23ec96c4438 100644
--- a/apps/v4/registry/bases/__components__/aria.tsx
+++ b/apps/v4/registry/bases/__components__/aria.tsx
@@ -446,6 +446,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/registry/bases/aria/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/registry/bases/aria/ui/marker")
const exportName =
@@ -970,6 +978,16 @@ export const Components: Record = {
) || "message-scroller-example"
return { default: mod.default || mod[exportName] }
}),
+ "questionnaire-example": React.lazy(async () => {
+ const mod = await import(
+ "@/registry/bases/aria/examples/questionnaire-example"
+ )
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-example"
+ return { default: mod.default || mod[exportName] }
+ }),
"marker-example": React.lazy(async () => {
const mod = await import("@/registry/bases/aria/examples/marker-example")
const exportName =
diff --git a/apps/v4/registry/bases/__components__/base.tsx b/apps/v4/registry/bases/__components__/base.tsx
index 2b63eaa3216..3ecd580a029 100644
--- a/apps/v4/registry/bases/__components__/base.tsx
+++ b/apps/v4/registry/bases/__components__/base.tsx
@@ -478,6 +478,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/registry/bases/base/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/registry/bases/base/ui/marker")
const exportName =
@@ -1038,6 +1046,16 @@ export const Components: Record = {
) || "message-scroller-example"
return { default: mod.default || mod[exportName] }
}),
+ "questionnaire-example": React.lazy(async () => {
+ const mod = await import(
+ "@/registry/bases/base/examples/questionnaire-example"
+ )
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-example"
+ return { default: mod.default || mod[exportName] }
+ }),
"marker-example": React.lazy(async () => {
const mod = await import("@/registry/bases/base/examples/marker-example")
const exportName =
diff --git a/apps/v4/registry/bases/__components__/index.tsx b/apps/v4/registry/bases/__components__/index.tsx
index cb56d4a7756..8d29426d6bc 100644
--- a/apps/v4/registry/bases/__components__/index.tsx
+++ b/apps/v4/registry/bases/__components__/index.tsx
@@ -74,6 +74,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
"accordion-example",
@@ -139,6 +140,7 @@ const shards: Record<
"attachment-example",
"bubble-example",
"message-scroller-example",
+ "questionnaire-example",
"marker-example",
"message-example",
"utils",
@@ -234,6 +236,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
"accordion-example",
@@ -295,6 +298,7 @@ const shards: Record<
"attachment-example",
"bubble-example",
"message-scroller-example",
+ "questionnaire-example",
"marker-example",
"message-example",
"utils",
@@ -393,6 +397,7 @@ const shards: Record<
"attachment",
"bubble",
"message-scroller",
+ "questionnaire",
"marker",
"message",
"accordion-example",
@@ -457,6 +462,7 @@ const shards: Record<
"attachment-example",
"bubble-example",
"message-scroller-example",
+ "questionnaire-example",
"marker-example",
"message-example",
"utils",
diff --git a/apps/v4/registry/bases/__components__/radix.tsx b/apps/v4/registry/bases/__components__/radix.tsx
index 5db8338fec8..d9c728188ae 100644
--- a/apps/v4/registry/bases/__components__/radix.tsx
+++ b/apps/v4/registry/bases/__components__/radix.tsx
@@ -470,6 +470,14 @@ export const Components: Record = {
) || "message-scroller"
return { default: mod.default || mod[exportName] }
}),
+ questionnaire: React.lazy(async () => {
+ const mod = await import("@/registry/bases/radix/ui/questionnaire")
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire"
+ return { default: mod.default || mod[exportName] }
+ }),
marker: React.lazy(async () => {
const mod = await import("@/registry/bases/radix/ui/marker")
const exportName =
@@ -1032,6 +1040,16 @@ export const Components: Record = {
) || "message-scroller-example"
return { default: mod.default || mod[exportName] }
}),
+ "questionnaire-example": React.lazy(async () => {
+ const mod = await import(
+ "@/registry/bases/radix/examples/questionnaire-example"
+ )
+ const exportName =
+ Object.keys(mod).find(
+ (key) => typeof mod[key] === "function" || typeof mod[key] === "object"
+ ) || "questionnaire-example"
+ return { default: mod.default || mod[exportName] }
+ }),
"marker-example": React.lazy(async () => {
const mod = await import("@/registry/bases/radix/examples/marker-example")
const exportName =
diff --git a/apps/v4/registry/bases/__index__.tsx b/apps/v4/registry/bases/__index__.tsx
index 467ab9923de..401bb9c0a58 100644
--- a/apps/v4/registry/bases/__index__.tsx
+++ b/apps/v4/registry/bases/__index__.tsx
@@ -1326,6 +1326,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "registry/bases/base/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -2579,6 +2601,29 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ "questionnaire-example": {
+ name: "questionnaire-example",
+ title: "Questionnaire",
+ description: "",
+ type: "registry:example",
+ registryDependencies: [
+ "button",
+ "card",
+ "dialog",
+ "example",
+ "questionnaire",
+ "sonner",
+ ],
+ files: [
+ {
+ path: "registry/bases/base/examples/questionnaire-example.tsx",
+ type: "registry:example",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: undefined,
+ },
"marker-example": {
name: "marker-example",
title: "Marker",
@@ -4944,6 +4989,28 @@ export const Index: Record> = {
},
},
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "registry/bases/aria/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -6145,6 +6212,29 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ "questionnaire-example": {
+ name: "questionnaire-example",
+ title: "Questionnaire",
+ description: "",
+ type: "registry:example",
+ registryDependencies: [
+ "button",
+ "card",
+ "dialog",
+ "example",
+ "questionnaire",
+ "sonner",
+ ],
+ files: [
+ {
+ path: "registry/bases/aria/examples/questionnaire-example.tsx",
+ type: "registry:example",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: undefined,
+ },
"marker-example": {
name: "marker-example",
title: "Marker",
@@ -8557,6 +8647,28 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ questionnaire: {
+ name: "questionnaire",
+ title: "undefined",
+ description: "",
+ type: "registry:ui",
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "registry/bases/radix/ui/questionnaire.tsx",
+ type: "registry:ui",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
marker: {
name: "marker",
title: "undefined",
@@ -9794,6 +9906,29 @@ export const Index: Record> = {
categories: undefined,
meta: undefined,
},
+ "questionnaire-example": {
+ name: "questionnaire-example",
+ title: "Questionnaire",
+ description: "",
+ type: "registry:example",
+ registryDependencies: [
+ "button",
+ "card",
+ "dialog",
+ "example",
+ "questionnaire",
+ "sonner",
+ ],
+ files: [
+ {
+ path: "registry/bases/radix/examples/questionnaire-example.tsx",
+ type: "registry:example",
+ target: "",
+ },
+ ],
+ categories: undefined,
+ meta: undefined,
+ },
"marker-example": {
name: "marker-example",
title: "Marker",
diff --git a/apps/v4/registry/bases/aria/examples/_registry.ts b/apps/v4/registry/bases/aria/examples/_registry.ts
index a6abeed58d1..00782dfbb9c 100644
--- a/apps/v4/registry/bases/aria/examples/_registry.ts
+++ b/apps/v4/registry/bases/aria/examples/_registry.ts
@@ -916,6 +916,25 @@ export const examples: Registry["items"] = [
},
],
},
+ {
+ name: "questionnaire-example",
+ title: "Questionnaire",
+ type: "registry:example",
+ registryDependencies: [
+ "button",
+ "card",
+ "dialog",
+ "example",
+ "questionnaire",
+ "sonner",
+ ],
+ files: [
+ {
+ path: "examples/questionnaire-example.tsx",
+ type: "registry:example",
+ },
+ ],
+ },
{
name: "marker-example",
title: "Marker",
diff --git a/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx b/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx
new file mode 100644
index 00000000000..cfd24b0e07b
--- /dev/null
+++ b/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx
@@ -0,0 +1,414 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Example,
+ ExampleWrapper,
+} from "@/registry/bases/aria/components/example"
+import { Button } from "@/registry/bases/aria/ui/button"
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/registry/bases/aria/ui/card"
+import {
+ Dialog,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/registry/bases/aria/ui/dialog"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/registry/bases/aria/ui/questionnaire"
+
+const questionnaireItems = [
+ {
+ choices: [
+ { value: "delegation" },
+ { value: "questions" },
+ { value: "both" },
+ ],
+ name: "direction",
+ required: true,
+ },
+ {
+ choices: [
+ { value: "progress" },
+ { value: "decisions" },
+ { value: "risks" },
+ ],
+ name: "signals",
+ },
+ {
+ choices: [{ value: "week" }, { value: "cycle" }, { value: "later" }],
+ name: "timing",
+ required: true,
+ },
+] as const
+
+const taskItems = [
+ {
+ choices: [
+ { value: "inspect" },
+ { value: "implement" },
+ { value: "review" },
+ ],
+ name: "task",
+ required: true,
+ },
+] as const
+
+export default function QuestionnaireExample() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireNoDescription() {
+ return (
+
+
+
+
+
+ What should the agent do next?
+
+
+
+ Inspect the codebase
+
+
+ Implement the change
+
+
+ Review the result
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireStandalone() {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireCard() {
+ return (
+
+
+
+
+
+ )
+}
+
+function QuestionnaireDialog() {
+ return (
+
+
+ Open questionnaire
+
+
+
+
+ Plan an agent interface
+
+
+ Answer three questions to shape the next prototype.
+
+ (
+
+ Question {state.current} of {state.total}
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireCardQuestions() {
+ const directionTitleId = React.useId()
+ const signalsTitleId = React.useId()
+ const timingTitleId = React.useId()
+
+ return (
+ <>
+
+
+
+ }>
+ What should we prototype next?
+
+ }>
+ Choose one direction or write another answer.
+
+
+
+
+
+
+
+
+ Sub-agent delegation
+
+ Show when work is delegated and what comes back.
+
+
+
+ Question prompts
+
+ Show choices while the agent waits for input.
+
+
+
+ Both together
+
+ Explore one unified interaction pattern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ What should every progress update include?
+
+ }>
+ Select all that apply, or skip this question.
+
+
+
+
+
+
+
+
+ Progress
+
+
+ Decisions
+
+ Risks
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ When should this be revisited?
+
+ }>
+ Choose when this should be revisited.
+
+
+
+
+
+
+
+ This week
+
+ Next cycle
+
+
+ Revisit later
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function QuestionnaireQuestions() {
+ return (
+ <>
+
+ What should we prototype next?
+
+ Choose one direction or write another answer.
+
+
+
+ Sub-agent delegation
+
+ Show when work is delegated and what comes back.
+
+
+
+ Question prompts
+
+ Show choices while the agent waits for input.
+
+
+
+ Both together
+
+ Explore one unified interaction pattern.
+
+
+
+
+
+
+
+
+ What should every progress update include?
+
+
+ Select all that apply, or skip this question.
+
+
+ Progress
+ Decisions
+ Risks
+
+
+
+
+ When should this be revisited?
+
+ Choose when this should be revisited.
+
+
+ This week
+ Next cycle
+ Revisit later
+
+
+
+ >
+ )
+}
+
+function QuestionnaireNavigation() {
+ return (
+
+
+
+ Next
+ Save answers
+
+ )
+}
+
+function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const values = {
+ direction: formData.get("direction"),
+ signals: formData.getAll("signals"),
+ timing: formData.get("timing"),
+ }
+
+ toast("Questionnaire submitted", {
+ description: `Direction: ${values.direction ?? "None"} · Progress signals: ${values.signals.join(", ") || "None"} · Timing: ${values.timing ?? "None"}`,
+ })
+}
diff --git a/apps/v4/registry/bases/aria/ui/_registry.ts b/apps/v4/registry/bases/aria/ui/_registry.ts
index 34ccb76e1ef..d05be8edf1b 100644
--- a/apps/v4/registry/bases/aria/ui/_registry.ts
+++ b/apps/v4/registry/bases/aria/ui/_registry.ts
@@ -1007,6 +1007,25 @@ export const ui: Registry["items"] = [
},
},
},
+ {
+ name: "questionnaire",
+ type: "registry:ui",
+ dependencies: ["@shadcn/react"],
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "ui/questionnaire.tsx",
+ type: "registry:ui",
+ },
+ ],
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/aria/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/aria/examples/questionnaire-example.tsx",
+ },
+ },
+ },
{
name: "marker",
type: "registry:ui",
diff --git a/apps/v4/registry/bases/aria/ui/questionnaire.tsx b/apps/v4/registry/bases/aria/ui/questionnaire.tsx
new file mode 100644
index 00000000000..a493e3bb9be
--- /dev/null
+++ b/apps/v4/registry/bases/aria/ui/questionnaire.tsx
@@ -0,0 +1,321 @@
+"use client"
+
+import * as React from "react"
+import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire"
+
+import { cn } from "@/registry/bases/aria/lib/utils"
+import { buttonVariants, type Button } from "@/registry/bases/aria/ui/button"
+import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
+
+function Questionnaire({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireProgress({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireChoices({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireChoice({
+ children,
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function QuestionnaireInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function QuestionnaireError({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireActions({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function QuestionnairePrevious({
+ children,
+ className,
+ size = "default",
+ variant = "outline",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Previous"}
+
+ )
+}
+
+function QuestionnaireSkip({
+ children,
+ className,
+ size = "default",
+ variant = "outline",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Skip"}
+
+ )
+}
+
+function QuestionnaireNext({
+ children,
+ className,
+ size = "default",
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Next"}
+
+ )
+}
+
+function QuestionnaireSubmit({
+ children,
+ className,
+ size = "default",
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Submit"}
+
+ )
+}
+
+export {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+}
diff --git a/apps/v4/registry/bases/base/examples/_registry.ts b/apps/v4/registry/bases/base/examples/_registry.ts
index e66503fb02e..866277be003 100644
--- a/apps/v4/registry/bases/base/examples/_registry.ts
+++ b/apps/v4/registry/bases/base/examples/_registry.ts
@@ -964,6 +964,25 @@ export const examples: Registry["items"] = [
},
],
},
+ {
+ name: "questionnaire-example",
+ title: "Questionnaire",
+ type: "registry:example",
+ registryDependencies: [
+ "button",
+ "card",
+ "dialog",
+ "example",
+ "questionnaire",
+ "sonner",
+ ],
+ files: [
+ {
+ path: "examples/questionnaire-example.tsx",
+ type: "registry:example",
+ },
+ ],
+ },
{
name: "marker-example",
title: "Marker",
diff --git a/apps/v4/registry/bases/base/examples/questionnaire-example.tsx b/apps/v4/registry/bases/base/examples/questionnaire-example.tsx
new file mode 100644
index 00000000000..bc7b60ec989
--- /dev/null
+++ b/apps/v4/registry/bases/base/examples/questionnaire-example.tsx
@@ -0,0 +1,417 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Example,
+ ExampleWrapper,
+} from "@/registry/bases/base/components/example"
+import { Button } from "@/registry/bases/base/ui/button"
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/registry/bases/base/ui/card"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/registry/bases/base/ui/dialog"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/registry/bases/base/ui/questionnaire"
+
+const questionnaireItems = [
+ {
+ choices: [
+ { value: "delegation" },
+ { value: "questions" },
+ { value: "both" },
+ ],
+ name: "direction",
+ required: true,
+ },
+ {
+ choices: [
+ { value: "progress" },
+ { value: "decisions" },
+ { value: "risks" },
+ ],
+ name: "signals",
+ },
+ {
+ choices: [{ value: "week" }, { value: "cycle" }, { value: "later" }],
+ name: "timing",
+ required: true,
+ },
+] as const
+
+const taskItems = [
+ {
+ choices: [
+ { value: "inspect" },
+ { value: "implement" },
+ { value: "review" },
+ ],
+ name: "task",
+ required: true,
+ },
+] as const
+
+export default function QuestionnaireExample() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireNoDescription() {
+ return (
+
+
+
+
+
+ What should the agent do next?
+
+
+
+ Inspect the codebase
+
+
+ Implement the change
+
+
+ Review the result
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireStandalone() {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireCard() {
+ return (
+
+
+
+
+
+ )
+}
+
+function QuestionnaireDialog() {
+ return (
+
+
+ }>
+ Open questionnaire
+
+
+
+
+
+ Plan an agent interface
+
+
+ Answer three questions to shape the next prototype.
+
+ (
+
+ Question {state.current} of {state.total}
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireCardQuestions() {
+ const directionTitleId = React.useId()
+ const signalsTitleId = React.useId()
+ const timingTitleId = React.useId()
+
+ return (
+ <>
+
+
+
+ }>
+ What should we prototype next?
+
+ }>
+ Choose one direction or write another answer.
+
+
+
+
+
+
+
+
+ Sub-agent delegation
+
+ Show when work is delegated and what comes back.
+
+
+
+ Question prompts
+
+ Show choices while the agent waits for input.
+
+
+
+ Both together
+
+ Explore one unified interaction pattern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ What should every progress update include?
+
+ }>
+ Select all that apply, or skip this question.
+
+
+
+
+
+
+
+
+ Progress
+
+
+ Decisions
+
+ Risks
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ When should this be revisited?
+
+ }>
+ Choose when this should be revisited.
+
+
+
+
+
+
+
+ This week
+
+ Next cycle
+
+
+ Revisit later
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function QuestionnaireQuestions() {
+ return (
+ <>
+
+ What should we prototype next?
+
+ Choose one direction or write another answer.
+
+
+
+ Sub-agent delegation
+
+ Show when work is delegated and what comes back.
+
+
+
+ Question prompts
+
+ Show choices while the agent waits for input.
+
+
+
+ Both together
+
+ Explore one unified interaction pattern.
+
+
+
+
+
+
+
+
+ What should every progress update include?
+
+
+ Select all that apply, or skip this question.
+
+
+ Progress
+ Decisions
+ Risks
+
+
+
+
+ When should this be revisited?
+
+ Choose when this should be revisited.
+
+
+ This week
+ Next cycle
+ Revisit later
+
+
+
+ >
+ )
+}
+
+function QuestionnaireNavigation() {
+ return (
+
+
+
+ Next
+ Save answers
+
+ )
+}
+
+function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const values = {
+ direction: formData.get("direction"),
+ signals: formData.getAll("signals"),
+ timing: formData.get("timing"),
+ }
+
+ toast("Questionnaire submitted", {
+ description: `Direction: ${values.direction ?? "None"} · Progress signals: ${values.signals.join(", ") || "None"} · Timing: ${values.timing ?? "None"}`,
+ })
+}
diff --git a/apps/v4/registry/bases/base/ui/_registry.ts b/apps/v4/registry/bases/base/ui/_registry.ts
index feae27a71ef..80a3a705187 100644
--- a/apps/v4/registry/bases/base/ui/_registry.ts
+++ b/apps/v4/registry/bases/base/ui/_registry.ts
@@ -1075,6 +1075,25 @@ export default function RootLayout({ children }: { children: React.ReactNode })
},
],
},
+ {
+ name: "questionnaire",
+ type: "registry:ui",
+ dependencies: ["@shadcn/react"],
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "ui/questionnaire.tsx",
+ type: "registry:ui",
+ },
+ ],
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/base/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/base/examples/questionnaire-example.tsx",
+ },
+ },
+ },
{
name: "marker",
type: "registry:ui",
diff --git a/apps/v4/registry/bases/base/ui/questionnaire.tsx b/apps/v4/registry/bases/base/ui/questionnaire.tsx
new file mode 100644
index 00000000000..3589c13d1a1
--- /dev/null
+++ b/apps/v4/registry/bases/base/ui/questionnaire.tsx
@@ -0,0 +1,321 @@
+"use client"
+
+import * as React from "react"
+import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire"
+
+import { cn } from "@/registry/bases/base/lib/utils"
+import { buttonVariants, type Button } from "@/registry/bases/base/ui/button"
+import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
+
+function Questionnaire({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireProgress({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireChoices({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireChoice({
+ children,
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function QuestionnaireInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function QuestionnaireError({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireActions({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function QuestionnairePrevious({
+ children,
+ className,
+ size = "default",
+ variant = "outline",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Previous"}
+
+ )
+}
+
+function QuestionnaireSkip({
+ children,
+ className,
+ size = "default",
+ variant = "outline",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Skip"}
+
+ )
+}
+
+function QuestionnaireNext({
+ children,
+ className,
+ size = "default",
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Next"}
+
+ )
+}
+
+function QuestionnaireSubmit({
+ children,
+ className,
+ size = "default",
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Submit"}
+
+ )
+}
+
+export {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+}
diff --git a/apps/v4/registry/bases/radix/examples/_registry.ts b/apps/v4/registry/bases/radix/examples/_registry.ts
index bc970fa6cae..56361dd942b 100644
--- a/apps/v4/registry/bases/radix/examples/_registry.ts
+++ b/apps/v4/registry/bases/radix/examples/_registry.ts
@@ -952,6 +952,25 @@ export const examples: Registry["items"] = [
},
],
},
+ {
+ name: "questionnaire-example",
+ title: "Questionnaire",
+ type: "registry:example",
+ registryDependencies: [
+ "button",
+ "card",
+ "dialog",
+ "example",
+ "questionnaire",
+ "sonner",
+ ],
+ files: [
+ {
+ path: "examples/questionnaire-example.tsx",
+ type: "registry:example",
+ },
+ ],
+ },
{
name: "marker-example",
title: "Marker",
diff --git a/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx b/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx
new file mode 100644
index 00000000000..b76fcffc897
--- /dev/null
+++ b/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx
@@ -0,0 +1,417 @@
+"use client"
+
+import * as React from "react"
+import { toast } from "sonner"
+
+import {
+ Example,
+ ExampleWrapper,
+} from "@/registry/bases/radix/components/example"
+import { Button } from "@/registry/bases/radix/ui/button"
+import {
+ Card,
+ CardAction,
+ CardContent,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardTitle,
+} from "@/registry/bases/radix/ui/card"
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/registry/bases/radix/ui/dialog"
+import {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+} from "@/registry/bases/radix/ui/questionnaire"
+
+const questionnaireItems = [
+ {
+ choices: [
+ { value: "delegation" },
+ { value: "questions" },
+ { value: "both" },
+ ],
+ name: "direction",
+ required: true,
+ },
+ {
+ choices: [
+ { value: "progress" },
+ { value: "decisions" },
+ { value: "risks" },
+ ],
+ name: "signals",
+ },
+ {
+ choices: [{ value: "week" }, { value: "cycle" }, { value: "later" }],
+ name: "timing",
+ required: true,
+ },
+] as const
+
+const taskItems = [
+ {
+ choices: [
+ { value: "inspect" },
+ { value: "implement" },
+ { value: "review" },
+ ],
+ name: "task",
+ required: true,
+ },
+] as const
+
+export default function QuestionnaireExample() {
+ return (
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireNoDescription() {
+ return (
+
+
+
+
+
+ What should the agent do next?
+
+
+
+ Inspect the codebase
+
+
+ Implement the change
+
+
+ Review the result
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireStandalone() {
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireCard() {
+ return (
+
+
+
+
+
+ )
+}
+
+function QuestionnaireDialog() {
+ return (
+
+
+
+ Open questionnaire
+
+
+
+
+
+ Plan an agent interface
+
+
+ Answer three questions to shape the next prototype.
+
+ (
+
+ Question {state.current} of {state.total}
+
+ )}
+ />
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function QuestionnaireCardQuestions() {
+ const directionTitleId = React.useId()
+ const signalsTitleId = React.useId()
+ const timingTitleId = React.useId()
+
+ return (
+ <>
+
+
+
+ }>
+ What should we prototype next?
+
+ }>
+ Choose one direction or write another answer.
+
+
+
+
+
+
+
+
+ Sub-agent delegation
+
+ Show when work is delegated and what comes back.
+
+
+
+ Question prompts
+
+ Show choices while the agent waits for input.
+
+
+
+ Both together
+
+ Explore one unified interaction pattern.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ What should every progress update include?
+
+ }>
+ Select all that apply, or skip this question.
+
+
+
+
+
+
+
+
+ Progress
+
+
+ Decisions
+
+ Risks
+
+
+
+
+
+
+
+
+
+
+
+ }>
+ When should this be revisited?
+
+ }>
+ Choose when this should be revisited.
+
+
+
+
+
+
+
+ This week
+
+ Next cycle
+
+
+ Revisit later
+
+
+
+
+
+
+
+
+
+ >
+ )
+}
+
+function QuestionnaireQuestions() {
+ return (
+ <>
+
+ What should we prototype next?
+
+ Choose one direction or write another answer.
+
+
+
+ Sub-agent delegation
+
+ Show when work is delegated and what comes back.
+
+
+
+ Question prompts
+
+ Show choices while the agent waits for input.
+
+
+
+ Both together
+
+ Explore one unified interaction pattern.
+
+
+
+
+
+
+
+
+ What should every progress update include?
+
+
+ Select all that apply, or skip this question.
+
+
+ Progress
+ Decisions
+ Risks
+
+
+
+
+ When should this be revisited?
+
+ Choose when this should be revisited.
+
+
+ This week
+ Next cycle
+ Revisit later
+
+
+
+ >
+ )
+}
+
+function QuestionnaireNavigation() {
+ return (
+
+
+
+ Next
+ Save answers
+
+ )
+}
+
+function handleSubmit(event: React.FormEvent) {
+ event.preventDefault()
+
+ const formData = new FormData(event.currentTarget)
+ const values = {
+ direction: formData.get("direction"),
+ signals: formData.getAll("signals"),
+ timing: formData.get("timing"),
+ }
+
+ toast("Questionnaire submitted", {
+ description: `Direction: ${values.direction ?? "None"} · Progress signals: ${values.signals.join(", ") || "None"} · Timing: ${values.timing ?? "None"}`,
+ })
+}
diff --git a/apps/v4/registry/bases/radix/ui/_registry.ts b/apps/v4/registry/bases/radix/ui/_registry.ts
index 0118a48e74f..834ad844cb1 100644
--- a/apps/v4/registry/bases/radix/ui/_registry.ts
+++ b/apps/v4/registry/bases/radix/ui/_registry.ts
@@ -1053,6 +1053,25 @@ export default function RootLayout({ children }: { children: React.ReactNode })
},
],
},
+ {
+ name: "questionnaire",
+ type: "registry:ui",
+ dependencies: ["@shadcn/react"],
+ registryDependencies: ["button"],
+ files: [
+ {
+ path: "ui/questionnaire.tsx",
+ type: "registry:ui",
+ },
+ ],
+ meta: {
+ links: {
+ docs: "https://ui.shadcn.com/docs/components/radix/questionnaire",
+ examples:
+ "https://ui.shadcn.com/code/apps/v4/registry/bases/radix/examples/questionnaire-example.tsx",
+ },
+ },
+ },
{
name: "marker",
type: "registry:ui",
diff --git a/apps/v4/registry/bases/radix/ui/questionnaire.tsx b/apps/v4/registry/bases/radix/ui/questionnaire.tsx
new file mode 100644
index 00000000000..39b85c0d702
--- /dev/null
+++ b/apps/v4/registry/bases/radix/ui/questionnaire.tsx
@@ -0,0 +1,321 @@
+"use client"
+
+import * as React from "react"
+import { Questionnaire as QuestionnairePrimitive } from "@shadcn/react/questionnaire"
+
+import { cn } from "@/registry/bases/radix/lib/utils"
+import { buttonVariants, type Button } from "@/registry/bases/radix/ui/button"
+import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
+
+function Questionnaire({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireProgress({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireChoices({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireChoice({
+ children,
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+
+ {children}
+
+
+
+ )
+}
+
+function QuestionnaireInput({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function QuestionnaireError({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function QuestionnaireActions({
+ className,
+ ...props
+}: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function QuestionnairePrevious({
+ children,
+ className,
+ size = "default",
+ variant = "outline",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Previous"}
+
+ )
+}
+
+function QuestionnaireSkip({
+ children,
+ className,
+ size = "default",
+ variant = "outline",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Skip"}
+
+ )
+}
+
+function QuestionnaireNext({
+ children,
+ className,
+ size = "default",
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Next"}
+
+ )
+}
+
+function QuestionnaireSubmit({
+ children,
+ className,
+ size = "default",
+ variant = "default",
+ ...props
+}: React.ComponentProps &
+ Pick, "size" | "variant">) {
+ return (
+
+ {children ?? "Submit"}
+
+ )
+}
+
+export {
+ Questionnaire,
+ QuestionnaireActions,
+ QuestionnaireChoice,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+}
diff --git a/apps/v4/registry/styles/style-luma.css b/apps/v4/registry/styles/style-luma.css
index c0c7271ae2d..d284e0eb991 100644
--- a/apps/v4/registry/styles/style-luma.css
+++ b/apps/v4/registry/styles/style-luma.css
@@ -1590,6 +1590,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-6;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-5;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-base font-semibold [&:not(:has(~[data-slot=questionnaire-description]))]:mb-5;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-3;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input bg-input/20 hover:bg-input/40 data-checked:border-primary/40 data-checked:bg-primary/10 data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/50 gap-3 rounded-3xl border px-4 py-3 text-sm has-[>input:focus-visible]:ring-3;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply bg-input/90 group-data-checked/questionnaire-choice:bg-primary dark:group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-[5px] border-transparent;
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2 dark:size-2.5;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-1;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-primary/10 bg-background/80 text-muted-foreground size-5 items-center justify-center rounded-full border font-mono text-[0.625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-3xl border px-3 py-1 text-base focus-visible:ring-3 aria-invalid:ring-3 md:text-sm;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-2 sm:min-h-9;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-8;
diff --git a/apps/v4/registry/styles/style-lyra.css b/apps/v4/registry/styles/style-lyra.css
index ecd03244519..5a0f06c647e 100644
--- a/apps/v4/registry/styles/style-lyra.css
+++ b/apps/v4/registry/styles/style-lyra.css
@@ -1569,6 +1569,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-4;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-4;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-sm font-medium [&:not(:has(~[data-slot=questionnaire-description]))]:mb-4;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-xs/relaxed;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-2;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input hover:bg-muted/50 data-checked:bg-muted data-checked:border-foreground/30 data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/50 gap-2.5 rounded-none border bg-transparent px-3 py-2.5 text-xs has-[>input:focus-visible]:ring-1;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply border-input group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-none;
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-0.5;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-input bg-background text-muted-foreground size-4 items-center justify-center rounded-none border font-mono text-[0.625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-none border bg-transparent px-2.5 py-1 text-xs focus-visible:ring-1 aria-invalid:ring-1 md:text-xs;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-1.5 sm:min-h-8;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-6;
diff --git a/apps/v4/registry/styles/style-maia.css b/apps/v4/registry/styles/style-maia.css
index 62f5eb67f8e..f64b6380263 100644
--- a/apps/v4/registry/styles/style-maia.css
+++ b/apps/v4/registry/styles/style-maia.css
@@ -1591,6 +1591,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-6;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-5;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-base font-semibold [&:not(:has(~[data-slot=questionnaire-description]))]:mb-5;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-3;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input bg-input/20 hover:bg-input/40 data-checked:border-primary/40 data-checked:bg-primary/10 data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/50 gap-3.5 rounded-4xl border px-4 py-3.5 text-sm has-[>input:focus-visible]:ring-[3px];
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply border-input dark:bg-input/30 group-data-checked/questionnaire-choice:bg-primary dark:group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-[6px];
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-1;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-input bg-background/80 text-muted-foreground size-5 items-center justify-center rounded-full border font-mono text-[0.625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-4xl border px-3 py-1 text-base focus-visible:ring-[3px] aria-invalid:ring-[3px] md:text-sm;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-2 sm:min-h-9;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-8;
diff --git a/apps/v4/registry/styles/style-mira.css b/apps/v4/registry/styles/style-mira.css
index a2750006790..bc8f08bdfd5 100644
--- a/apps/v4/registry/styles/style-mira.css
+++ b/apps/v4/registry/styles/style-mira.css
@@ -1596,6 +1596,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-4;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-[0.625rem];
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-3;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-sm font-semibold [&:not(:has(~[data-slot=questionnaire-description]))]:mb-3;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-xs/relaxed;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-1.5;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input bg-input/20 hover:bg-input/40 data-checked:border-primary/40 data-checked:bg-primary/10 data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/30 gap-2.5 rounded-xl border px-3 py-2.5 text-xs/relaxed has-[>input:focus-visible]:ring-2;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply border-input dark:bg-input/30 group-data-checked/questionnaire-choice:bg-primary dark:group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-[4px];
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-0.5;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-input bg-background/80 text-muted-foreground size-4 items-center justify-center rounded-sm border font-mono text-[0.5625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply bg-input/20 dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-7 rounded-md border px-2 py-0.5 text-sm focus-visible:ring-2 aria-invalid:ring-2 md:text-xs/relaxed;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-xs/relaxed;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-1.5 sm:min-h-7;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-6;
diff --git a/apps/v4/registry/styles/style-nova.css b/apps/v4/registry/styles/style-nova.css
index 084f66cf70c..dc9d3d807c0 100644
--- a/apps/v4/registry/styles/style-nova.css
+++ b/apps/v4/registry/styles/style-nova.css
@@ -1594,6 +1594,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-4;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-4;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-base leading-snug font-medium [&:not(:has(~[data-slot=questionnaire-description]))]:mb-4;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-muted-foreground text-sm;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-2;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input dark:bg-input/20 hover:bg-muted/50 data-checked:border-primary/40 data-checked:bg-muted dark:data-checked:bg-muted data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/50 gap-2.5 rounded-lg border bg-transparent px-3 py-2.5 text-sm has-[>input:focus-visible]:ring-3;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply border-input dark:bg-input/30 group-data-checked/questionnaire-choice:bg-primary dark:group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-[4px];
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-0.5;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-input bg-background text-muted-foreground size-5 items-center justify-center rounded-md border font-mono text-[0.625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-lg border bg-transparent px-2.5 py-1 text-base focus-visible:ring-3 aria-invalid:ring-3 md:text-sm;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-2 sm:min-h-8;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-6;
diff --git a/apps/v4/registry/styles/style-rhea.css b/apps/v4/registry/styles/style-rhea.css
index 053bb5d7cc1..10bc6922315 100644
--- a/apps/v4/registry/styles/style-rhea.css
+++ b/apps/v4/registry/styles/style-rhea.css
@@ -1590,6 +1590,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-6;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-5;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-base font-semibold [&:not(:has(~[data-slot=questionnaire-description]))]:mb-5;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-3;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input bg-input/20 hover:bg-input/40 data-checked:border-primary/40 data-checked:bg-primary/10 data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/50 gap-3 rounded-2xl border px-4 py-3 text-sm has-[>input:focus-visible]:ring-3;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply bg-input/90 group-data-checked/questionnaire-choice:bg-primary dark:group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-[5px] border-transparent;
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2 dark:size-2.5;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-1;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-primary/10 bg-background/80 text-muted-foreground size-5 items-center justify-center rounded-full border font-mono text-[0.625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply bg-input/50 border-transparent focus-visible:border-ring focus-visible:ring-ring/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-8 rounded-2xl border px-2.5 py-1 text-base duration-200 focus-visible:ring-3 aria-invalid:ring-3 md:text-sm;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-2 sm:min-h-8;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-8;
diff --git a/apps/v4/registry/styles/style-sera.css b/apps/v4/registry/styles/style-sera.css
index 3d983e34f59..908bcd61458 100644
--- a/apps/v4/registry/styles/style-sera.css
+++ b/apps/v4/registry/styles/style-sera.css
@@ -1581,6 +1581,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-6;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs uppercase tracking-wide;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-5;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-xs font-semibold uppercase tracking-wide [&:not(:has(~[data-slot=questionnaire-description]))]:mb-5;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-sm normal-case tracking-normal;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-3;
+ }
+
+ .cn-questionnaire-choice {
+ @apply hover:bg-muted/50 data-checked:bg-primary/5 data-checked:border-primary/30 dark:data-checked:border-primary/20 dark:data-checked:bg-primary/10 data-invalid:border-destructive has-[>input:focus-visible]:ring-ring/30 gap-3 rounded-none border border-input bg-transparent px-4 py-4 text-sm has-[>input:focus-visible]:ring-2;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply border-input group-data-checked/questionnaire-choice:border-foreground group-data-[type=checkbox]/questionnaire-choice:group-data-checked/questionnaire-choice:bg-primary group-data-[type=checkbox]/questionnaire-choice:group-data-checked/questionnaire-choice:text-primary-foreground group-data-[type=checkbox]/questionnaire-choice:group-data-checked/questionnaire-choice:border-primary size-4.5;
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-foreground size-2;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-1;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-input bg-background text-muted-foreground size-5 items-center justify-center rounded-none border font-mono text-[0.625rem] font-medium leading-none;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply border-transparent border-b-input bg-transparent focus-visible:border-b-ring aria-invalid:border-b-destructive dark:aria-invalid:border-b-destructive/50 h-10 border px-0 py-1 text-base md:text-sm;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-2 sm:min-h-10;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-8;
diff --git a/apps/v4/registry/styles/style-vega.css b/apps/v4/registry/styles/style-vega.css
index 656709dabd6..a7f219baf64 100644
--- a/apps/v4/registry/styles/style-vega.css
+++ b/apps/v4/registry/styles/style-vega.css
@@ -1590,6 +1590,71 @@
@apply group-data-[variant=separator]/marker:flex-none group-data-[variant=separator]/marker:text-center *:[a]:hover:text-foreground *:[a]:underline *:[a]:underline-offset-3;
}
+ /* MARK: Questionnaire */
+ .cn-questionnaire {
+ @apply gap-6;
+ }
+
+ .cn-questionnaire-progress {
+ @apply text-xs;
+ }
+
+ .cn-questionnaire-item {
+ @apply flex flex-col gap-5;
+ }
+
+ .cn-questionnaire-title {
+ @apply text-base font-semibold [&:not(:has(~[data-slot=questionnaire-description]))]:mb-5;
+ }
+
+ .cn-questionnaire-description {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-choices {
+ @apply gap-3;
+ }
+
+ .cn-questionnaire-choice {
+ @apply border-input dark:bg-input/20 hover:bg-muted/50 data-checked:border-primary/40 data-checked:bg-muted dark:data-checked:bg-muted data-invalid:border-destructive has-[>input:focus-visible]:border-ring has-[>input:focus-visible]:ring-ring/50 gap-3.5 rounded-md border bg-transparent px-4 py-3.5 text-sm shadow-xs has-[>input:focus-visible]:ring-3;
+ }
+
+ .cn-questionnaire-choice-indicator {
+ @apply border-input dark:bg-input/30 group-data-checked/questionnaire-choice:bg-primary dark:group-data-checked/questionnaire-choice:bg-primary group-data-checked/questionnaire-choice:text-primary-foreground group-data-checked/questionnaire-choice:border-primary size-4 rounded-[4px];
+ }
+
+ .cn-questionnaire-choice-indicator-dot {
+ @apply bg-primary-foreground size-2;
+ }
+
+ .cn-questionnaire-choice-indicator-check {
+ @apply size-3.5;
+ }
+
+ .cn-questionnaire-choice-content {
+ @apply gap-1;
+ }
+
+ .cn-questionnaire-shortcut {
+ @apply border-input bg-background text-muted-foreground size-5 items-center justify-center rounded-md border font-mono text-[0.625rem] font-medium leading-none shadow-xs;
+ }
+
+ .cn-questionnaire-input-wrapper {
+ @apply w-full;
+ }
+
+ .cn-questionnaire-input {
+ @apply dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 h-9 rounded-md border bg-transparent px-2.5 py-1 text-base shadow-xs focus-visible:ring-3 aria-invalid:ring-3 md:text-sm;
+ }
+
+ .cn-questionnaire-error {
+ @apply text-sm;
+ }
+
+ .cn-questionnaire-actions {
+ @apply gap-2 sm:min-h-9;
+ }
+
/* MARK: Message Scroller */
.cn-message-scroller-content {
@apply gap-8;
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 697c5964a32..ce7b89aeeaf 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,11 @@
# @shadcn/react
+## 0.3.0
+
+### Minor Changes
+
+- [#11414](https://github.com/shadcn-ui/ui/pull/11414) [`3e54530e31020e2277df03490a30a08e3bc1792b`](https://github.com/shadcn-ui/ui/commit/3e54530e31020e2277df03490a30a08e3bc1792b) Thanks [@shadcn](https://github.com/shadcn)! - Add the Questionnaire primitive for multi-step questions, freeform answers, validation, and keyboard navigation.
+
## 0.2.1
### Patch Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index 9a9c1fd716c..7cf369a0a31 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
{
"name": "@shadcn/react",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Unstyled components for React.",
"publishConfig": {
"access": "public"
@@ -37,6 +37,10 @@
"./message-scroller": {
"types": "./dist/message-scroller/index.d.ts",
"default": "./dist/message-scroller/index.js"
+ },
+ "./questionnaire": {
+ "types": "./dist/questionnaire/index.d.ts",
+ "default": "./dist/questionnaire/index.js"
}
},
"scripts": {
@@ -69,6 +73,7 @@
"rimraf": "^6.0.1",
"tsup": "^8.5.0",
"typescript": "^5.9.2",
- "vite-tsconfig-paths": "^4.3.2"
+ "vite-tsconfig-paths": "^4.3.2",
+ "vitest": "^3.2.6"
}
}
diff --git a/packages/react/src/questionnaire/README.md b/packages/react/src/questionnaire/README.md
new file mode 100644
index 00000000000..50e49b4bb04
--- /dev/null
+++ b/packages/react/src/questionnaire/README.md
@@ -0,0 +1,98 @@
+# Questionnaire
+
+An unstyled, multi-step questionnaire for React. `Root` renders a native form,
+and answers are read with `FormData`.
+
+Questionnaire supports server rendering. Pass `Root.items` to include complete
+collection-derived state in the initial HTML.
+
+## Usage
+
+```tsx
+const items = [
+ {
+ name: "prototype",
+ required: true,
+ prompt: "What should we prototype next?",
+ description: "Choose a direction or write your own.",
+ choices: [
+ {
+ value: "delegation",
+ label: "Delegation",
+ description: "Show how work moves to a specialist.",
+ },
+ { value: "questions", label: "Question prompts" },
+ ],
+ input: { label: "Another answer", placeholder: "Type another answer…" },
+ },
+ {
+ name: "detail",
+ required: false,
+ prompt: "How much detail?",
+ description: "Skip this if you are not sure yet.",
+ choices: [
+ { value: "focused", label: "Focused" },
+ { value: "complete", label: "Complete flow" },
+ ],
+ },
+] as const
+```
+
+```tsx
+import { Questionnaire } from "@shadcn/react/questionnaire"
+
+export function ProjectQuestionnaire() {
+ return (
+ {
+ event.preventDefault()
+ const answers = new FormData(event.currentTarget)
+ }}
+ >
+
+ {items.map((question) => (
+
+ {question.prompt}
+
+ {question.description}
+
+
+ {question.choices.map((choice) => (
+
+
+
+ {choice.label}
+ {"description" in choice ? (
+ {choice.description}
+ ) : null}
+
+
+
+ ))}
+ {"input" in question ? (
+
+ ) : null}
+
+
+
+ ))}
+
+
+
+
+
+ )
+}
+```
+
+## Documentation
+
+Read the full docs at [ui.shadcn.com/docs/react/questionnaire](https://ui.shadcn.com/docs/react/questionnaire).
diff --git a/packages/react/src/questionnaire/collection.ts b/packages/react/src/questionnaire/collection.ts
new file mode 100644
index 00000000000..b4c14b13ed9
--- /dev/null
+++ b/packages/react/src/questionnaire/collection.ts
@@ -0,0 +1,234 @@
+import type {
+ ItemRegistration,
+ QuestionnaireItemDefinition,
+ QuestionnaireShortcutMode,
+} from "./types"
+import { getShortcutKeys } from "./utils"
+
+type QuestionnaireCollection = {
+ enabledItems: readonly QuestionnaireItemDefinition[]
+ itemByName: ReadonlyMap
+ items: readonly QuestionnaireItemDefinition[]
+}
+
+function createQuestionnaireCollection(
+ items: readonly QuestionnaireItemDefinition[] | undefined
+): QuestionnaireCollection | null {
+ if (items === undefined) {
+ return null
+ }
+
+ return {
+ enabledItems: items.filter((item) => !item.disabled),
+ itemByName: new Map(items.map((item) => [item.name, item])),
+ items,
+ }
+}
+
+function getInitialItemName(
+ collection: QuestionnaireCollection | null,
+ defaultItem: string | undefined
+) {
+ if (!collection) {
+ return defaultItem ?? null
+ }
+
+ const defaultDefinition = defaultItem
+ ? collection.itemByName.get(defaultItem)
+ : undefined
+
+ if (defaultDefinition && !defaultDefinition.disabled) {
+ return defaultDefinition.name
+ }
+
+ return collection.enabledItems[0]?.name ?? null
+}
+
+function getShortcutByChoiceValue(
+ item: QuestionnaireItemDefinition | undefined,
+ shortcuts: QuestionnaireShortcutMode | null
+) {
+ const shortcutByChoiceValue = new Map()
+
+ if (!item || !shortcuts) {
+ return shortcutByChoiceValue
+ }
+
+ const keys = getShortcutKeys(shortcuts)
+ let shortcutIndex = 0
+
+ for (const choice of item.choices ?? []) {
+ if (choice.disabled) {
+ continue
+ }
+
+ const shortcut = keys[shortcutIndex]
+
+ if (!shortcut) {
+ break
+ }
+
+ shortcutByChoiceValue.set(choice.value, shortcut)
+ shortcutIndex += 1
+ }
+
+ return shortcutByChoiceValue
+}
+
+function getCollectionDefinitionWarnings(
+ collection: QuestionnaireCollection,
+ defaultItem: string | undefined
+) {
+ const warnings: string[] = []
+ const itemNames = new Set()
+
+ for (const item of collection.items) {
+ if (itemNames.has(item.name)) {
+ warnings.push(`Item name "${item.name}" is defined more than once.`)
+ }
+
+ itemNames.add(item.name)
+
+ const choiceValues = new Set()
+
+ for (const choice of item.choices ?? []) {
+ if (choiceValues.has(choice.value)) {
+ warnings.push(
+ `Choice value "${choice.value}" is defined more than once in item "${item.name}".`
+ )
+ }
+
+ choiceValues.add(choice.value)
+ }
+ }
+
+ if (defaultItem) {
+ const defaultDefinition = collection.itemByName.get(defaultItem)
+
+ if (!defaultDefinition || defaultDefinition.disabled) {
+ warnings.push(
+ `defaultItem "${defaultItem}" does not identify an enabled item. The first enabled item will be used instead.`
+ )
+ }
+ }
+
+ return warnings
+}
+
+function getCollectionRegistrationWarnings(
+ collection: QuestionnaireCollection,
+ registrations: readonly ItemRegistration[],
+ shortcuts: QuestionnaireShortcutMode | null
+) {
+ const warnings: string[] = []
+ const registrationByName = new Map(
+ registrations.map((registration) => [registration.name, registration])
+ )
+
+ for (const definition of collection.items) {
+ const registration = registrationByName.get(definition.name)
+
+ if (!registration) {
+ if (!definition.disabled) {
+ warnings.push(
+ `Item "${definition.name}" is defined but has no rendered Questionnaire.Item.`
+ )
+ }
+
+ continue
+ }
+
+ if (registration.disabled !== Boolean(definition.disabled)) {
+ warnings.push(
+ `Item "${definition.name}" has different disabled values in Root.items and Questionnaire.Item.`
+ )
+ }
+
+ if (registration.required !== Boolean(definition.required)) {
+ warnings.push(
+ `Item "${definition.name}" has different required values in Root.items and Questionnaire.Item.`
+ )
+ }
+
+ const definedChoices = definition.choices ?? []
+ const definedChoiceByValue = new Map(
+ definedChoices.map((choice) => [choice.value, choice])
+ )
+ const registeredChoiceByValue = new Map(
+ registration.choices.map((choice) => [choice.value, choice])
+ )
+
+ for (const choice of definedChoices) {
+ const registeredChoice = registeredChoiceByValue.get(choice.value)
+
+ if (!registeredChoice) {
+ warnings.push(
+ `Choice "${choice.value}" is defined for item "${definition.name}" but has no rendered Questionnaire.Choice.`
+ )
+ continue
+ }
+
+ if (registeredChoice.disabled !== Boolean(choice.disabled)) {
+ warnings.push(
+ `Choice "${choice.value}" in item "${definition.name}" has different disabled values in Root.items and Questionnaire.Choice.`
+ )
+ }
+ }
+
+ if (shortcuts) {
+ for (const choice of registration.choices) {
+ if (!definedChoiceByValue.has(choice.value)) {
+ warnings.push(
+ `Rendered choice "${choice.value}" in item "${definition.name}" is missing from Root.items and will not receive a shortcut.`
+ )
+ }
+ }
+
+ const definedOrder = definedChoices
+ .filter((choice) => !choice.disabled)
+ .map((choice) => choice.value)
+ const registeredOrder = registration.choices
+ .filter((choice) => !choice.disabled)
+ .map((choice) => choice.value)
+
+ const sameChoices =
+ definedOrder.length > 0 &&
+ definedOrder.length === registeredOrder.length &&
+ definedOrder.every((choiceValue) =>
+ registeredOrder.includes(choiceValue)
+ )
+
+ if (
+ sameChoices &&
+ definedOrder.some(
+ (choiceValue, index) => choiceValue !== registeredOrder[index]
+ )
+ ) {
+ warnings.push(
+ `Choice order for item "${definition.name}" differs between Root.items and the rendered Questionnaire.Choice elements.`
+ )
+ }
+ }
+ }
+
+ for (const registration of registrations) {
+ if (
+ !registration.disabled &&
+ !collection.itemByName.has(registration.name)
+ ) {
+ warnings.push(
+ `Rendered item "${registration.name}" is missing from Root.items and is excluded from the questionnaire collection.`
+ )
+ }
+ }
+
+ return warnings
+}
+
+export {
+ createQuestionnaireCollection,
+ getCollectionDefinitionWarnings,
+ getCollectionRegistrationWarnings,
+ getInitialItemName,
+ getShortcutByChoiceValue,
+}
diff --git a/packages/react/src/questionnaire/components.tsx b/packages/react/src/questionnaire/components.tsx
new file mode 100644
index 00000000000..0b0513a6946
--- /dev/null
+++ b/packages/react/src/questionnaire/components.tsx
@@ -0,0 +1,551 @@
+import * as React from "react"
+
+import { mergeProps, useRender } from "../use-render"
+import {
+ QuestionnaireChoiceContext,
+ QuestionnaireContext,
+ QuestionnaireItemContext,
+ useQuestionnaireChoiceContext,
+ useQuestionnaireContext,
+ useQuestionnaireItemContext,
+} from "./context"
+import type {
+ QuestionnaireChoiceInputProps,
+ QuestionnaireChoiceLabelProps,
+ QuestionnaireChoiceProps,
+ QuestionnaireChoiceShortcutProps,
+ QuestionnaireChoicesProps,
+ QuestionnaireDescriptionProps,
+ QuestionnaireErrorProps,
+ QuestionnaireInputProps,
+ QuestionnaireItemProps,
+ QuestionnaireNavigationState,
+ QuestionnaireNextProps,
+ QuestionnairePreviousProps,
+ QuestionnaireProgressProps,
+ QuestionnaireRootProps,
+ QuestionnaireSkipProps,
+ QuestionnaireSubmitProps,
+ QuestionnaireTitleProps,
+} from "./types"
+import { useQuestionnaireChoice } from "./use-questionnaire-choice"
+import { useQuestionnaireInput } from "./use-questionnaire-input"
+import { useQuestionnaireItem } from "./use-questionnaire-item"
+import { useQuestionnaireRoot } from "./use-questionnaire-root"
+
+function QuestionnaireRoot({
+ defaultItem,
+ item,
+ items,
+ noValidate = true,
+ onItemChange,
+ onReset,
+ onSubmit,
+ ref,
+ shortcuts,
+ ...props
+}: QuestionnaireRootProps) {
+ const { context, rootProps, state } = useQuestionnaireRoot({
+ defaultItem,
+ item,
+ items,
+ noValidate,
+ onItemChange,
+ onReset,
+ onSubmit,
+ ref,
+ shortcuts,
+ })
+ const element = useRender({
+ defaultTagName: "form",
+ props: mergeProps<"form">({ ...rootProps, noValidate }, props),
+ state,
+ })
+
+ return (
+
+ {element}
+
+ )
+}
+
+function QuestionnaireProgress({
+ children,
+ render,
+ ...props
+}: QuestionnaireProgressProps) {
+ const { current, first, last, total } = useQuestionnaireContext(
+ "Questionnaire.Progress"
+ )
+ const label = total ? `Question ${current} of ${total}` : undefined
+
+ return useRender({
+ defaultTagName: "div",
+ props: mergeProps<"div">(
+ {
+ "aria-label": "Questionnaire progress",
+ "aria-live": "polite",
+ "aria-valuemax": total || undefined,
+ "aria-valuemin": total ? 1 : undefined,
+ "aria-valuenow": total ? current : undefined,
+ "aria-valuetext": label,
+ children: children ?? label,
+ role: "progressbar",
+ },
+ props
+ ),
+ render,
+ state: { current, first, last, total },
+ })
+}
+
+function QuestionnaireItem({
+ "aria-describedby": ariaDescribedBy,
+ "aria-keyshortcuts": ariaKeyShortcuts,
+ children,
+ disabled = false,
+ invalid = false,
+ multiple = false,
+ name,
+ onStatusChange,
+ ref,
+ required = false,
+ ...props
+}: QuestionnaireItemProps) {
+ const { context, itemProps, state } = useQuestionnaireItem({
+ "aria-describedby": ariaDescribedBy,
+ "aria-keyshortcuts": ariaKeyShortcuts,
+ disabled,
+ invalid,
+ multiple,
+ name,
+ onStatusChange,
+ ref,
+ required,
+ })
+ const element = useRender({
+ defaultTagName: "fieldset",
+ props: mergeProps<"fieldset">({ ...itemProps, children }, props),
+ state,
+ stateAttributesMapping: {
+ active: (isActive) => ({
+ "data-active": isActive ? "" : undefined,
+ }),
+ },
+ })
+
+ return (
+
+ {element}
+
+ )
+}
+
+function QuestionnaireTitle({ render, ...props }: QuestionnaireTitleProps) {
+ useQuestionnaireItemContext("Questionnaire.Title")
+
+ return useRender({
+ defaultTagName: "legend",
+ props,
+ render,
+ })
+}
+
+function QuestionnaireDescription({
+ id,
+ render,
+ ...props
+}: QuestionnaireDescriptionProps) {
+ const { registerDescription } = useQuestionnaireItemContext(
+ "Questionnaire.Description"
+ )
+ const generatedId = React.useId()
+ const descriptionId = id ?? generatedId
+
+ React.useLayoutEffect(
+ () => registerDescription(descriptionId),
+ [descriptionId, registerDescription]
+ )
+
+ return useRender({
+ defaultTagName: "p",
+ props: mergeProps<"p">({ id: descriptionId }, props),
+ render,
+ })
+}
+
+function QuestionnaireChoices({ render, ...props }: QuestionnaireChoicesProps) {
+ const { shortcuts } = useQuestionnaireItemContext("Questionnaire.Choices")
+
+ return useRender({
+ defaultTagName: "div",
+ props,
+ render,
+ state: { shortcuts },
+ })
+}
+
+function QuestionnaireChoice({
+ checked,
+ children,
+ defaultChecked = false,
+ disabled = false,
+ onChange,
+ render,
+ value,
+ ...props
+}: QuestionnaireChoiceProps) {
+ const { inputProps, state } = useQuestionnaireChoice({
+ checked,
+ defaultChecked,
+ disabled,
+ onChange,
+ value,
+ })
+ const element = useRender({
+ defaultTagName: "label",
+ props: mergeProps<"label">({ children }, props),
+ render,
+ state,
+ stateAttributesMapping: {
+ checked: (isChecked) => ({
+ "data-checked": isChecked ? "" : undefined,
+ "data-unchecked": isChecked ? undefined : "",
+ }),
+ },
+ })
+
+ return (
+
+ {element}
+
+ )
+}
+
+function QuestionnaireChoiceInput({
+ render,
+ ...props
+}: QuestionnaireChoiceInputProps) {
+ const { inputProps, state } = useQuestionnaireChoiceContext(
+ "Questionnaire.ChoiceInput"
+ )
+
+ return useRender({
+ defaultTagName: "input",
+ props: mergeProps<"input">(inputProps, props),
+ render,
+ state,
+ stateAttributesMapping: {
+ checked: (isChecked) => ({
+ "data-checked": isChecked ? "" : undefined,
+ "data-unchecked": isChecked ? undefined : "",
+ }),
+ },
+ })
+}
+
+function QuestionnaireChoiceLabel({
+ render,
+ ...props
+}: QuestionnaireChoiceLabelProps) {
+ useQuestionnaireChoiceContext("Questionnaire.ChoiceLabel")
+
+ return useRender({
+ defaultTagName: "span",
+ props,
+ render,
+ })
+}
+
+function QuestionnaireChoiceShortcut({
+ children,
+ render,
+ ...props
+}: QuestionnaireChoiceShortcutProps) {
+ const { state } = useQuestionnaireChoiceContext(
+ "Questionnaire.ChoiceShortcut"
+ )
+ const shortcutState = { shortcut: state.shortcut }
+
+ return useRender({
+ defaultTagName: "span",
+ props: mergeProps<"span">(
+ {
+ "aria-hidden": true,
+ children: children ?? state.shortcut,
+ hidden: state.shortcut === null,
+ },
+ props
+ ),
+ render,
+ state: shortcutState,
+ })
+}
+
+function QuestionnaireInput({
+ defaultValue,
+ disabled = false,
+ onChange,
+ ref,
+ render,
+ type = "text",
+ value,
+ ...props
+}: QuestionnaireInputProps) {
+ const { inputProps, state } = useQuestionnaireInput({
+ defaultValue,
+ disabled,
+ onChange,
+ ref,
+ type,
+ value,
+ })
+
+ return useRender({
+ defaultTagName: "input",
+ props: mergeProps<"input">(inputProps, props),
+ render,
+ state,
+ stateAttributesMapping: {
+ filled: (isFilled) => ({
+ "data-empty": isFilled ? undefined : "",
+ "data-filled": isFilled ? "" : undefined,
+ }),
+ },
+ })
+}
+
+function QuestionnaireError({
+ children,
+ id,
+ render,
+ ...props
+}: QuestionnaireErrorProps) {
+ const { invalid, registerError, required } = useQuestionnaireItemContext(
+ "Questionnaire.Error"
+ )
+ const generatedId = React.useId()
+ const errorId = id ?? generatedId
+
+ React.useLayoutEffect(() => registerError(errorId), [errorId, registerError])
+
+ return useRender({
+ defaultTagName: "p",
+ props: mergeProps<"p">(
+ {
+ children:
+ children ??
+ (required
+ ? "Choose an answer to continue."
+ : "Choose an answer or skip this question."),
+ hidden: !invalid,
+ id: errorId,
+ role: invalid ? "alert" : undefined,
+ },
+ props
+ ),
+ render,
+ state: { invalid },
+ })
+}
+
+function QuestionnairePrevious({
+ children,
+ disabled: disabledProp = false,
+ onClick,
+ render,
+ tabIndex,
+ type = "button",
+ ...props
+}: QuestionnairePreviousProps) {
+ const context = useQuestionnaireContext("Questionnaire.Previous")
+ const visible = context.total > 1 && !context.first
+
+ function handleClick(event: React.MouseEvent) {
+ onClick?.(event)
+
+ if (!event.defaultPrevented) {
+ context.goPrevious()
+ }
+ }
+
+ return useRenderNavigationButton({
+ children: children ?? "Previous",
+ disabled: disabledProp,
+ onClick: handleClick,
+ props,
+ render,
+ status: context.activeItemStatus,
+ tabIndex,
+ type,
+ visible,
+ })
+}
+
+function QuestionnaireSkip({
+ children,
+ disabled: disabledProp = false,
+ onClick,
+ render,
+ tabIndex,
+ type = "button",
+ ...props
+}: QuestionnaireSkipProps) {
+ const context = useQuestionnaireContext("Questionnaire.Skip")
+ const visible = context.activeItemRequired === false
+
+ function handleClick(event: React.MouseEvent) {
+ onClick?.(event)
+
+ if (!event.defaultPrevented) {
+ context.skipCurrent()
+ }
+ }
+
+ return useRenderNavigationButton({
+ children: children ?? "Skip",
+ disabled: disabledProp,
+ onClick: handleClick,
+ props,
+ render,
+ status: context.activeItemStatus,
+ tabIndex,
+ type,
+ visible,
+ })
+}
+
+function QuestionnaireNext({
+ children,
+ disabled: disabledProp = false,
+ onClick,
+ render,
+ tabIndex,
+ type = "button",
+ ...props
+}: QuestionnaireNextProps) {
+ const context = useQuestionnaireContext("Questionnaire.Next")
+ const visible = context.total > 1 && !context.last
+
+ function handleClick(event: React.MouseEvent) {
+ onClick?.(event)
+
+ if (!event.defaultPrevented) {
+ context.goNext()
+ }
+ }
+
+ return useRenderNavigationButton({
+ children: children ?? "Next",
+ disabled: disabledProp,
+ onClick: handleClick,
+ props,
+ render,
+ shortcut: "Enter",
+ status: context.activeItemStatus,
+ tabIndex,
+ type,
+ visible,
+ })
+}
+
+function QuestionnaireSubmit({
+ children,
+ disabled: disabledProp = false,
+ render,
+ tabIndex,
+ type = "submit",
+ ...props
+}: QuestionnaireSubmitProps) {
+ const context = useQuestionnaireContext("Questionnaire.Submit")
+ const visible = context.total > 0 && context.last
+
+ return useRenderNavigationButton({
+ children: children ?? "Submit",
+ disabled: disabledProp,
+ props,
+ render,
+ shortcut: "Enter",
+ status: context.activeItemStatus,
+ tabIndex,
+ type,
+ visible,
+ })
+}
+
+function useRenderNavigationButton({
+ children,
+ disabled,
+ onClick,
+ props,
+ render,
+ shortcut,
+ status,
+ tabIndex,
+ type,
+ visible,
+}: {
+ children: React.ReactNode
+ disabled: boolean
+ onClick?: React.MouseEventHandler
+ props: React.ComponentPropsWithRef<"button">
+ render: QuestionnaireNextProps["render"]
+ shortcut?: "Enter"
+ status: QuestionnaireNavigationState["status"]
+ tabIndex: number | undefined
+ type: "button" | "reset" | "submit"
+ visible: boolean
+}) {
+ const activeShortcut = visible && !disabled ? (shortcut ?? null) : null
+ const state: QuestionnaireNavigationState = {
+ disabled,
+ shortcut: activeShortcut,
+ status,
+ visible,
+ }
+
+ return useRender({
+ defaultTagName: "button",
+ props: mergeProps<"button">(
+ {
+ "aria-hidden": !visible || undefined,
+ "aria-keyshortcuts": activeShortcut ?? undefined,
+ children,
+ disabled,
+ hidden: !visible,
+ inert: !visible,
+ onClick,
+ tabIndex: visible ? tabIndex : -1,
+ type,
+ },
+ props
+ ),
+ render,
+ state,
+ stateAttributesMapping: {
+ visible: (isVisible) => ({
+ "data-hidden": isVisible ? undefined : "",
+ "data-visible": isVisible ? "" : undefined,
+ }),
+ },
+ })
+}
+
+export {
+ QuestionnaireChoice,
+ QuestionnaireChoiceInput,
+ QuestionnaireChoiceLabel,
+ QuestionnaireChoiceShortcut,
+ QuestionnaireChoices,
+ QuestionnaireDescription,
+ QuestionnaireError,
+ QuestionnaireInput,
+ QuestionnaireItem,
+ QuestionnaireNext,
+ QuestionnairePrevious,
+ QuestionnaireProgress,
+ QuestionnaireRoot,
+ QuestionnaireSkip,
+ QuestionnaireSubmit,
+ QuestionnaireTitle,
+}
diff --git a/packages/react/src/questionnaire/context.ts b/packages/react/src/questionnaire/context.ts
new file mode 100644
index 00000000000..74233db652b
--- /dev/null
+++ b/packages/react/src/questionnaire/context.ts
@@ -0,0 +1,59 @@
+import * as React from "react"
+
+import type {
+ QuestionnaireChoiceContextValue,
+ QuestionnaireContextValue,
+ QuestionnaireItemContextValue,
+} from "./types"
+
+const QuestionnaireChoiceContext =
+ React.createContext(null)
+const QuestionnaireContext =
+ React.createContext(null)
+const QuestionnaireItemContext =
+ React.createContext(null)
+
+function useQuestionnaireContext(component: string) {
+ const context = React.useContext(QuestionnaireContext)
+
+ if (!context) {
+ throw new Error(
+ `${component} must be used within a Questionnaire.Root component.`
+ )
+ }
+
+ return context
+}
+
+function useQuestionnaireChoiceContext(component: string) {
+ const context = React.useContext(QuestionnaireChoiceContext)
+
+ if (!context) {
+ throw new Error(
+ `${component} must be used within a Questionnaire.Choice component.`
+ )
+ }
+
+ return context
+}
+
+function useQuestionnaireItemContext(component: string) {
+ const context = React.useContext(QuestionnaireItemContext)
+
+ if (!context) {
+ throw new Error(
+ `${component} must be used within a Questionnaire.Item component.`
+ )
+ }
+
+ return context
+}
+
+export {
+ QuestionnaireChoiceContext,
+ QuestionnaireContext,
+ QuestionnaireItemContext,
+ useQuestionnaireChoiceContext,
+ useQuestionnaireContext,
+ useQuestionnaireItemContext,
+}
diff --git a/packages/react/src/questionnaire/index.ts b/packages/react/src/questionnaire/index.ts
new file mode 100644
index 00000000000..2c110b09bbb
--- /dev/null
+++ b/packages/react/src/questionnaire/index.ts
@@ -0,0 +1,45 @@
+import {
+ QuestionnaireChoice as Choice,
+ QuestionnaireChoiceInput as ChoiceInput,
+ QuestionnaireChoiceLabel as ChoiceLabel,
+ QuestionnaireChoices as Choices,
+ QuestionnaireChoiceShortcut as ChoiceShortcut,
+ QuestionnaireDescription as Description,
+ QuestionnaireError as Error,
+ QuestionnaireInput as Input,
+ QuestionnaireItem as Item,
+ QuestionnaireNext as Next,
+ QuestionnairePrevious as Previous,
+ QuestionnaireProgress as Progress,
+ QuestionnaireRoot as Root,
+ QuestionnaireSkip as Skip,
+ QuestionnaireSubmit as Submit,
+ QuestionnaireTitle as Title,
+} from "./components"
+
+export const Questionnaire = {
+ Root,
+ Progress,
+ Item,
+ Title,
+ Description,
+ Choices,
+ Choice,
+ ChoiceInput,
+ ChoiceLabel,
+ ChoiceShortcut,
+ Input,
+ Error,
+ Previous,
+ Skip,
+ Next,
+ Submit,
+}
+
+export type {
+ QuestionnaireChoiceDefinition,
+ QuestionnaireInputType,
+ QuestionnaireItemDefinition,
+ QuestionnaireItemStatus,
+ QuestionnaireShortcutMode,
+} from "./types"
diff --git a/packages/react/src/questionnaire/questionnaire-ssr.test.tsx b/packages/react/src/questionnaire/questionnaire-ssr.test.tsx
new file mode 100644
index 00000000000..85091eaa98d
--- /dev/null
+++ b/packages/react/src/questionnaire/questionnaire-ssr.test.tsx
@@ -0,0 +1,724 @@
+// @vitest-environment jsdom
+
+import * as React from "react"
+import { act } from "react"
+import { hydrateRoot, type Root } from "react-dom/client"
+import { renderToString } from "react-dom/server"
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
+
+import { Questionnaire } from "."
+import type { QuestionnaireItemDefinition } from "./types"
+
+const items = [
+ {
+ choices: [
+ { value: "delegation" },
+ { disabled: true, value: "automatic" },
+ { value: "questions" },
+ ],
+ name: "scope",
+ required: true,
+ },
+ {
+ choices: [{ value: "ignored" }],
+ disabled: true,
+ name: "disabled",
+ },
+ {
+ choices: [{ value: "focused" }, { value: "complete" }],
+ name: "detail",
+ },
+] as const satisfies readonly QuestionnaireItemDefinition[]
+
+let container: HTMLDivElement
+let root: Root | null
+
+beforeEach(() => {
+ ;(
+ globalThis as typeof globalThis & {
+ IS_REACT_ACT_ENVIRONMENT?: boolean
+ }
+ ).IS_REACT_ACT_ENVIRONMENT = true
+
+ container = document.createElement("div")
+ document.body.appendChild(container)
+ root = null
+})
+
+afterEach(async () => {
+ if (root) {
+ await act(async () => {
+ root?.unmount()
+ })
+ }
+
+ container.remove()
+ vi.restoreAllMocks()
+})
+
+describe("Questionnaire server rendering", () => {
+ it("renders collection progress, active item, actions, and shortcuts", () => {
+ renderServerQuestionnaire()
+
+ expect(form().dataset.current).toBe("1")
+ expect(form().dataset.total).toBe("2")
+ expect(form().hasAttribute("data-first")).toBe(true)
+ expect(progress().textContent).toBe("Question 1 of 2")
+ expect(progress().getAttribute("aria-valuenow")).toBe("1")
+ expect(progress().getAttribute("aria-valuemax")).toBe("2")
+ expect(item("scope").hasAttribute("data-active")).toBe(true)
+ expect(item("scope").hidden).toBe(false)
+ expect(item("disabled").hidden).toBe(true)
+ expect(item("detail").hidden).toBe(true)
+ expect(action("previous").hidden).toBe(true)
+ expect(action("skip").hidden).toBe(true)
+ expect(action("next").hidden).toBe(false)
+ expect(action("next").dataset.status).toBe("unanswered")
+ expect(action("submit").hidden).toBe(true)
+ expect(choice("delegation").dataset.shortcut).toBe("A")
+ expect(choiceInput("delegation").getAttribute("aria-keyshortcuts")).toBe(
+ "A"
+ )
+ expect(choice("automatic").hasAttribute("data-shortcut")).toBe(false)
+ expect(shortcut("automatic").hidden).toBe(true)
+ expect(choice("questions").dataset.shortcut).toBe("B")
+ })
+
+ it("renders a requested optional item and its applicable actions", () => {
+ renderServerQuestionnaire({ defaultItem: "detail" })
+
+ expect(form().dataset.current).toBe("2")
+ expect(form().hasAttribute("data-last")).toBe(true)
+ expect(progress().textContent).toBe("Question 2 of 2")
+ expect(item("scope").hidden).toBe(true)
+ expect(item("detail").hasAttribute("data-active")).toBe(true)
+ expect(action("previous").hidden).toBe(false)
+ expect(action("skip").hidden).toBe(false)
+ expect(action("next").hidden).toBe(true)
+ expect(action("submit").hidden).toBe(false)
+ })
+
+ it("renders a controlled item from the collection", () => {
+ renderMarkup( )
+
+ expect(progress().textContent).toBe("Question 2 of 2")
+ expect(item("scope").hidden).toBe(true)
+ expect(item("detail").hasAttribute("data-active")).toBe(true)
+ })
+
+ it.each(["missing", "disabled"])(
+ "falls back from an invalid default item %s during render",
+ (defaultItem) => {
+ renderServerQuestionnaire({ defaultItem })
+
+ expect(progress().textContent).toBe("Question 1 of 2")
+ expect(item("scope").hasAttribute("data-active")).toBe(true)
+ expect(item("detail").hidden).toBe(true)
+ }
+ )
+
+ it("renders numeric shortcuts and leaves overflow choices unassigned", () => {
+ const choices = Array.from({ length: 10 }, (_, index) => ({
+ value: `choice-${index + 1}`,
+ }))
+ const numericItems = [{ choices, name: "numeric" }]
+
+ renderMarkup(
+
+
+ Choose a number
+ {choices.map((choice) => (
+
+ ))}
+
+
+ )
+
+ expect(choice("choice-1").dataset.shortcut).toBe("1")
+ expect(choice("choice-9").dataset.shortcut).toBe("9")
+ expect(choice("choice-10").hasAttribute("data-shortcut")).toBe(false)
+ })
+
+ it("renders an input-only item without fixed-choice definitions", () => {
+ renderMarkup(
+
+
+
+ Describe the result
+
+
+
+
+ )
+
+ expect(progress().textContent).toBe("Question 1 of 1")
+ expect(item("input").hasAttribute("data-active")).toBe(true)
+ expect(query("input-control").type).toBe("text")
+ expect(action("submit").hidden).toBe(false)
+ })
+
+ it("renders both navigation directions for a middle item", () => {
+ const middleItems = [
+ { name: "first" },
+ { name: "middle" },
+ { name: "last" },
+ ]
+
+ renderMarkup(
+
+
+ {middleItems.map((definition) => (
+
+ {definition.name}
+
+ ))}
+
+
+
+
+ )
+
+ expect(progress().textContent).toBe("Question 2 of 3")
+ expect(action("previous").hidden).toBe(false)
+ expect(action("next").hidden).toBe(false)
+ expect(action("submit").hidden).toBe(true)
+ })
+
+ it("pins the current defaultChecked server-rendering limitation", () => {
+ const defaultItems = [{ choices: [{ value: "default" }], name: "answer" }]
+
+ renderMarkup(
+
+
+
+
+
+ )
+
+ expect(choiceInput("default").checked).toBe(false)
+ expect(choice("default").hasAttribute("data-checked")).toBe(false)
+ })
+})
+
+describe("Questionnaire hydration", () => {
+ it("hydrates without changing collection-derived output", async () => {
+ const consoleWarn = vi.spyOn(console, "warn").mockImplementation(() => {})
+ const onRecoverableError = vi.fn()
+ const questionnaire =
+
+ renderMarkup(questionnaire)
+ const initialMarkup = {
+ actions: actionsMarkup(),
+ progress: progress().outerHTML,
+ root: form().outerHTML.match(/