import { z } from "zod"; import type { BlockMdxConfig } from "../types.js"; /** * Pure (React-free) part of the shared `question-form` and `visual-questions` * blocks: their data schema and MDX round-trip config. Lives in core so any * app's server/shared registry and the client spec (`question-form.tsx`) * consume one definition. Keeping it React-free means importing it into a server * module never pulls React into the Nitro/SSR bundle. * * `question-form` is the general-purpose respondent-facing form (Open Questions, * single/multi/freeform answers, recommended options, optional inline * wireframe/diagram previews). `visual-questions` is the same data shape and UI, * branded for an explicit visual-intake flow before a plan. Both originated in * the plan template (the data shape mirrors the legacy `visual-questions` block) * before moving here, so their MDX `tag` + attribute encoding MUST match the * historical `` / `` * forms so stored `.mdx` round-trips byte-compatibly. * * The per-option `wireframe`/`diagram` previews stay opaque (`unknown`) at the * core layer: the core block renders them through `ctx.renderBlock` (the same * nested-block seam tabs/columns use) so core never imports an app's wireframe * or diagram renderer. Each app supplies the matching `wireframe`/`diagram` * block spec and shape. */ export type QuestionMode = "single" | "multi" | "freeform"; export interface QuestionFormOption { id: string; label: string; detail?: string; /** Authored recommendation only (not a runtime answer). */ recommended?: boolean; /** * Optional inline visual previews for this option. Kept opaque at the core * layer; rendered via `ctx.renderBlock` as a nested `wireframe`/`diagram` * block so core stays app-agnostic. Apps validate the concrete shape with * their own wireframe/diagram schemas. */ wireframe?: unknown; diagram?: unknown; } export interface QuestionFormQuestion { id: string; title: string; subtitle?: string; mode: QuestionMode; options?: QuestionFormOption[]; allowOther?: boolean; placeholder?: string; required?: boolean; } export interface QuestionFormData { questions: QuestionFormQuestion[]; submitLabel?: string; } /** `visual-questions` shares the exact `question-form` data shape. */ export type VisualQuestionsData = QuestionFormData; const idSchema = z.string().trim().min(1).max(120); const questionOptionSchema = z.object({ id: idSchema, label: z.string().trim().min(1).max(220), detail: z.string().trim().max(800).optional(), recommended: z.boolean().optional(), // Opaque at the core layer — the inline preview renders via `ctx.renderBlock` // with the app's own wireframe/diagram block spec; the app validates the shape. wireframe: z.unknown().optional(), diagram: z.unknown().optional(), }) as z.ZodType; const questionSchema = z.object({ id: idSchema, title: z.string().trim().min(1).max(260), subtitle: z.string().trim().max(700).optional(), mode: z.enum(["single", "multi", "freeform"]), options: z.array(questionOptionSchema).max(40).optional(), allowOther: z.boolean().optional(), placeholder: z.string().trim().max(240).optional(), required: z.boolean().optional(), }) as z.ZodType; export const questionFormSchema = z.object({ questions: z.array(questionSchema).min(1).max(40), submitLabel: z.string().trim().max(80).optional(), }) as unknown as z.ZodType; /** `visual-questions` validates with the same schema as `question-form`. */ export const visualQuestionsSchema = questionFormSchema as unknown as z.ZodType; /** * MDX config: `questions` and `submitLabel` are both attributes (the questions * array is JSON-encoded by the shared `prop()` encoder). Mirrors the legacy * `` encoding so stored `.mdx` * round-trips byte-compatibly. */ export const questionFormMdx: BlockMdxConfig = { tag: "QuestionForm", toAttrs: (data) => ({ questions: data.questions, submitLabel: data.submitLabel, }), fromAttrs: (attrs) => ({ questions: attrs.array("questions") ?? [], submitLabel: attrs.string("submitLabel"), }), }; /** `visual-questions` uses the historical `` tag. */ export const visualQuestionsMdx: BlockMdxConfig = { tag: "VisualQuestions", toAttrs: (data) => ({ questions: data.questions, submitLabel: data.submitLabel, }), fromAttrs: (attrs) => ({ questions: attrs.array("questions") ?? [], submitLabel: attrs.string("submitLabel"), }), };