@frozencloud/studio

Runtime block editor for Nuxt — edit pages visually in a database-backed dashboard, using your existing components without rewriting them
5,362
Install
npm install @frozencloud/studio
Latest Version:0.2.36
Vue:3
License:MIT
Last Updated:Jul 1, 2026
Links: npm
Maintainer: niki2k1

@frozencloud/studio

Runtime, database-backed block editor for Nuxt. Edit pages visually in a dashboard using your existing Vue components without rewriting them — no field definitions, no markdown+git+rebuild cycle.

  • Your components drive the editor. Prop forms are auto-generated from your stock SFCs (typed props, defaults, JSDoc descriptions, unions, arrays of objects) via nuxt-component-meta. Prop names steer the inputs: *icon* props get an Iconify picker, html/content/body props get a WYSIWYG editor.
  • Pages live in Postgres. Trees are stored as Comark AST (JSON) via Drizzle and rendered at runtime — publishing never rebuilds the app.
  • Pruvious-style editor. Block tree with icons and slot nesting, live iframe preview of the real page, prop form; click-to-select in the preview, drag/drop, undo/redo, resizable sidebars, named page variants.
  • AI built in (optional). Compose sections from your catalog by prompt, or generate entirely new components — compiled server-side, previewed in a sandbox, and gated by approval before they reach a page.

Feature tour

Area What you get
Dashboard /studio Page list grouped into collapsible folders by path segment; create/duplicate/delete/publish
Editor /studio/:pageId 3-pane layout (resizable sidebars, sizes persist): block tree · live preview · prop form
Block tree Per-block icons, hover actions, "Add block" modal (searchable icon grid + per-block "?" docs view), "Add inner block" for components with a default slot
Visual editing Click/hover selection synced both ways with overlay labels, SortableJS drag/drop (slot-aware drop targets), undo/redo, copy/paste, Delete key
Prop forms Generated from JSON schema: string/number/boolean/enum inputs, repeatable groups for arrays of objects, icon picker, rich text (UEditor), slot text
Variants Named snapshots of a page's draft — save, restore (undoable), delete; backed by studio_page_variants
Theming /studio/theme Site-wide runtime theme (Nuxt UI semantic colors + radius) with live sample preview; StudioTheme block re-themes just its nested blocks; preview light/dark toggle in the editor
AI tier 1 "Generate" in the editor: prompt → block tree using only catalog components → preview in iframe → accept/discard
AI tier 2 /studio/components: prompt → new SFC, compiled with the host's vue/compiler-sfc, sandboxed preview, approve/reject
Component workbench /studio/components/:id: monaco editor · sandbox preview · AI chat (streamed, "Apply to editor"); hand-write components without AI; edits force re-approval
Runtime mount Approved components mount client-side on public pages and join the editor catalog like any allowlisted block (SSR deferred)

Setup

pnpm add @frozencloud/studio drizzle-orm@^1.0.0-rc.3
// nuxt.config.ts
export default defineNuxtConfig({
  modules: ["@frozencloud/studio"],
  studio: {
    // allowlist of editor-visible components (dirs or globs, relative to srcDir)
    components: ["components/blocks"],
  },
  components: [
    // blocks must be globally registered so the renderer can resolve them by name
    { path: "~/components/blocks", global: true, pathPrefix: false },
    "~/components",
  ],
});

The dashboard is built on Nuxt UI (installed by the module) — wrap your app in <UApp> in app.vue if you don't already.

Database

The module never owns a connection or migrations. Hand it your Drizzle instance from a Nitro plugin:

// server/plugins/studio.ts
import pg from "pg";
import { drizzle } from "drizzle-orm/node-postgres";

export default defineNitroPlugin(() => {
  const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
  setStudioDatabase(drizzle({ client: pool }));
});

Add the schema to your own migration pipeline:

// drizzle.config.ts — point drizzle-kit at the exported schema
export default defineConfig({
  dialect: "postgresql",
  schema: ["./server/database/schema.ts", "./node_modules/@frozencloud/studio/dist/db.mjs"],
  // …
});

The schema is also importable as @frozencloud/studio/db (studioPages, studioComponents, studioPageVariants, studioSettings, types).

Auth

Studio routes (/studio/**, /api/_studio/** except the public render endpoint, the approved-components feed and token-gated sandbox documents) are open in dev and forbidden in production until you register an authorizer. With nuxt-auth-utils:

// server/plugins/studio.ts (same plugin as the database)
setStudioAuthorize(async (event) => {
  const session = await getUserSession(event);
  return !!session.user;
});

Any callback (event: H3Event) => boolean | Promise<boolean> works.

Theming

Studio pages adopt Nuxt UI's runtime theming: colors are CSS variables, so they change at runtime — no rebuild.

  • Site theme (/studio/theme): assign Tailwind palettes to the semantic colors (primary, secondary, neutral, …) and pick a radius preset. Stored in studio_settings, applied by the renderer around every studio page (layout chrome included); the dashboard itself is unaffected. Unset keys fall back to the host's app.config.ts colors.
  • Sub-theme blocks: the built-in StudioTheme block wraps other blocks in its default slot and re-scopes the same variables for just that section. Nest them — inner themes inherit whatever they don't override. The AI assistant knows about it ("make this section emerald").
  • Preview color mode: the editor toolbar toggles the preview iframe between light and dark without touching your dashboard preference.

Custom palettes work too: any palette defined in the host's CSS with @theme static { --color-brand-50: …; } can be typed into the theme API (PUT /api/_studio/site-theme, e.g. { "primary": "brand" }) — the pickers only list Tailwind's built-ins.

AI (optional)

Generation needs one of two credentials at runtime — without one, AI endpoints return a clean 503 and the rest of studio works normally:

Env var Provider
NUXT_STUDIO_AI_API_KEY (or ANTHROPIC_API_KEY) Anthropic direct
NUXT_AI_GATEWAY_API_KEY (or AI_GATEWAY_API_KEY) Vercel AI Gateway — model ids are mapped automatically (claude-opus-4-8 → anthropic/claude-opus-4.8)

Generated components are constrained to plain-JavaScript SFCs with vue-only imports and scoped CSS, compiled and validated server-side, and never served to visitors until approved in /studio/components.

Options

Option Default Description
studio.enabled true Master switch. When false the module registers nothing — no routes, pages or code.
studio.components [] Allowlist of editor-visible components (dirs/globs relative to srcDir).
studio.ai.enabled true Disable the AI endpoints and editor UI entirely.
studio.ai.model claude-opus-4-8 Model id (also overridable via NUXT_STUDIO_AI_MODEL).

Preview tokens are signed with runtimeConfig.studio.previewSecret (override via NUXT_STUDIO_PREVIEW_SECRET; defaults to a per-build random value).

How it works

  1. Catalog — nuxt-component-meta extracts props/slots from allowlisted SFCs; /api/_studio/components serves them as JSON Schema, which drives the auto-generated prop forms, the block picker docs, and the AI prompts. Approved runtime components merge into the same catalog.
  2. Storage — pages live in studio_pages (draft tree + published tree as jsonb, versioned); snapshots in studio_page_variants; generated components in studio_components (source + compiled + meta + approval status).
  3. Rendering — a lowest-priority catch-all route resolves unknown paths against the database and renders the published tree with @comark/vue's ComarkRenderer. Your explicit routes always win; publishing requires no rebuild. A client plugin mounts approved runtime components globally.
  4. Editing — the editor iframe loads the real page with a short-lived signed preview token (draft tree); edits stream into it over a same-origin postMessage protocol (tree:update, select, hover, highlight).
  5. AI component pipeline — prompt → SFC → server compile (host's vue/compiler-sfc, bundler-free output) → pending registry row → opaque-origin sandbox preview (<iframe sandbox="allow-scripts"> + CSP) → approval → public runtime feed.

For the full build log — per-phase decisions, verified exit criteria and gotchas — see PLAN.md.

Contribution

Local development
# Install dependencies
vp install

# Generate type stubs
npm run dev:prepare

# Start the playground database, migrate and seed it
cd playground && npm run db:up && npm run db:migrate && npm run db:seed && cd ..

# Develop with the playground
npm run dev

# Lint, type check, test, build
vp check
npm run test:types
vp test
npm run prepack

Module changes (src/module.ts routes/pages) need a dev-server restart; if generated route types stay stale, rebuild the stub and clear the jiti cache: npx nuxt-module-build build --stub && rm -rf node_modules/.cache/jiti && npx nuxt prepare playground.

Version History

Version Released Vue License
0.2.36 ^3.0 MIT
0.2.35 ^3.0 MIT
0.2.34 ^3.0 MIT
0.2.33 ^3.0 MIT
0.2.32 ^3.0 MIT