"use client" /** * NewFocusTemplate — focused, single-task "New X" workflow shell. * * One template, three variants — pick the one that matches your task shape: * * 1. `variant="shell"` empty body slot — caller renders anything inside the * hero header (title + description + Back link). * * 2. `variant="workflow"` multi-step wizard like New placement — `StepIndicator`, * step content render-prop, sticky footer with progress * dots + Back / Next / Submit buttons (Kbd hints + bound * ⌘Enter / ⌘⌥← shortcuts). * * 3. `variant="form-inspector"` two-column split (builder + inspector scroll inside * their cards; main inset does not page-scroll) — New question. * Inspector is collapsible via the caller's controlled * open state. * * The template does NOT own form state. Callers wrap `` in a * `
` (or `` from `react-hook-form`) and pass step validators / submit * handlers — this keeps the template framework-agnostic. * * IMPORTANT — `` MUST flex inside the app shell row. * `NewFocusTemplate` renders a `PrimaryPageTemplate` which renders a `SidebarInset`. * The `SidebarInset` itself sits as a flex child of the `(app)/layout` row alongside * the primary sidebar + secondary panel + Ask Leo rail and uses `w-full flex-1` to * fill the remaining space. If the caller wraps `NewFocusTemplate` in a `` (which * is the canonical pattern for `react-hook-form`), the `` itself becomes the flex * child and `SidebarInset`'s `flex-1` no longer reaches the row. The page then * collapses to its intrinsic content width and renders as a thin column with the rest * of the viewport empty. ALWAYS apply `flex min-h-0 min-w-0 flex-1 flex-col` to the * wrapping `` so it behaves like a normal flex column host: * * ```tsx * * *
* ``` * * See `new-placement-form.tsx` and `new-library-item-form.tsx` for canonical usage. * * Focus workflow routes hide **both** primary sidebar and secondary panel * (`lib/focus-workflow.ts`). `SidebarAutoCollapse` collapses primary on mount. * * The template owns: * • Page chrome (`PrimaryPageTemplate` underneath: `SidebarInset`, `SiteHeader`). * • The hero `

` + description + Back link. * • For `workflow`: `StepIndicator`, step content render slot, sticky footer with * keyboard-shortcut Kbd hints + bound `` handlers (⌘Enter advance, * ⌘⌥← back, plain Enter submit on final step). * • For `form-inspector`: 2-column split panes — builder scrolls inside the left card, * inspector inside the right card (lg+). Uses `containScroll` so the shell stays * viewport-height like a focused workspace, unlike hub routes that page-scroll. * * WCAG 2.1 AA — same rules as `new-placement-form` / `new-library-item-form`: * ✓ Hero `

` carries the page title (only one h1 per route). * ✓ Step indicator uses `aria-current="step"` and visible labels (1.3.1). * ✓ Focus moves to step content when step changes (2.4.3). * ✓ Submit/Cancel/Back/Next buttons carry inline Kbd hints + Shortcut bindings. * ✓ Footer is sticky and contained within `
` so Enter on step 1..n-1 is no-op. */ import * as React from "react" import { Link } from "react-router-dom" import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { Kbd, KbdGroup } from "@/components/ui/kbd" import { Shortcut } from "@/components/ui/dropdown-menu" import { Tip } from "@/components/ui/tip" import { useModKeyLabel, useAltKeyLabel } from "@/hooks/use-mod-key-label" import { SidebarAutoCollapse } from "@/components/sidebar" import { PrimaryPageTemplate, type PrimaryPageTemplateProps, } from "@/components/templates/primary-page-template" import { AiThinkingOverlay } from "@/components/ui/ai-thinking-surface" import { DotPattern } from "@/components/ui/dot-pattern" // ─── Shared types ──────────────────────────────────────────────────────────── interface BackLink { href: string label?: string ariaLabel?: string } interface NewFocusBaseProps { /** Page `

` text. */ title: string /** Subhead below the title — short sentence describing the task. */ description?: React.ReactNode /** Back link rendered above the hero (and as `siteHeader.back` if `useSiteHeaderBack`). */ back: BackLink /** * When `true`, the parent route's `SiteHeader` carries the back-icon (parent-link only; * no hero back link rendered). Mirrors `NewQuestionPage` which passes `back` to `SiteHeader`. * Default `false` — Back link is rendered inline above the hero (matches `new-record/page`). */ useSiteHeaderBack?: boolean /** * Replace the default hero (Back link + `

` + description) with a fully custom node. * Use this when the page needs a `PageHeader`-style chrome (subtitle / actions / row), * e.g. New question composer with Save / More actions. Title and description props are * ignored when `header` is provided. */ header?: React.ReactNode /** * Render alongside the default hero `

` as right-aligned actions (filled CTA + overflow). * Ignored when `header` is provided. */ headerActions?: React.ReactNode /** * Optional ID-style subtitle rendered below the `

` in lieu of `description`. Use this * for stable record identifiers (e.g. draft question id + version). */ headerSubtitle?: React.ReactNode /** Override the `PrimaryPageTemplate` max-width. Default: `max-w-3xl` (workflow / shell), `max-w-[1100px]` (form-inspector). */ maxWidthClassName?: string /** Extra classes for the `PrimaryPageTemplate` body wrapper. Default sets `overflow-y-auto`. */ bodyClassName?: string /** Extra classes for the `PrimaryPageTemplate` content column. */ contentClassName?: string /** Optional extra chrome rendered before `SiteHeader` (e.g. command-menu). `SidebarAutoCollapse` is included by default. */ beforeSiteHeader?: React.ReactNode /** Customize the `siteHeader` props passed to `PrimaryPageTemplate`. */ siteHeader?: PrimaryPageTemplateProps["siteHeader"] } // ─── Variant 1: shell ──────────────────────────────────────────────────────── interface ShellVariantProps extends NewFocusBaseProps { variant: "shell" /** Body content rendered below the hero. */ children: React.ReactNode } // ─── Variant 2: workflow ───────────────────────────────────────────────────── export interface NewFocusStep { /** Stable identifier for the step (used as React key + `STEP_FIELDS` map key). */ id: string /** Short label shown under the step circle (e.g. "Student"). */ label: string /** Optional Font Awesome glyph (e.g. `fa-user-graduate`) shown in the section heading. */ icon?: string /** Render the step body. Receives the active step index (0-based). */ render: (ctx: { stepIndex: number; isActive: boolean }) => React.ReactNode } interface WorkflowVariantProps extends NewFocusBaseProps { variant: "workflow" /** Ordered step list. Length must be ≥ 1. */ steps: NewFocusStep[] /** Active step (0-based). The caller owns this state so a step can be reached via review-section "Edit". */ step: number /** Called when the user clicks a step circle directly (jump). Optional — omit to disable jumps. */ onStepChange?: (next: number) => void /** * Called when Next / ⌘Enter is invoked. Caller validates the current step's fields and * returns `true` if advance should proceed. Async-friendly for `react-hook-form` triggers. */ onNext: () => boolean | Promise /** Called when the form is submitted on the final step. */ onSubmit: () => void | Promise /** Label on the final-step submit button (e.g. "Create placement"). */ submitLabel: string /** Optional icon glyph for the submit button (defaults to `fa-check`). */ submitIcon?: string /** Disable inputs / show spinner when `true`. */ submitting?: boolean /** Override the Next button label. Default `"Next"`. */ nextLabel?: string /** Override the Back button label. Default `"Back"`. */ backLabel?: string /** Scroll to top of window when step changes. Default `true`. */ scrollOnStepChange?: boolean } // ─── Variant 3: form-inspector ─────────────────────────────────────────────── interface FormInspectorVariantProps extends NewFocusBaseProps { variant: "form-inspector" /** Form body rendered in the left column. */ children: React.ReactNode /** * Inspector body rendered in the right rail (sticky on lg+). * Either a static node (template handles open/close by toggling visibility) OR a render * function that receives `{ open }` so the caller can render different content in collapsed * state (e.g. show a single "Open inspector" button when `!open`). Use the function form * when the caller's inspector has its own internal show/hide affordance. */ inspector: React.ReactNode | ((ctx: { open: boolean }) => React.ReactNode) /** `true` → inspector expanded; `false` → collapsed to a rail with an "Open" affordance. */ inspectorOpen: boolean /** Called when the toolbar inspector toggle is clicked. */ onInspectorOpenChange: (open: boolean) => void /** Width of the inspector rail when open. Default `"320px"`. */ inspectorOpenWidth?: string /** Width of the inspector rail when collapsed. Default `"3.5rem"` (~56px). */ inspectorCollapsedWidth?: string /** Accessible label on the inspector `