UI designer

Designer composables · Beta

Beta

The behaviour behind the Canvas, as building blocks for editors of your own.

One document, many views

DomDesigner and the Social post studio are built from the same composables. A session holds the document. The stage, the layers, the inspector and the code panel are all views of it. Edits go through the session as undoable operations, so every view updates together. A fix or a feature in a composable reaches every editor built on it.

Example

A small editor

A stage and a code panel on one session. Double-click the text to edit it, select words and press B, or edit the code. Undo covers both.

Release notes

Double-click this text, select a few words and press Bold. Or edit the code.

Install
npm install @getdom/studio
vue
<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomButton, DomDesignerStage } from '@getdom/studio';
import { DomCodePanel, DomEditorRender, createComponentCatalog, createEditorTypes, createWorkspace, useCodeSync, useEditorSession, useInlineText } from '../../../lib/designer';

// One session holds the document; every part below reads and writes through it.
const source = '<template>\n<article class="space-y-3 p-6">\n\t<h2 class="text-xl font-semibold">Release notes</h2>\n\t<p>Double-click this text, select a few words and press <strong>Bold</strong>. Or edit the code.</p>\n</article>\n</template>';
// Ids are counted, so a page rendered on the server matches when it hydrates.
let next = 0;
const session = useEditorSession({
	workspace: createWorkspace([{ name: 'Note.vue', source }], { widths: [600], makeId: () => `note${next++}` }),
});
const { selected, doc, activeArtboard, historySize, undo, redo } = session;
const types = createEditorTypes(createComponentCatalog([]));

// Double-click text on the stage to edit it in place.
const inline = useInlineText({ session });
const { editing, editState, revisions } = inline;

// The code of the same document, in sync both ways.
const stage = ref(null);
const codeSync = useCodeSync({ session, inline, element: (id) => stage.value?.$el.querySelector(`[data-designer-id="${id}"]`) || null });

const marks = [['bold', 'B'], ['italic', 'I'], ['underline', 'U'], ['strike', 'S']];
/** Marks the words selected on the stage while editing, else the text selected in the code. */
function mark(name) {
	if (editing.value) inline.toggle(name);
	else codeSync.mark(name);
}
function active(name) {
	return Boolean(editing.value ? editState.value.marks?.[name] : codeSync.marks.value?.[name]);
}
</script>

<template>
	<div class="grid gap-4 lg:grid-cols-2">
		<div class="min-w-0 overflow-hidden rounded-xl border border-border">
			<div class="flex items-center gap-1 border-b border-border px-2 py-1.5">
				<DomButton v-for="[name, label] in marks" :key="name" size="sm" :variant="active(name) ? 'primary' : 'ghost'" :aria-label="name" :aria-pressed="active(name)" @mousedown.prevent @click="mark(name)">{{ label }}</DomButton>
				<span class="flex-1"></span>
				<DomButton size="sm" variant="ghost" :disabled="!historySize.undo" @click="undo">Undo</DomButton>
				<DomButton size="sm" variant="ghost" :disabled="!historySize.redo" @click="redo">Redo</DomButton>
			</div>
			<DomDesignerStage ref="stage" v-model="selected" :editing="editing?.id || ''" class="min-h-48" @edit-text="inline.start(activeArtboard, $event)">
				<DomEditorRender :doc="doc" :types="types" :revisions="revisions" />
			</DomDesignerStage>
		</div>
		<DomCodePanel :sync="codeSync" label="Note.vue source" class="h-72 overflow-hidden rounded-xl border border-border" />
	</div>
</template>

@getdom/studio/designer

Import

The composables come from @getdom/studio/designer, the same entry as DomDesigner. It includes the Vue template compiler, so load it on the pages where people design. The APIs are in beta and may change.