Skip to main content

Studio

Studio

Full-screen editor frame - outline, live canvas and inspector - for survey, slide and page builders

Tags: component layout editor

View Demo

Studio is the frame for anything people build: surveys, slide decks, forms, pages. It replaces the "record page with a form bolted on" pattern with one learnable layout:

┌ StudioBar: ← Title · Saved   [Build | Settings | Share]   ⚠ 2  Publish  ⋯ ┐
├ Outline ──────┬ Canvas ───────────────────────┬ Inspector ────────────────┤
│ ⠿ 1 Question  │ the real thing, as people see │ settings for the          │
│ ⠿ 2 Question  │ it - click to select,         │ selection only            │
│ + Add         │ "+" between items to insert   │                           │
└───────────────┴───────────────────────────────┴───────────────────────────┘

The rules it encodes:

  • One frame, no page scroll. Outline, canvas and inspector scroll on their own.
  • Modes, not fake rows. Building lives in Build; configuring (settings, audience, sharing) lives in its own modes rendered as a StudioPage.
  • The canvas is the preview. No separate form column and preview column to map between.
  • One add path (InsertPalette), one reorder path (drag handle + keyboard), one save signal (SaveStatus), one issues list (IssuesButton).
  • Responsive by default. Below 1280px the outline becomes a left sheet; below 768px the inspector becomes a bottom sheet that opens when something is picked.

See the full survey-builder example at /studio-demo.

Import​

<script>
  import {
    Studio,
    StudioBar,
    StudioOutline,
    StudioCanvas,
    StudioCanvasItem,
    StudioInspector,
    StudioInspectorSection,
    StudioPage,
    SaveStatus,
    IssuesButton,
    InsertPalette,
  } from '@classic-homes/theme-svelte';
</script>

Basic Usage​

<script>
  let questions = $state(loadQuestions());
  let selected = $state(questions[0].id);
  let mode = $state('build');
  let adding = $state(false);
</script>

{#snippet bar()}
  <StudioBar
    bind:title
    backHref="/surveys"
    backLabel="Surveys"
    bind:mode
    modes={[
      { value: 'build', label: 'Build', icon: 'edit' },
      { value: 'settings', label: 'Settings', icon: 'settings' },
    ]}
    menuItems={[{ label: 'Delete survey', icon: 'trash', destructive: true, onSelect: remove }]}
  >
    {#snippet status()}<SaveStatus state={saveState} onRetry={save} />{/snippet}
    {#snippet issues()}<IssuesButton {issues} bind:open={issuesOpen} onSelect={jump} />{/snippet}
    {#snippet actions()}<Button size="sm" onclick={publish}>Publish</Button>{/snippet}
  </StudioBar>
{/snippet}

{#snippet rail()}
  <StudioOutline
    items={questions.map((q) => ({ id: q.id, title: q.title, subtitle: q.typeLabel }))}
    bind:selected
    onReorder={move}
    onAdd={() => (adding = true)}
    addLabel="Add question"
    label="Questions"
  />
{/snippet}

{#snippet canvas()}
  <StudioCanvas deviceToggle>
    {#each questions as q, i (q.id)}
      <StudioCanvasItem
        label="Question {i + 1}"
        selected={q.id === selected}
        onSelect={() => (selected = q.id)}
        onInsert={() => (adding = true)}
      >
        <QuestionPreview question={q} />
      </StudioCanvasItem>
    {/each}
  </StudioCanvas>
{/snippet}

{#snippet inspector()}
  <StudioInspector title="Question 1" description="Single choice">
    <StudioInspectorSection><!-- content fields --></StudioInspectorSection>
    <StudioInspectorSection title="Advanced" collapsible
      ><!-- rarely used --></StudioInspectorSection
    >
  </StudioInspector>
{/snippet}

{#if mode === 'build'}
  <Studio {bar} {rail} {canvas} {inspector} railLabel="Questions" />
{:else}
  <Studio {bar} canvas={settingsPage} />
{/if}

<InsertPalette bind:open={adding} groups={questionTypes} onSelect={add} title="Add a question" />

React​

The same parts ship in @classic-homes/theme-react with React conventions: snippets become ReactNode props (bar, rail, canvas, inspector, status, actions...), bind: values become controlled props with on…Change callbacks (mode/onModeChange, railOpen/onRailOpenChange, open/onOpenChange), thumbnail becomes renderThumbnail, and parts read the layout with useStudio() instead of getStudioContext(). Icons are ReactNodes rather than icon names. See /studio in the React example app.

Props​

Studio​

Prop Type Default Description
bar Snippet required Top bar, normally a StudioBar (required)
canvas Snippet required Main area: a StudioCanvas or StudioPage (required)
rail Snippet - Left column, normally a StudioOutline
inspector Snippet - Right column, normally a StudioInspector
banner Snippet - One slot under the bar for conflicts and blocking decisions
railLabel string 'Outline' Name of the rail and its sheet button
inspectorLabel string 'Properties' Name of the inspector and its sheet button
railOpen boolean false Rail sheet open below 1280px (bindable)
inspectorOpen boolean false Inspector sheet open below 768px (bindable)

StudioBar​

Prop Type Default Description
title string '' Document title, edited in place (bindable)
onTitleChange (title: string) => void - Called on blur/Enter; Escape reverts
titleReadonly boolean false Show the title as text
subtitle string - e.g. "Survey · 5 questions · Draft"
backHref string - Back link
modes SegmentedControlOption[] - Build / Settings / Share...
mode string first mode Active mode (bindable)
status Snippet - Normally a SaveStatus
issues Snippet - Normally an IssuesButton
actions Snippet - Primary action (Publish, Present)
menuItems (DropdownMenuItem | DropdownMenuGroup)[] - Everything else: export, history, delete

StudioOutline​

Prop Type Default Description
items StudioOutlineEntry[] - { id, title, subtitle?, icon?, issues?, muted?, badge? }
selected string | null null Selected id (bindable)
onSelect (id) => void - Row picked
onReorder (from, to) => void - Enables drag handles and Move up/down
menuItems (entry, index) => DropdownMenuItem[] - Row menu (duplicate, delete...)
onAdd () => void - Shows the add button
thumbnail Snippet<[entry, index]> - Preview per row (slides)

StudioCanvas / StudioCanvasItem​

Prop Type Default Description
deviceToggle boolean false Desktop/Phone switch (hidden on phones)
device 'desktop' | 'phone' 'desktop' Bindable
maxWidth string '48rem' Frame width on desktop
onBackgroundClick () => void - Click outside any item, usually clears selection
label (item) string - e.g. "Question 2" (required)
selected (item) boolean false Ring + toolbar
toolbar (item) Snippet - Quick actions on the selected item
onInsert (item) () => void - "+" below the item
invalid (item) boolean false Dashed amber outline

SaveStatus / IssuesButton / InsertPalette​

Component Key props
SaveStatus state: 'saved' | 'saving' | 'unsaved' | 'error' | 'offline' | 'conflict' | 'invalid' | 'readonly', detail?, onRetry?
IssuesButton issues: { id, message, severity?, location? }[], onSelect, open (bindable)
InsertPalette groups: { label, items: { id, label, description?, icon?, keywords? }[] }[], onSelect, thumbnail?

Keyboard Navigation​

  • Outline: ArrowUp / ArrowDown / Home / End move between rows; Enter selects
  • Drag handle: Space/Enter picks up, arrows move, Space/Enter drops, Escape cancels; Alt+Arrow moves one step
  • Canvas: tabbing into an item selects it; items without focusable content get a hidden "Edit …" button
  • Title: Enter commits, Escape reverts
  • Insert palette: type to filter, Enter adds the first match, arrows move between entries

Accessibility​

  • Rail and inspector are labelled aside landmarks; the canvas is a labelled section.
  • Every reorder is announced (moved to position 3 of 5).
  • Issue counts are text, never colour alone; SaveStatus only announces problems, not every save.
  • Problems are a dashed amber outline so they never look like the (brand-red) selection ring.
On this page