Overview

Open the panel from the left sidebar by switching the left-sidebar status to Blocks (the internal key for the component / blocks tab — see LeftSidebarKeys.Blocks in types/editor.type.ts). The sidebar then renders leftsidebar/Components.tsx, a flat list of 15 component categories. Selecting a category opens a portal-rendered side flyout next to the sidebar with the available variants for that category — each variant shows a thumbnail, title, author and price (free or subscription).

Behind the catalogue sits the Blocks library — leftsidebar/Blocks.tsx — which is the same surface used by the Dev Portal and Store Editor for re-usable blocks. The two cooperate: the Component panel is the curated, marketplace-style entry point; Blocks is the raw library of every page, component, widget, group, form, dialog and library block currently available in the theme or app.

How it works

Blocks are loaded from two sources and merged in leftsidebar/Blocks.tsx. Default / built-in blocks ship in common/json/components/*.json and are eagerly imported via Vite's import.meta.glob — each gets isLocal: true so it can co-exist with API blocks that happen to share a block_id. API blocks are fetched per project: Dev Portal calls apiCall.devApps.getGlobalBlocks(app_id, app_version_code); Store Editor calls apiCall.store.getStoreThemePages(store_theme_version_code) and then hydrates each 'page' block in parallel with getStoreBlockData so the full BaseNode[] tree and jsCode are ready before the user drags or clicks.

Block identity lives on the useCommmerceEditor store via the Block slice (store/slices/createBlockSlice.ts). It holds block_id, block_version_id, block_version_code, block_name, block_slug, block_version_slug, block_image, block_type and block_version_settings for the currently-open block, plus editedBlocks / dirtyBlockKeys for tracking edited global blocks for a batch save. The setter setBlockData is what seeds the editor when a block is loaded — it propagates dialog metadata to the root child, resets history, and clears unsaved-changes state.

Dropping a block onto the canvas runs the import pipeline in handleBlockClick / handleDragStart: updateUniqueIdsAndClasses regenerates every node's id and CSS class (root sections keep the commmerce_ prefix, grid cells keep the grid-cell-item marker, all other tags use a tag-based prefix from getClassPrefixForTag); transformImportedBlock maps the block's source breakpoints onto the theme's target breakpoints (with a BreakpointMappingDialog popping up if they're incompatible); and finally addElementToSection, replaceSection or setChildren is called depending on the block type. jsCode from the block is appended to the page's JavaScript via setJsCode.

Blocks installed from the App marketplace carry source_app_id / source_app_version_id. The original install path is the backend's installAppAsBlock orchestrator, which materializes an app's published root block into a new store_block row. After install, if the source app publishes a newer version, getStoreThemePages sets has_update: true on the block and the panel shows an up-arrow icon — clicking it calls apiCall.store.updateBlockFromApp(block_id, { new_app_version_id }), which re-shapes the latest app content for the block's type (page or component) and overwrites block_data in place. See backend/.../store-editor/store.controller.ts updateBlockFromApp for the full flow.

Key features

  • Curated component categories — the Component panel (leftsidebar/Components.tsx) lists 15 categories: Headers, Banner, Product Details Pages, Text Slider, Flashbars, Hero Sections, Blogs, Testimonials, Video Slots, Footers, Hotspots, Product Slider, Product Grid, Category Slider, Category Grid. Selecting a category opens a portal-rendered flyout pinned to the right edge of the sidebar. Use case: browse the full catalogue without leaving the editor — open Headers to compare three header variants side-by-side without committing to one.
  • Variant browser — each category exposes one or more variants with thumbnail, title, author and price. Click outside the list or the flyout to dismiss; the flyout also re-positions on window resize. Use case: evaluate "Mega Menu Header" against "Minimal Header" with live thumbnails before committing.
  • Default + API mergeBlocks.tsx merges json/components/*.json (loaded via import.meta.glob, marked isLocal) with the API response. A source filter at the top of the panel toggles between All / Default / Saved. Use case: hide your custom blocks while showcasing defaults to a client, or filter to "Saved" when looking for a block you made earlier today.
  • Eight block typespage, component, widget, group, form, dialog, library, unknown, displayed in that order (blockTypeOrder in Blocks.tsx). Use case: Pages always show first so merchants can replace the entire canvas first, then layer components and widgets on top.
  • Click-to-insert and drag-to-insert — clicking a block with a target element selected calls handleBlockClick; dragging onto the canvas calls handleDragStart and the drop handler. Both regenerate ids and transform breakpoints. Use case: click when you have a precise insertion point selected; drag when you want to choose a coordinate on the canvas visually.
  • Page vs section vs nested behaviour — a page block replaces every section on the canvas (via setChildren); a component block replaces the target root section (via replaceSection) or fills the parent with absolute positioning when dropped into a nested element; any other block is appended into the target via addElementToSection. Use case: a Page block acts as a template swap, a Component block acts as a section swap, and a Widget/Group acts as a nested insert — pick the type that matches your intent.
  • Layout-boundary constraint — when inserting into a root section, the block's width is capped by getEffectiveContainerBoundary so it never exceeds the contained-mode content width for the active device, and a max-width in rem is added for contained layouts. Use case: drop a wide image-grid block into a contained section and have it clamp to the section's content area instead of breaking into the gutter.
  • Breakpoint mapping — incompatible source breakpoints open the BreakpointMappingDialog; the user's choice is persisted per block in confirmedImportOptions so the next drag of the same block re-uses the mapping. Use case: install a block from an app that defines its own breakpoints — map them to the theme once, then drag the same block to a second page without seeing the dialog again.
  • Per-block JavaScriptjsCode attached to a block is concatenated onto the page's jsCode via setJsCode, so the block's behaviour ships with it. Use case: a slider block ships its own carousel JS — merchants get a working slider without separately copy-pasting the script.
  • Update-from-app — API blocks carrying a source_app_id show an up-arrow when has_update: true. Confirming the Popconfirm calls updateBlockFromApp, which fetches the target app version's published block_data, reshapes it for the block's type, and uploads the new block_data to GCS without creating a new block version. Use case: an app developer ships a bug fix to their installed block; merchants click the up-arrow once and the block's content updates in place.
  • Delete API blocks — non-local blocks expose a trash icon that calls devApps.deleteBlock in Dev Portal or store.deleteStoreBlock in Store Editor. Use case: retire an obsolete saved block (e.g. "Old Promo Banner") without leaving the editor.
  • Dialog-block awareness — when block_type === 'dialog', setBlockData propagates the type and block_version_slug down to the root child so the parser can detect dialog metadata (data-dialog-* attrs). Use case: creates a clean separation between modal sections and in-flow sections in the Layers panel and SSR output.
  • Incompatible-breakpoint warning — rows surface an AlertTriangle tooltip when the block's block_version_settings.breakpoints don't match the theme's targetBreakpoints. Use case: spot blocks that will require mapping before clicking, so you know to schedule a mapping pass.

Component panel — category catalog

The 15 category labels exposed by COMPONENT_ITEMS in leftsidebar/Components.tsx. Each category opens a flyout with curated variants drawn from COMPONENT_VARIANTS (placeholder mock data in source — replaced by API content when wired up at runtime).

CategoryTypical block_typeInsert behaviourUse case
Headerscomponent / globalreplaceSection on the header sectionSwap navigation styles (Mega Menu vs Minimal vs Centered Logo) without re-cutting markup.
BannercomponentreplaceSection or addElementToSectionDrop a hero banner with imagery, copy and CTA — pre-styled for the active theme.
Product Details PagespagesetChildren — replaces every sectionSwitch the entire PDP layout (Classic / Gallery / Sticky CTA) as a template swap.
Text Slidercomponent / widgetaddElementToSectionAdd a marquee or quote slider — ships its own carousel jsCode.
FlashbarscomponentaddElementToSection at top of pagePromo or countdown bar above the header — common for "Free shipping" or "Sale ends in" messaging.
Hero SectionscomponentreplaceSection on hero sectionDrop the page's primary above-the-fold section — Split, Full Bleed, or Video variants.
BlogscomponentaddElementToSectionBlog index grid or editorial list — pulls blog content from the store via the data-source web component.
Testimonialscomponent / widgetaddElementToSectionQuote cards or avatar carousel — adds social proof above conversion CTAs.
Video SlotscomponentaddElementToSectionEmbed a hero video or video grid — useful for product demos and brand storytelling.
Footerscomponent / globalreplaceSection on footer sectionSwap footer styles (Mega / Minimal / Newsletter) — typically a global block so changes propagate everywhere.
HotspotscomponentaddElementToSectionInteractive product or lookbook hotspots — pin a "+" marker over a fashion shot that opens a product card on click.
Product SlidercomponentaddElementToSectionDrop a carousel of featured / bestseller / new-arrival products on the homepage.
Product GridcomponentaddElementToSectionDrop a 2/3/4-column product grid — wired to the data-source web component for live products.
Category SlidercomponentaddElementToSectionRound or card-style category navigation slider for browsing entry points.
Category GridcomponentaddElementToSection2x2 or masonry category grid — typically used as a homepage section above featured products.

Install behaviour matrix (block_type)

The block_type on every block determines what happens when it is clicked or dropped. The matrix below is taken from handleBlockClick and handleBlockClickWithTransform in Blocks.tsx.

TypeScopeStore actionUse case
pageEntire canvassetChildren(newChildren) + setJsCode(block.jsCode)Template swap — replace the entire page with the saved page block; merchant uses this to switch PDP/category page layouts.
componentTarget root sectionreplaceSection(rootChildId, blockNode)Section swap — replace the targeted root section (e.g. swap the homepage hero variant) while leaving other sections intact.
component dropped into nested elementNested filladdElementToSection with position: absolute; inset: 0; width: 100%; height: 100% + parent gets position: relativeWrap a nested element with a component block that fills it edge-to-edge — useful for overlay components.
widgetTarget element (append)addElementToSection(sectionId, blockNode, parentId)Append a small functional widget (e.g. a star-rating widget) inside an existing element.
groupTarget element (append)addElementToSection(sectionId, blockNode, parentId)Drop a grouped set of elements that should travel together (e.g. a feature-card group).
formTarget element (append)addElementToSection(sectionId, blockNode, parentId)Drop a pre-styled form block — ships with submit handlers and success message templates.
dialogTarget element (append)addElementToSection — root child carries block_type='dialog' + block_version_slugAdd a modal dialog block — the parser picks up data-dialog-* attrs so the SSR output knows it is a modal, not in-flow.
libraryTarget element (append)addElementToSection(sectionId, blockNode, parentId)Drop a re-usable design-library piece (icon, badge, decoration) that lives in the theme's block library.
unknownTarget element (append)addElementToSection(sectionId, blockNode, parentId)Fallback for any block whose block_type wasn't set — same append behaviour as group/widget.

Block identity fields

Every block carries the identity fields below. They are preserved on insertion by updateUniqueIdsAndClasses, propagate through to the saved BaseNode tree, and are read by Layers (for the Global / Dialog badges) and the right-side panels (for the gear-icon Block Settings dialog).

FieldTypeDefaultDescription / use case
block_idnumber | nullnullStable id of the block in store_blocks_tb or the dev portal equivalent — used for save/delete API calls.
block_version_idnumber | nullnullId of the specific block version currently loaded into the editor — supports rollback to a prior version.
block_version_codenumber | nullnullMonotonic version code used when calling saveSettings / getStoreBlockData — increments each time the block is republished.
block_type'page' | 'component' | 'widget' | 'group' | 'form' | 'dialog' | 'library' | nullnullDetermines insert behaviour (see Install matrix) and which settings UI applies.
block_namestring | nullnullHuman-friendly label shown in the Layers tree and the Blocks list — set this well so Layers reads cleanly.
block_slugstring | nullnullURL-safe slug used for de-duping local vs API blocks — local blocks with the same slug are silently skipped.
block_version_slugstring | nullnullVersion-specific slug propagated onto dialog roots so the parser can detect them via data-dialog-*.
block_imagestring | nullnullOptional preview image URL used by external surfaces (e.g. App Store, theme marketplace).
block_version_settingsobject | nullnullPer-block settings bag — seo, headerSdk, footerSdk, and breakpoints for blocks that ship custom breakpoint configs. Edit via the gear-icon dialog on pages.
is_globalbooleanfalseMarks a section that originated from a global block; surfaces the Global badge in Layers and enables Edit Block.
source_app_idnumber | nullnullSet when a block was installed from a marketplace app; enables the Update-from-app flow.
source_app_version_idnumber | nullnullThe exact app version the block was installed from; has_update compares this to latest_available_version_id.
has_updatebooleanfalseSet by getStoreThemePages when a newer published app version is available; surfaces the up-arrow icon for the merchant to opt in.
isLocalboolean (panel-only)falseSet on blocks loaded from json/components/*.json so the panel can distinguish them from API blocks (no delete button, separate de-dupe).

Per-block settings dialog

Page-type blocks expose a gear-icon dialog (block-settings/BlockSettingsDialog.tsx) with two tabs. The settings shape is defined in block-settings/types.ts and saved through useBlockSettings.saveSettings to GCS-backed block_version_settings.

TabStored underField shapeUse case
SEOblock_version_settings.seodefaultTitle, prefixTitle, suffixTitle, defaultKeywords, defaultMetaImage, defaultDescriptionSet page-level meta — title prefix/suffix for site-wide branding, OG image for share previews, keywords for the SEO engine.
Library / SDK — Headerblock_version_settings.headerSdk[]string[] (raw script tags / URLs / link tags)Inject scripts into <head> for analytics, fonts, or meta tags — strings are emitted verbatim by SSR.
Library / SDK — Footerblock_version_settings.footerSdk[]string[] (raw script tags / URLs / link tags)Inject scripts before </body> for tracking pixels or late-loading scripts that should not block render.

Non-page block types display a "Block settings coming soon." placeholder — their settings surface lives in the right-sidebar dialog block settings (for dialog blocks) and the Layers Edit Block action (for component / library blocks).

Workflows

1. Drop a hero section onto the page

  1. Switch the left sidebar to Blocks (Components tab).
  2. Click Hero Sections in the category list — the variant flyout opens to the right.
  3. Drag a variant card onto the canvas. handleDragStart writes the block's serialized BaseNode + identity to dataTransfer.
  4. On drop, ids are regenerated by updateUniqueIdsAndClasses and the block is added via addElementToSection (or replaceSection if it's a component block targeting a root section).

2. Replace the whole page with a saved page block

  1. Open the Blocks panel and filter by Saved.
  2. Click a block under the page group.
  3. handleBlockClick calls saveToHistory(), then setChildren(newChildren) with the freshly id-regenerated children — every existing section on the canvas is replaced.
  4. jsCode from the page block becomes the new page jsCode (via setJsCode); the action is fully undo-able from history.

3. Install an app block then update it later

  1. Install a marketplace app — the backend runs installAppAsBlock to materialize its published root block into a store_block row carrying source_app_id / source_app_version_id.
  2. The block now appears in the Blocks panel; drag or click it to insert.
  3. When the source app publishes a newer version, the next getStoreThemePages call sets has_update: true on the row and the panel renders an up-arrow icon next to the block name.
  4. Click the icon, confirm the Popconfirm — updateBlockFromApp fetches the new app content, reshapes it for the block's type, and overwrites block_data in place; the editor reloads the block list.

4. Drop a block with a mismatched breakpoint setup

  1. Pick a block whose block_version_settings.breakpoints differ from the theme's.
  2. Click it — areBreakpointsCompatible rejects, so the BreakpointMappingDialog opens.
  3. Choose how each source breakpoint maps onto a target breakpoint; confirm.
  4. createImportContext builds the mapping, transformImportedBlock rewrites every responsive style, and the block is inserted with theme-aligned breakpoints. The mapping is cached in confirmedImportOptions for the next drag of the same block.

5. Set page-level SEO via Block Settings

  1. Open the gear icon on a page-type block (in PageSelector or via the Layers row's Edit Block action).
  2. The dialog opens (BlockSettingsDialog); pick the SEO tab.
  3. Fill in title, description, keywords (Antd tags input), OG image; the search preview re-renders live.
  4. Save — useBlockSettings writes the merged BlockSettingsData back to block_version_settings via the project's save endpoint, and setBlockVersionSettings propagates the new value into the editor store.

6. Replace the header section with a curated variant

  1. Open the Component panel and click Headers.
  2. Pick a variant (e.g. "Mega Menu Header") from the flyout — its block_type is component.
  3. The header section is automatically targeted; replaceSection(rootChildId, blockNode) swaps it without touching any other section.
  4. Any jsCode attached to the header (e.g. dropdown logic) is concatenated to the page's existing jsCode via setJsCode.

Tips & gotchas

  • Default blocks loaded from json/components/ always appear first in each type group — Blocks.tsx prepends them after the API merge. To override a default with a saved version, save it with the same block_slug but a different block_id; the local de-dupe check is by block_slug only.
  • Clicking a non-page block when no canvas element is selected does nothing — the panel logs "No target node information available for non-page block". Select an element first (the Selection Info panel must show a node), or drag the block onto the canvas instead.
  • The commmerce_ class prefix is reserved for root sections. updateUniqueIdsAndClasses remaps div / section on non-root nodes to container_ via getClassPrefixForTag, so don't expect a nested commmerce_ id after insertion.
  • setBlockData resets history (history: [], historyIndex: -1) — anything previously on the canvas is gone from the undo stack once a block is loaded as the active block. Use this only when you intentionally swap to a different block context.
  • The Update from app Popconfirm warns that the action overwrites your local edits. There is no diff step — if the merchant has hand-edited the block, those edits are lost on update. Treat it as a hard re-install.
  • Header / Footer SDK entries in BlockLibrarySdkTab are stored as raw strings and injected verbatim by SSR. They can be full <script> tags, plain URLs (https://...), or <link> tags — the renderer does not sanitise them.
  • Drag-and-drop does NOT show the BreakpointMappingDialog (it can't pause a native drag operation). Transformation runs automatically on drop with default mapping; to customise the mapping, click the block first (requires a canvas selection) and the dialog will open.
  • Dialog blocks propagate block_type='dialog' and block_version_slug down to the root child in setBlockData; the parser uses this to emit data-dialog-* attrs in SSR. Don't strip those metadata fields when manually editing BaseNode trees.
  • Local blocks from json/components/ ALWAYS appear regardless of block_id collisions with API blocks — the de-dupe inside the local set uses block_slug. To prevent a local block from showing, remove its JSON file or change its block_slug.