SeatSquirrel
DeploymentSelf-Hosted Mode

Designer API Reference

Complete API reference for the SeatSquirrel Designer

Constructor Options

new SeatSquirrel.Designer(options)

Shared Callbacks (Both Modes)

These options work in both standalone and iframe SDK modes.

OptionTypeRequiredDescription
permissionsDesignerPermissionsNoRestrict which features are enabled in the designer. Default: all features enabled.
onReady() => voidNoCalled when designer is ready.
onLayoutLoaded() => voidNoCalled when loadLayout() succeeds. Pure confirmation signal — no payload.
onSave(data: { layout: LayoutOutput }) => voidNoCalled when user clicks Save.
onLayoutChanged(data: { hasChanges: boolean }) => voidNoCalled when unsaved changes status changes.
onPricingCategoriesChanged(data: { hasChanges: boolean }) => voidNoCalled when pricing changes; call getPricingCategories() for data.
onError(error: { message: string, code: string, details?: string }) => voidNoCalled when an error occurs. See Error Codes below.
ui{ fullscreenButton?: boolean }NoUI configuration. ui.fullscreenButton: false hides the built-in fullscreen toggle. Default: true. You can still trigger fullscreen programmatically via setFullscreen() when the button is hidden.
onFullscreenChange(isFullscreen: boolean) => voidNoCalled whenever the designer enters or exits viewport-fill fullscreen.
onRequestSectionContent(sectionId: string) => SectionContent | Promise<SectionContent>NoSections only. Lazily provide one Section's contents when the user drills in to edit it. Required to edit sectioned layouts — without it, drilling into a section loaded from the parent layout is blocked (its host-stored contents are never wiped).
onSaveSectionContent(sectionId: string, content: SectionContent) => void | Promise<void>NoSections only. Persist one Section's edited contents to your per-section store. See when it fires below.
onDeleteSectionContent(sectionId: string) => void | Promise<void>NoSections only. Remove a deleted Section's content blob from your per-section store. Called on save for sections the user deleted. Without it, deleted sections leave orphaned blobs in your store.
onSectionChange(activeSectionId: string | null) => voidNoSections only. Fires when the user drills into a Section (its id) or back to the parent (null).

Desktop only — The Designer requires a mouse or trackpad. On touch-only devices (phones, tablets without an external pointer), a "Desktop Required" screen is shown and onError fires with code MOBILE_NOT_SUPPORTED. The onReady callback will not fire and the API will not be available. Connecting an external mouse or trackpad to a tablet will allow the designer to initialize. Narrow screens (below 768px) are also blocked regardless of input device.

iFrame SDK Only

These options only apply when using the iframe SDK. They are not available in standalone mode.

OptionTypeRequiredDescription
containerstring | HTMLElementYesCSS selector or DOM element for the iframe
baseUrlstringNoLocation of the SeatSquirrel app (default: https://seatsquirrel.com)
designerUrlstringNoFull URL override for the designer page. Takes precedence over baseUrl. Useful for servers requiring .html extensions (e.g., https://example.com/designer.html)

Restricting Designer Features

Use the permissions option to control which tools and features are available in the designer.

Disable pricing category management

When you supply pricing categories from your external system, you typically want to prevent users from creating or modifying them:

Standalone mode (window.SeatSquirrelConfig):

window.SeatSquirrelConfig = {
  permissions: {
    properties: {
      pricingCategoriesCrud: false,
      settingsPanels: { currency: false },
    },
  },
  onReady: function () {
    SeatSquirrel.designer.setPricingCategories({
      mode: 'replace',
      pricingCategories: [
        { id: 'vip', label: 'VIP', slug: 'vip', price: 15000, color: '#8b5cf6' },
        { id: 'standard', label: 'Standard', slug: 'standard', price: 5000, color: '#3b82f6' },
      ],
    });
  },
  onSave: function (layout) {
    console.log('Layout saved:', layout);
  },
};

iFrame SDK mode (new SeatSquirrel.Designer()):

const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: {
    properties: {
      pricingCategoriesCrud: false,
      settingsPanels: { currency: false },
    },
  },
  onReady: () => {
    designer.setPricingCategories({
      mode: 'replace',
      pricingCategories: [
        { id: 'vip', label: 'VIP', slug: 'vip', price: 15000, color: '#8b5cf6' },
        { id: 'standard', label: 'Standard', slug: 'standard', price: 5000, color: '#3b82f6' },
      ],
    });
  },
});

Hide individual settings panels

The Settings tab contains five panels: Pricing Categories, Background, Display, Alignment, and Currency. Use properties.settingsPanels to hide any of them individually. Omitted panels stay visible, so you only list the ones you want hidden. When every panel is hidden, the Settings tab itself is hidden.

const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: {
    properties: {
      settingsPanels: {
        background: false,
        display: false,
        alignment: false,
        // pricingCategories and currency omitted → still visible
      },
    },
  },
});

Note: the Currency panel is shown only when settingsPanels.currency is not false. Editing within the Pricing Categories panel is still governed separately by pricingCategoriesCrud.

Read-only preview mode

// Standalone
window.SeatSquirrelConfig = {
  permissions: { readOnly: true },
};

// iFrame SDK
const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: { readOnly: true },
});

Assignment-only mode

Allows pricing category assignment while blocking all structural and visual edits:

  • Pricing category assignment remains enabled
  • Creating, moving, resizing, and deleting objects is disabled
  • Undo/redo, copy/paste, and destructive controls are hidden
  • Settings tab and category CRUD are disabled
  • Save and export remain available
  • Selected elements show greyed-out (non-editable) properties below the pricing editor
  • If both readOnly and assignmentOnly are true, readOnly takes precedence
// Standalone
window.SeatSquirrelConfig = {
  permissions: { assignmentOnly: true },
};

// iFrame SDK
const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: { assignmentOnly: true },
});

// Show greyed-out structural properties alongside the pricing panel for context
window.SeatSquirrelConfig = {
  permissions: {
    assignmentOnly: true,
    properties: { showProperties: true },
  },
};

Limit available tools

// Only allow rows and areas (hide tables, shapes, text, sections)

// Standalone
window.SeatSquirrelConfig = {
  permissions: {
    tools: {
      table: false,
      shape: false,
      text: false,
      traceImage: false,
      section: false,
    },
  },
};

// iFrame SDK
const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: {
    tools: {
      table: false,
      shape: false,
      text: false,
      traceImage: false,
      section: false,
    },
  },
});

Hide the Settings tab

// Standalone
window.SeatSquirrelConfig = {
  permissions: {
    properties: {
      settingsTab: false,
    },
  },
};

// iFrame SDK
const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: {
    properties: {
      settingsTab: false,
    },
  },
});

Suppress pricing category panel warning

Hide the amber "Missing Pricing Categories" warning box shown in the properties panel when nothing is selected:

// Standalone
window.SeatSquirrelConfig = {
  permissions: {
    warnings: {
      pricingCategoriesAssignmentPanelWarning: false,
    },
  },
};

// iFrame SDK
const designer = new SeatSquirrel.Designer({
  container: '#designer',
  permissions: {
    warnings: {
      pricingCategoriesAssignmentPanelWarning: false,
    },
  },
});

Methods

Summary

| Method | Parameters | Returns | | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | loadLayout(layoutData) | layoutData: LayoutOutput | Standalone: { success: boolean }
iFrame: Promise<{ success: boolean }> | | getLayout({ mode }) | { mode: 'current' \| 'last-saved' } | Standalone: { layout: LayoutOutput \| null }
iFrame: Promise<{ layout: LayoutOutput \| null }> | | hasChanges() | — | { hasChanges: boolean } | | setPermissions(permissions) | DesignerPermissions | { success: boolean } | | clearLayout() | — | { success: boolean } | | setPricingCategories({ mode, ... }) | { mode, pricingCategories } | Standalone: { success: boolean }
iFrame: Promise<{ success: boolean }> | | getPricingCategories() | — | Standalone: { pricingCategories: PricingCategory[] }
iFrame: Promise<{ pricingCategories: PricingCategory[] }> | | assignPricingCategories(input) | AssignPricingCategoriesInput | Standalone: { success, errors? }
iFrame: Promise<{ success, errors? }> | | showToast(options) | ShowToastOptions | Standalone: { success: boolean }
iFrame: Promise<{ actionClicked: boolean }> | | showAlertDialog(options) | ShowAlertDialogOptions | { success: boolean } | | showNotice(options) | ShowNoticeOptions | { success: boolean, noticeId: string } | | removeNotice(noticeId) | string | { success: boolean } | | isDesignerReady() | — | { designerReady: boolean } | | setFullscreen(value) | boolean | { success: boolean } | | destroy() | — | void | iFrame SDK only |

loadLayout(layoutData)

Description

Load an existing layout into the designer for editing.

Parameters

  • layoutData (LayoutOutput): The layout JSON from a previous export

Older layout exports that used layout.sections for label groups and sectionId on rows/areas/tables are accepted and normalized to the current layout.types/typeId format during load.

Returns

{ success: boolean } — In standalone mode this is synchronous. In iframe SDK mode it returns a Promise<{ success: boolean }>.

Example

// Standalone mode (synchronous)
const { success } = SeatSquirrel.designer.loadLayout(savedLayout);

// Iframe SDK mode (async)
const { success } = await designer.loadLayout(savedLayout);
console.log('Layout loaded:', success);

getLayout({ mode })

Description

Get layout data from the designer. The mode parameter controls which snapshot is returned:

  • 'current' — Returns the real-time layout state including any unsaved changes.
  • 'last-saved' — Returns the last saved or loaded layout snapshot (not real-time). Returns null if no layout has been saved or loaded.

Note: getLayout({ mode: 'current' }) does not trigger the onSave callback. onSave only fires when the user clicks the Save button in the UI.

Parameters

  • options (Object):
PropertyTypeRequiredDescription
mode'current' | 'last-saved'YesWhich layout snapshot to return

Returns

{ layout: LayoutOutput | null } — In standalone mode this is synchronous. In iframe SDK mode it returns a Promise<{ layout: LayoutOutput | null }>. When mode is 'current', layout is always present. When mode is 'last-saved', layout may be null.

Example

// Standalone mode (synchronous)
const { layout } = SeatSquirrel.designer.getLayout({ mode: 'current' });

// Iframe SDK mode (async)
const { layout } = await designer.getLayout({ mode: 'current' });
console.log(layout);

// Get the last saved snapshot
const { layout: savedLayout } = designer.getLayout({ mode: 'last-saved' });
if (savedLayout) {
  console.log('Last saved:', savedLayout);
}

setPermissions(permissions)

Description

Set designer permissions at runtime. Use this to dynamically enable/disable features such as read-only mode, tool restrictions, or properties panel controls.

This method goes through the same permissions system as the constructor permissions option, so all permission fields are supported.

Parameters

Returns

{ success: boolean }

Example

// Enable read-only mode
designer.setPermissions({ readOnly: true });

// Disable read-only mode
designer.setPermissions({ readOnly: false });

// Enable assignment-only mode (pricing assignment only)
designer.setPermissions({ assignmentOnly: true });

// Disable specific tools at runtime
designer.setPermissions({ tools: { row: false, table: false } });

// Lock down properties panel
designer.setPermissions({
  properties: { pricingCategoriesCrud: false, settingsPanels: { currency: false } },
});

clearLayout()

Description

Clear the current layout and start with a blank canvas.

Returns

{ success: boolean }

Example

designer.clearLayout();

setPricingCategories({ mode, ... })

Description

Set or update pricing categories in the designer. The mode parameter controls how the categories are applied:

  • 'replace' — Replace all pricing categories with the provided array. Marks them as "externally managed" — users can only modify colors in the designer UI.
  • 'update-by-id' — Partially update existing categories by ID. Merges provided fields into matching categories while leaving unmentioned categories untouched. All-or-nothing: if any ID doesn't match an existing category, the entire call fails with no changes applied.
  • 'update-by-slug' — Partially update existing categories by slug. Merges provided fields into matching categories while leaving unmentioned categories untouched. All-or-nothing: if any slug doesn't match an existing category, the entire call fails with no changes applied.

Note: When using 'replace' mode, the designer UI restricts users to only modifying category colors. They cannot add, delete, or change pricing details.

Note: When using 'update-by-slug' mode, the slug field is used only for matching — it is not merged into the category. To change a category's slug, use 'update-by-id' mode and pass slug as an update field.

Parameters

  • options (Object):
PropertyTypeRequiredDescription
mode'replace' | 'update-by-id' | 'update-by-slug'YesHow to apply the pricing categories
pricingCategoriesArrayYesArray of pricing category objects (shape depends on mode)

Category Object Properties (mode: 'replace'):

PropertyTypeRequiredDescription
idstringYesUnique identifier for the category
labelstringYesDisplay label (e.g., "VIP", "Standard")
slugstringYesURL-friendly identifier
pricenumberYesPrice in cents (e.g., 15000 for $150)
colorstringYesHex color code (e.g., "#8b5cf6")
customerNotesstringNoOptional notes visible to customers

Update Object Properties (mode: 'update-by-id'):

PropertyTypeRequiredDescription
idstringYesID of the category to update (must match an existing category)
labelstringNoNew display label
slugstringNoNew slug
pricenumberNoNew price in cents
colorstringNoNew hex color code
customerNotesstringNoNew customer notes

Update Object Properties (mode: 'update-by-slug'):

PropertyTypeRequiredDescription
slugstringYesSlug of the category to match (used for lookup only, not merged)
labelstringNoNew display label
pricenumberNoNew price in cents
colorstringNoNew hex color code
customerNotesstringNoNew customer notes

Returns

{ success: boolean } — In standalone mode this is synchronous. In iframe SDK mode it returns a Promise<{ success: boolean }>.

Example

// Replace all categories (standalone, synchronous)
const result = SeatSquirrel.designer.setPricingCategories({
  mode: 'replace',
  pricingCategories: [
    { id: 'vip', label: 'VIP', slug: 'vip', price: 15000, color: '#8b5cf6' },
    { id: 'standard', label: 'Standard', slug: 'standard', price: 5000, color: '#3b82f6' },
  ],
});
console.log(result.success); // true

// Replace all categories (iframe SDK, async)
const result = await designer.setPricingCategories({
  mode: 'replace',
  pricingCategories: [
    { id: 'vip', label: 'VIP', slug: 'vip', price: 15000, color: '#8b5cf6' },
    { id: 'standard', label: 'Standard', slug: 'standard', price: 5000, color: '#3b82f6' },
    { id: 'economy', label: 'Economy', slug: 'economy', price: 2500, color: '#10b981' },
  ],
});
console.log('Pricing set:', result.success);

// Partially update by ID
const result = SeatSquirrel.designer.setPricingCategories({
  mode: 'update-by-id',
  pricingCategories: [
    { id: 'price_abc123', price: 7500, label: 'Standard (updated)' },
    { id: 'price_def456', color: '#10B981' },
  ],
});

// Partially update by slug
const result = await designer.setPricingCategories({
  mode: 'update-by-slug',
  pricingCategories: [
    { slug: 'standard', price: 7500, label: 'Standard (updated)' },
    { slug: 'vip', color: '#10B981' },
  ],
});

getPricingCategories()

Description

Get the current pricing categories from the designer.

Returns

{ pricingCategories: PricingCategory[] } — In standalone mode this is synchronous. In iframe SDK mode it returns a Promise<{ pricingCategories: PricingCategory[] }>.

Example

// Standalone mode (synchronous)
const { pricingCategories } = SeatSquirrel.designer.getPricingCategories();

// Iframe SDK mode (async)
const { pricingCategories } = await designer.getPricingCategories();
console.log(pricingCategories);

assignPricingCategories(input)

Description

Assign pricing categories to layout objects (rows, seats, areas, tables, table seats) programmatically. This is useful when pricing categories are managed externally and need to be mapped to layout objects after loading.

Atomic semantics: If any assignment fails validation, none are applied. All errors are returned in the errors array.

The mode parameter controls how objects and categories are identified:

  • 'by-id' — Reference objects and categories by their internal UUIDs.
  • 'by-slug' — Reference objects and categories by their user-defined slugs. This is the recommended mode.
  • 'dangerously-by-labels' — Reference objects by human-readable labels (can be ambiguous across sections). Categories are always referenced by slug in this mode.

Parameters

PropertyTypeRequiredDescription
mode'by-id' | 'by-slug' | 'dangerously-by-labels'YesHow to identify objects and categories

Additional properties for mode 'by-id' and 'by-slug':

Each property is a Record<key, categoryKeys[]> map (keys are IDs or slugs depending on mode).

PropertyTypeRequiredDescription
rowsRecord<string, string[]>NoRow assignments
rowSeatsRecord<string, string[]>NoSeat assignments
areasRecord<string, string[]>NoArea assignments
tablesRecord<string, string[]>NoTable assignments
tableSeatsRecord<string, string[]>NoTable seat assignments

Additional properties for mode 'dangerously-by-labels': Each property is an array of label-based entries with pricingCategorySlugs.

Returns

AssignPricingCategoriesResult { success: boolean, errors?: string[] } — In standalone mode this is synchronous. In iframe SDK mode it returns a Promise.

Example

// Assign by slug (recommended)
const result = SeatSquirrel.designer.assignPricingCategories({
  mode: 'by-slug',
  rows: {
    'row-a': ['vip', 'standard'],
    'row-b': ['standard'],
  },
  areas: {
    'vip-lounge': ['vip'],
  },
});

// Assign by ID
const result = await designer.assignPricingCategories({
  mode: 'by-id',
  rows: {
    'row-uuid-1': ['cat-uuid-1', 'cat-uuid-2'],
    'row-uuid-2': ['cat-uuid-2'],
  },
});

if (!result.success) {
  console.error('Assignment errors:', result.errors);
}

showToast(options)

Description

Show a toast notification inside the designer UI.

Parameters

  • options (Object):
PropertyTypeRequiredDescription
titlestringYesMain toast message
variantstringNo'info', 'warning', 'error', or 'success' (default: 'info')
descriptionstringNoSecondary description text
actionObjectNoAction button: { label: string, onClick?: function }

Returns

  • Standalone mode: { success: boolean }
  • iFrame SDK mode: Promise<{ actionClicked: boolean }> — resolves when the toast is dismissed or the action button is clicked

Example

designer.showToast({ title: 'Layout saved successfully', variant: 'success' });

// Per-toast action callback (works in both modes)
const { actionClicked } = await designer.showToast({
  title: 'External changes detected',
  variant: 'info',
  description: 'Pricing was updated in your ticketing system.',
  action: {
    label: 'Reload pricing',
    onClick: function () {
      fetchLatestPricing().then(categories =>
        designer.setPricingCategories({ mode: 'replace', pricingCategories: categories })
      );
    },
  },
});
console.log('User clicked action:', actionClicked);

Action handling by mode:

  • Standalone modeaction.onClick runs directly when the button is clicked.
  • iFrame SDK modeaction.onClick is called via correlation IDs when the action button is clicked. The returned Promise resolves with { actionClicked: true } if the action was clicked, or { actionClicked: false } if the toast was dismissed.

showAlertDialog(options)

Description

Show a modal alert dialog inside the designer UI. The dialog blocks interaction and requires the user to explicitly confirm or cancel.

Note: Only one alert dialog can be shown at a time. Calling showAlertDialog() while a dialog is open cancels the previous one. In both modes, onConfirm and onCancel work the same way. In iframe mode, the SDK matches callbacks via internal correlation IDs transparently.

Parameters

  • options (Object):
PropertyTypeRequiredDescription
titlestringYesDialog title text
descriptionstringNoDialog body/description text
confirmTextstringNoConfirm button label (default: 'OK')
cancelTextstringNoCancel button label (default: 'Cancel')
variantstringNo'info', 'warning', 'error', or 'success' — each with distinct icon and color (default: 'info')
modestringNo'alert' (OK button only) or 'confirm' (OK/Cancel buttons) (default: 'alert')
onConfirmfunctionNoCalled when the user clicks the confirm button
onCancelfunctionNoCalled when the user clicks the cancel button

Returns

{ success: boolean }

Example

designer.showAlertDialog({
  title: 'Unsaved changes',
  description: 'You have unsaved changes that will be lost. Continue?',
  variant: 'error',
  confirmText: 'Discard changes',
  cancelText: 'Go back',
  onConfirm: function () {
    navigateAway();
  },
});

designer.showAlertDialog({
  title: 'Layout published',
  description: 'Your layout is now live and available for booking.',
  mode: 'alert',
});

showNotice(options)

Description

Show a persistent, non-blocking notice banner in the designer UI. Notices remain visible until dismissed by the user or removed via removeNotice(). One notice per position — calling showNotice() with the same position replaces the existing notice.

Designer limitation: Only 'stage-top' is supported. Cart positions ('cart-top', 'cart-before-proceed') are picker-only and will return { success: false }.

Parameters

ParameterTypeRequiredDefaultDescription
options.positionstringYesOnly 'stage-top' in Designer
options.variantstringNo'info''info', 'warning', 'error', or 'success'
options.titlestringYesMain notice message
options.descriptionstringNoOptional secondary text
options.dismissablebooleanNotrueWhether the user can dismiss via X button
options.actionobjectNo{ label: string, onClick?: () => void } — optional action button
options.onDismissfunctionNoCalled when user dismisses (not called on API removal or replacement)

Returns

{ success: boolean, noticeId: string }noticeId can be passed to removeNotice().

Example

const { noticeId } = designer.showNotice({
  position: 'stage-top',
  variant: 'info',
  title: 'Auto-save enabled',
  description: 'Changes are saved automatically every 30 seconds.',
});

// Later, remove it
designer.removeNotice(noticeId);

removeNotice(noticeId)

Description

Remove a notice by its ID. The notice's onDismiss callback is not called when removed via API.

Parameters

ParameterTypeRequiredDescription
noticeIdstringYesThe noticeId returned by showNotice()

Returns

{ success: boolean }true if the notice was found and removed.


hasChanges()

Description

Check if there are unsaved changes.

Returns

{ hasChanges: boolean } — Whether there are unsaved changes


isDesignerReady()

Description

Check if the designer is ready to receive commands.

Returns

{ designerReady: boolean } — Whether the designer is ready


setFullscreen(value)

Description

Expand the designer to fill the browser viewport, or restore it. This drives the same state as the built-in fullscreen button and respects the ui.fullscreenButton option — calling it imperatively still works when the button is hidden.

This is viewport-fill fullscreen, not the browser Fullscreen API: the designer covers the browser window via CSS, browser chrome stays visible, and no permissions chain is required. Pressing Escape always exits.

In iframe SDK mode, the SDK resizes the iframe element on the host page and locks document.body scroll until exit. In standalone mode, the page root is expanded directly.

Parameters

  • value (boolean): true to enter fullscreen, false to exit

Returns

{ success: boolean }

Example

document.getElementById('my-fullscreen-button').addEventListener('click', () => {
  designer.setFullscreen(true);
});

destroy()

iFrame SDK only. Not available in standalone mode — standalone cleanup is handled automatically by the page lifecycle.

Description

Destroy the designer instance. Removes the iframe from the DOM, detaches the PostMessage event listener, and clears all internal state (pending requests, cached layouts, callback maps). Call this when you no longer need the designer — for example, when a modal closes or the user navigates away in a SPA.

Returns

void

Example

designer.destroy();

Error Codes

The onError callback receives an object with a code string. Known codes:

CodeDescription
MOBILE_NOT_SUPPORTEDThe device is touch-only (no mouse/trackpad) or the viewport is too narrow (below 768px). The designer requires a pointer device. onReady will not fire and the API will not be exposed.
NO_DATAloadLayout() was called without providing layout data.

Sections (drill-in layouts)

A Section is a frame on the canvas holding its own nested sub-layout (rows, areas, tables). Parent-view sections show a double-click hint, including after content has been added. Designers double-click a section to drill in and edit it, then drill back out by pressing Escape or clicking the empty canvas outside the section frame. This keeps large venues (think 100k-seat stadiums) responsive: the layout you pass to / get back from the designer carries only lightweight section shells (frame, thumbnail, seat counts), and each section's full contents are streamed lazily to and from your per-section store. There is no SeatSquirrel database in self-hosted mode, so you own that store.

Three callbacks wire editing of sectioned layouts (both standalone and iframe):

  • onRequestSectionContent(sectionId) — return that section's contents on drill-in. May be async. Return empty content ({ rows: [], rowSeats: [] }) for a section your store doesn't have yet; throw/reject to abort the drill-in. Without this handler, drilling into a section loaded from the parent layout is blocked so its host-stored contents can never be silently wiped. If you return rows, each row must have at least one matching entry in rowSeats. (Sections drawn during the current session are still editable — their contents start in memory.)
  • onSaveSectionContent(sectionId, content) — persist a section's edited contents. May be async; reject to surface a save failure (it's logged, never fatal).
  • onDeleteSectionContent(sectionId) — remove a deleted section's content blob. Called on save for sections the user deleted. May be async. Without it, deleted sections leave orphaned blobs in your store.
  • onSectionChange(activeSectionId) — optional; fires on drill-in (the id) or back out (null).

When contents are persisted

onSaveSectionContent fires in two situations, and onDeleteSectionContent fires once, so by the time you store the parent layout your per-section store is consistent with it:

  • On drill-out — when the user leaves a section (Escape, a click outside the section frame, or Save/getLayout while drilled in), that section's current contents are saved immediately.
  • On Save / getLayout({ mode: 'current' }) — any section changed at the parent scope without a drill-out is flushed too. This covers newly drawn sections (a section box that absorbed existing rows) and moved sections (dragging a section frame translates its contents). Deleted sections are removed via onDeleteSectionContent at this point — deferred to save so it stays atomic with the parent layout that drops the shell.

Drawing a section that absorbs seats and then saving without ever drilling in still persists those seats — the flush-on-save covers it.

Format versioning: section content saved by the designer includes a formatVersion field. Store it opaquely with the rest of the content and return it unchanged from onRequestSectionContent — it tells the designer/picker which serialized format the content uses so older content can be upgraded automatically on load. Content returned without a formatVersion is treated as legacy and run through all format migrations (correct for anything saved before the field existed). If you hand-author section content, include the current version to opt out of that legacy conversion.

How persistence splits

WhatWhere it livesHow you get / set it
Parent layout (section shells only — frames, thumbnails, seat counts)Your layout storeonSave / getLayout returns it; loadLayout takes it
Each section's full contents (rows/areas/tables)Your per-section storeonSaveSectionContent writes; onRequestSectionContent reads

The parent layout returned by onSave / getLayout does not include section contents — only the refreshed shells. Persist the two stores together, keyed by section id, and feed them back the same way on load.

Example (standalone)

const sectionStore = new Map(); // sectionId -> SectionContent (your backend in real use)

window.SeatSquirrelConfig = {
  onRequestSectionContent: sectionId => sectionStore.get(sectionId) ?? { rows: [], rowSeats: [] },

  onSaveSectionContent: (sectionId, content) => {
    sectionStore.set(sectionId, content);
  },

  onDeleteSectionContent: sectionId => {
    sectionStore.delete(sectionId);
  },

  onSave: ({ layout }) => {
    // `layout` carries section shells only — store it as your parent layout.
    saveParentLayout(layout);
  },

  onReady: () => SeatSquirrel.designer.loadLayout(parentLayout),
};

The iframe SDK exposes the same onRequestSectionContent / onSaveSectionContent / onDeleteSectionContent / onSectionChange options on new SeatSquirrel.Designer({...}); contents flow over PostMessage, and a request that gets no reply within ~15s rejects (clearing the loading overlay / surfacing a save or delete error).

Next Steps

On this page