@tooto-tech/tootix-studio-sdk
v0.1.1
Published
Embeddable Tootix visual builder SDK powered by GrapesJS and Vue.
Readme
@tooto/tootix-studio-sdk
Embeddable Tootix visual builder SDK powered by GrapesJS and Vue 3.
The SDK is designed to be usable by a host application with only a root element. IndexedDB storage is enabled by default, styles are shipped with the package, and host applications can extend the studio through Tootix plugins or native GrapesJS plugins.
Install
pnpm add @tooto/tootix-studio-sdk vuevue is a peer dependency. GrapesJS and the editor runtime dependencies are bundled as package dependencies.
Quick Start
Import the JavaScript initializer and the package stylesheet:
import createTootixStudio from '@tooto/tootix-studio-sdk'
import '@tooto/tootix-studio-sdk/style'
const studio = createTootixStudio({
root: '#studio',
options: {
project: {
id: 'demo',
},
},
})
studio.getEditor()
studio.destroy()The host element should provide usable layout space:
<div id="studio" style="height: 100vh"></div>options.project.id defaults to default-project. The legacy top-level projectId field is still supported. If no storage adapter is provided, project data is stored in IndexedDB under that project id.
Vue Usage
<script setup lang="ts">
import TootixStudio from '@tooto/tootix-studio-sdk/vue'
import '@tooto/tootix-studio-sdk/style'
const studioOptions = {
project: {
id: 'demo',
},
}
</script>
<template>
<TootixStudio :options="studioOptions" />
</template>The Vue component accepts the same runtime options as the initializer, except root. The legacy project-id, plugins, storage, theme, height, and on-back props remain supported.
Initializer Options
import type {
TootixStudioConfig,
TootixStudioOptions,
TootixStudioStorageAdapter,
} from '@tooto/tootix-studio-sdk'
const options: TootixStudioOptions = {
root: '#studio',
options: {
project: {
id: 'demo',
},
identity: {
id: 'end-user-1',
},
storage,
plugins,
theme: 'light',
height: '100vh',
onBack: () => history.back(),
} satisfies TootixStudioConfig,
}| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| root | string \| HTMLElement | required | Mount target for the JavaScript initializer. |
| options.project.id | string | default-project | Key used by the storage adapter. |
| options.identity.id | string | undefined | Host end-user id for Studio-style configuration. Reserved for host/plugin use. |
| options.storage | TootixStudioStorageAdapter \| StorageConfig | IndexedDB | Custom persistence adapter or { type: 'indexeddb' } / { type: 'api' } config. |
| options.plugins | TootixStudioPluginInput | [] | Recommended extension API. Accepts one plugin entry or an array, including Tootix plugins and native GrapesJS plugin functions. |
| options.extensions | readonly WebBuilderExtension[] | [] | Deprecated compatibility alias for older contribution objects. |
| options.theme | 'light' \| 'dark' \| 'system' | stored preference or light | Sets the SDK theme mode. |
| options.height | string | 100vh | Applied to the SDK root. |
| options.onBack | () => void \| Promise<void> | hidden | Shows and handles the toolbar back button when provided. |
For compatibility, the same fields can still be passed at the top level next to root. Explicit top-level fields take precedence over options.
Styles
Always import the SDK stylesheet once in the host application:
import '@tooto/tootix-studio-sdk/style'The stylesheet includes GrapesJS CSS, SDK editor theme CSS, Vue component CSS, and generated Tailwind utilities used by SDK templates. The SDK Tailwind utilities use the tw- prefix, so host Tailwind classes should not collide with SDK internals.
Theme variables and layout resets are scoped under .tootix-studio-sdk. The SDK should not require the host to configure Tailwind and should not apply global body or html theme styles.
Storage
IndexedDB
IndexedDB is the default storage. You can also configure the database name or store name explicitly:
import createTootixStudio, { createIndexedDbStorage } from '@tooto/tootix-studio-sdk'
import '@tooto/tootix-studio-sdk/style'
createTootixStudio({
root: '#studio',
options: {
project: { id: 'cms-home' },
storage: createIndexedDbStorage({
databaseName: 'cms-studio',
storeName: 'projects',
}),
},
})HTTP API
import createTootixStudio, { createApiStorage } from '@tooto/tootix-studio-sdk'
const storage = createApiStorage({
loadUrl: (projectId) => `/api/studio/projects/${projectId}`,
storeUrl: (projectId) => `/api/studio/projects/${projectId}`,
headers: {
Authorization: `Bearer ${token}`,
},
})
createTootixStudio({
root: '#studio',
options: {
project: { id: 'cms-home' },
storage,
},
})Custom Adapter
import type { TootixStudioStorageAdapter } from '@tooto/tootix-studio-sdk'
const storage: TootixStudioStorageAdapter = {
async load(projectId) {
return await loadProjectData(projectId)
},
async store(projectId, data) {
await saveProjectData(projectId, data)
},
}load must return GrapesJS project data. Return {} for a new empty project.
Plugin System
Use options.plugins for new extensions. It accepts either a single plugin entry or an array. A plugin can contribute SDK UI, SDK commands, GrapesJS runtime behavior, and editor blocks. Native GrapesJS plugin functions can be placed in the same array.
import createTootixStudio, {
defineTootixPlugin,
type TootixStudioConfig,
} from '@tooto/tootix-studio-sdk'
import '@tooto/tootix-studio-sdk/style'
import type { Plugin } from 'grapesjs'
import { h, type Component } from 'vue'
const CmsPagesPanel: Component = {
name: 'CmsPagesPanel',
setup() {
return () => h('div', { style: { padding: '12px' } }, 'CMS pages')
},
}
const cmsPlugin = defineTootixPlugin<{ apiBase: string }>({
id: 'cms',
setup(ctx) {
return {
panels: [
{
id: 'cms-pages',
title: 'Pages',
region: 'primary',
component: CmsPagesPanel,
order: 20,
},
],
activityBar: [
{
id: 'cms-pages',
label: 'Pages',
icon: 'lucide:file-stack',
panelId: 'cms-pages',
order: 20,
},
],
commands: [
{
id: 'cms:sync',
async run({ editor }) {
await fetch(`${ctx.options.apiBase}/sync`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(editor.getProjectData()),
})
},
},
],
toolbar: [
{
id: 'cms-sync',
label: 'Sync',
icon: 'lucide:refresh-cw',
placement: 'right',
command: 'cms:sync',
order: 100,
},
],
runtimePlugins: [
{
id: 'cms-runtime',
setup(editor) {
editor.on('load', () => {
console.info('CMS plugin loaded')
})
},
},
],
}
},
})
const grapesjsPlugin: Plugin = (editor) => {
editor.Commands.add('cms:native-grapes-command', {
run() {
console.info('native GrapesJS command')
},
})
}
const options: TootixStudioConfig = {
project: {
id: 'demo',
},
identity: {
id: 'end-user-1',
},
plugins: [
cmsPlugin.init({ apiBase: '/api/cms' }),
grapesjsPlugin,
],
}
createTootixStudio({
root: '#studio',
options,
})For one plugin entry, pass it directly:
createTootixStudio({
root: '#studio',
options: {
project: { id: 'demo' },
plugins: cmsPlugin.init({ apiBase: '/api/cms' }),
},
})The older useTootixPlugin(plugin, options) helper is still exported and is equivalent to plugin.init(options).
Native GrapesJS plugins can still be passed next to Tootix plugins:
createTootixStudio({
root: '#studio',
options: {
project: { id: 'demo' },
plugins: [
cmsPlugin.init({ apiBase: '/api/cms' }),
grapesjsPlugin,
],
},
})Plugin Helpers
import {
defineTootixPlugin,
useTootixPlugin,
type TootixStudioPlugin,
type TootixStudioPluginContext,
type TootixStudioCommandContribution,
} from '@tooto/tootix-studio-sdk'defineTootixPlugin(plugin)preserves typed plugin options.plugin.init(options)binds host options to a plugin instance using the Studio SDK-style plugin configuration model.useTootixPlugin(plugin, options)remains available as a compatibility helper.- A plugin
idmust be unique. - Plugin
setup(ctx)is synchronous and returns contribution objects. - Business configuration belongs in plugin options. There is no global host context in v1.
Contribution Points
| Contribution | Purpose | Key fields |
| --- | --- | --- |
| panels | Add Vue panels to the primary sidebar, inspector sidebar, or modal region. | id, title, region, component, order |
| activityBar | Add a left activity bar item that activates a primary panel. | id, label, icon, panelId, placement, order |
| toolbar | Add toolbar buttons. | id, label, icon, placement, command, order |
| commands | Register SDK commands into editor.Commands. | id, run, stop, order |
| blocks | Register blocks after built-in blocks are created. | id, setup, order |
| runtimePlugins | Add runtime GrapesJS hooks after storage and canvas boot plugins. | id, setup, order |
icon values are passed to Iconify through @iconify/vue; use values such as lucide:file-stack or lucide:refresh-cw.
Ordering and Conflicts
The SDK registers plugins in this order:
- built-in Tootix Studio default plugin
- legacy
extensions - host
pluginsin array order
Contribution arrays are sorted by order, then by id. If order is omitted, the default order is 1000.
The SDK throws during initialization when duplicate ids are found for:
- plugin ids
- panel ids
- activity bar ids
- toolbar item ids
- command ids
- block ids
- runtime plugin ids
Commands
Commands are registered through GrapesJS editor.Commands.add.
commands: [
{
id: 'cms:publish',
async run({ editor, sender, options }) {
await publish(editor.getProjectData(), options)
},
stop({ editor }) {
editor.stopCommand('core:preview')
},
},
]A toolbar item with command: 'cms:publish' calls editor.runCommand('cms:publish').
Data Sources
The SDK includes an opt-in DataSource plugin. It is not enabled by default; add it to options.plugins when a project needs REST-backed preview data and field bindings.
import createTootixStudio, {
tootixDataSourcePlugin,
} from '@tooto/tootix-studio-sdk'
createTootixStudio({
root: '#studio',
options: {
project: { id: 'cms-home' },
plugins: [
tootixDataSourcePlugin.init({
sources: [
{
id: 'posts',
label: 'Posts',
url: '/api/posts',
recordsPath: 'items',
headers: {
Accept: 'application/json',
},
},
],
resolveRequestHeaders: () => ({
Authorization: `Bearer ${token}`,
}),
}),
],
},
})The first version supports GET JSON REST sources for editor preview. Supported payloads are raw arrays, { records, schema }, { data: [...] }, { items: [...] }, or any object with recordsPath pointing to an array. The plugin normalizes records, adds missing record ids, and infers GrapesJS data source schema fields.
Sensitive headers are never stored in project data. Authorization, Cookie, X-Api-Key, and similar credentials are stripped from saved source definitions. Use resolveRequestHeaders for runtime preview credentials and environment-managed credentials in your SSG publisher.
When enabled, the plugin adds:
- a
Data Sourcesactivity bar panel for REST configuration, loading, errors, and field previews Variable,Collection, andConditionblocks under theData Sourcesblock category- data binding buttons beside supported built-in traits: heading/paragraph content, button text/link, image source/alt/link, and embed source/title
Bindings use GrapesJS native data resolver props (dataResolver and component __data_values) and are saved in project data for downstream publishing. The SDK does not implement SSG output in this package; hosts can read project data and resolve those bindings during publish.
References:
Blocks
Use the Tootix block helpers for new host blocks. They register GrapesJS blocks and component types through a typed, declarative API:
import {
defineTootixBlock,
defineTootixComponentBlock,
defineTootixComponentType,
} from '@tooto/tootix-studio-sdk'
blocks: [
defineTootixComponentBlock({
id: 'cms-card',
type: 'cms-card',
label: 'CMS card',
category: 'CMS',
media: 'lucide:panel-top',
component: {
model: {
defaults: {
tagName: 'article',
attributes: { class: 'cms-card' },
components: '<h3>Card title</h3><p>Card body</p>',
},
},
},
}),
defineTootixBlock({
id: 'cms-hero',
label: 'Hero',
category: 'CMS',
media: 'lucide:layout-template',
content: '<section class="cms-hero"><h1>Hero title</h1></section>',
}),
defineTootixComponentType({
type: 'cms-only-type',
component: {
model: {
defaults: { tagName: 'div' },
},
},
}),
]Use the raw setup(editor) block contribution only when you need direct GrapesJS access:
blocks: [
{
id: 'cms-hero-blocks',
setup(editor) {
editor.Blocks.add('cms-hero', {
label: 'Hero',
category: 'CMS',
content: '<section class="hero"><h1>Hero title</h1></section>',
})
},
},
]Block Runtime Assets
Blocks that depend on canvas runtime libraries can declare assets. The SDK lazily injects those assets into the GrapesJS canvas iframe only when the current project contains the matching component type.
import {
defineTootixComponentBlock,
type TootixStudioRuntimeAsset,
} from '@tooto/tootix-studio-sdk'
const swiperAssets = [
{
id: 'swiper.css',
type: 'style',
href: '/vendor/swiper/swiper-bundle.min.css',
},
{
id: 'swiper.js',
type: 'script',
src: '/vendor/swiper/swiper-bundle.min.js',
global: 'Swiper',
},
] satisfies TootixStudioRuntimeAsset[]
blocks: [
defineTootixComponentBlock({
id: 'swiper-carousel',
type: 'tootix-swiper-carousel',
label: 'Swiper carousel',
category: 'Interactive',
media: 'lucide:gallery-horizontal',
assets: swiperAssets,
component: {
model: {
defaults: {
tagName: 'div',
attributes: { class: 'swiper' },
components: `
<div class="swiper-wrapper">
<div class="swiper-slide">Slide 1</div>
<div class="swiper-slide">Slide 2</div>
</div>
<div class="swiper-pagination"></div>
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
`,
loop: true,
slidesPerView: 1,
autoplayDelay: 0,
traits: [
{ type: 'checkbox', name: 'loop', label: 'Loop', changeProp: true },
{ type: 'number', name: 'slidesPerView', label: 'Slides', changeProp: true },
{ type: 'number', name: 'autoplayDelay', label: 'Autoplay delay', changeProp: true },
],
'script-props': ['loop', 'slidesPerView', 'autoplayDelay'],
script(props) {
const win = this.ownerDocument.defaultView
const Swiper = win?.Swiper
if (!Swiper) return
if (this.__tootixSwiper) {
this.__tootixSwiper.destroy(true, true)
}
this.__tootixSwiper = new Swiper(this, {
loop: Boolean(props.loop),
slidesPerView: Number(props.slidesPerView || 1),
autoplay: Number(props.autoplayDelay) > 0
? { delay: Number(props.autoplayDelay) }
: false,
pagination: { el: this.querySelector('.swiper-pagination') },
navigation: {
prevEl: this.querySelector('.swiper-button-prev'),
nextEl: this.querySelector('.swiper-button-next'),
},
})
},
},
},
},
}),
]For plain HTML blocks, assets are only included when assetUsage tells the SDK how to detect usage from saved project data:
defineTootixBlock({
id: 'gallery-html',
label: 'Gallery',
content: '<div data-gjs-type="gallery-runtime"></div>',
assets: swiperAssets,
assetUsage: {
componentTypes: ['gallery-runtime'],
},
})For publishing, collect the runtime assets used by the current project and insert them in the host page template:
import { collectTootixProjectAssets } from '@tooto/tootix-studio-sdk'
const editor = studio.getEditor()
const assets = editor ? collectTootixProjectAssets(editor) : { styles: [], scripts: [] }The SDK does not write asset tags into editor content. This keeps project data clean and lets the host control CDN paths, local vendor paths, cache policy, and final HTML ordering.
Runtime Hooks
Use runtimePlugins for editor events and GrapesJS runtime customization:
runtimePlugins: [
{
id: 'cms-runtime',
setup(editor) {
editor.on('component:selected', (component) => {
console.info('selected', component.getId())
})
},
},
]Native GrapesJS Plugins
Native GrapesJS plugin functions are supported directly:
import type { Plugin } from 'grapesjs'
const nativePlugin: Plugin = (editor) => {
editor.DomComponents.addType('cms-card', {
model: {
defaults: {
tagName: 'article',
},
},
})
}
createTootixStudio({
root: '#studio',
options: {
project: { id: 'demo' },
plugins: [nativePlugin],
},
})Native GrapesJS plugins are appended to grapesjs.init({ plugins }) after the SDK core plugin.
Legacy Extensions
extensions is retained for compatibility with older host code. New integrations should use plugins.
createTootixStudio({
root: '#studio',
extensions: [
{
id: 'legacy.cms',
panels: [],
activityBar: [],
toolbar: [],
commands: [],
blocks: [],
runtimePlugins: [],
},
],
})Some exported contribution types still use the historical WebBuilder* names. They are public types for compatibility.
Public Exports
import createTootixStudio, {
TootixStudio,
createApiStorage,
createIndexedDbStorage,
createStorageAdapter,
defaultStorage,
collectTootixProjectAssets,
defineTootixBlock,
defineTootixComponentBlock,
defineTootixComponentType,
defineTootixPlugin,
normalizeTootixPlugins,
resolveTootixStorage,
resolveTootixStudioConfig,
tootixDataSourcePlugin,
useTootixPlugin,
} from '@tooto/tootix-studio-sdk'
import type {
ApiAdapterConfig,
IndexedDbStorageOptions,
StorageAdapter,
StorageConfig,
ThemeMode,
ResolvedTootixStudioConfig,
TootixDataSourcePluginOptions,
TootixRestDataSourceInput,
TootixStudioAssetUsage,
TootixStudioBlockContribution,
TootixStudioBlockDefinition,
TootixStudioBlockRegistrar,
TootixStudioCollectedProjectAssets,
TootixStudioCommandContext,
TootixStudioCommandContribution,
TootixStudioConfig,
TootixStudioConfigInput,
TootixStudioComponentBlockDefinition,
TootixStudioComponentTypeDefinition,
TootixStudioGrapesPlugin,
TootixStudioIdentityOptions,
TootixStudioInstance,
TootixStudioOptions,
TootixStudioPlugin,
TootixStudioPluginContext,
TootixStudioPluginContribution,
TootixStudioPluginDefinition,
TootixStudioPluginEntry,
TootixStudioPluginInput,
TootixStudioPluginWithOptions,
TootixStudioProjectOptions,
TootixStudioRuntimeAsset,
TootixStudioRuntimeScriptAsset,
TootixStudioRuntimeStyleAsset,
TootixStudioStorageAdapter,
TootixStudioStorageInput,
WebBuilderActivityBarItem,
WebBuilderBlockContribution,
WebBuilderExtension,
WebBuilderPanelContribution,
WebBuilderRuntimePluginContribution,
WebBuilderToolbarContribution,
} from '@tooto/tootix-studio-sdk'Vue component entry:
import TootixStudio from '@tooto/tootix-studio-sdk/vue'Style entry:
import '@tooto/tootix-studio-sdk/style'Local Playground
Inside this repository, the playground demonstrates SDK usage and a host plugin:
pnpm --filter @tooto/tootix-playground devUseful verification commands:
pnpm --filter @tooto/tootix-studio-sdk test
pnpm --filter @tooto/tootix-studio-sdk type-check
pnpm --filter @tooto/tootix-studio-sdk build
pnpm --filter @tooto/tootix-playground type-check
pnpm --filter @tooto/tootix-playground buildCurrent Constraints
- v1 is designed around one active editor instance per page.
- Plugin setup is synchronous. Put async behavior inside commands or runtime event handlers.
- There is no plugin enable/disable lifecycle in v1.
- There is no plugin dependency graph in v1.
