Commmerce Editor
Component
The Component panel is the editor's catalogue of drop-in, pre-styled building blocks — headers, banners, hero sections, product grids, sliders, hotspots, footers, and more. Click a category to fan out its variants and drop one onto the canvas; the editor regenerates its ids, transforms its breakpoints, and inserts it into the selected section or, for page-level blocks, replaces the current page entirely.
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 merge —
Blocks.tsxmergesjson/components/*.json(loaded viaimport.meta.glob, markedisLocal) with the API response. A source filter at the top of the panel toggles betweenAll/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 types —
page,component,widget,group,form,dialog,library,unknown, displayed in that order (blockTypeOrderinBlocks.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 callshandleDragStartand 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
pageblock replaces every section on the canvas (viasetChildren); acomponentblock replaces the target root section (viareplaceSection) or fills the parent with absolute positioning when dropped into a nested element; any other block is appended into the target viaaddElementToSection. 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
getEffectiveContainerBoundaryso it never exceeds the contained-mode content width for the active device, and amax-widthinremis 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 inconfirmedImportOptionsso 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 JavaScript —
jsCodeattached to a block is concatenated onto the page'sjsCodeviasetJsCode, 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_idshow an up-arrow whenhas_update: true. Confirming the Popconfirm callsupdateBlockFromApp, which fetches the target app version's publishedblock_data, reshapes it for the block's type, and uploads the newblock_datato 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.deleteBlockin Dev Portal orstore.deleteStoreBlockin 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',setBlockDatapropagates the type andblock_version_slugdown 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
AlertTriangletooltip when the block'sblock_version_settings.breakpointsdon't match the theme'stargetBreakpoints. 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).
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.
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).
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.
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
- Switch the left sidebar to
Blocks(Components tab). - Click Hero Sections in the category list — the variant flyout opens to the right.
- Drag a variant card onto the canvas.
handleDragStartwrites the block's serializedBaseNode+ identity todataTransfer. - On drop, ids are regenerated by
updateUniqueIdsAndClassesand the block is added viaaddElementToSection(orreplaceSectionif it's acomponentblock targeting a root section).
2. Replace the whole page with a saved page block
- Open the Blocks panel and filter by
Saved. - Click a block under the page group.
handleBlockClickcallssaveToHistory(), thensetChildren(newChildren)with the freshly id-regenerated children — every existing section on the canvas is replaced.jsCodefrom the page block becomes the new pagejsCode(viasetJsCode); the action is fully undo-able from history.
3. Install an app block then update it later
- Install a marketplace app — the backend runs
installAppAsBlockto materialize its published root block into astore_blockrow carryingsource_app_id/source_app_version_id. - The block now appears in the Blocks panel; drag or click it to insert.
- When the source app publishes a newer version, the next
getStoreThemePagescall setshas_update: trueon the row and the panel renders an up-arrow icon next to the block name. - Click the icon, confirm the Popconfirm —
updateBlockFromAppfetches the new app content, reshapes it for the block's type, and overwritesblock_datain place; the editor reloads the block list.
4. Drop a block with a mismatched breakpoint setup
- Pick a block whose
block_version_settings.breakpointsdiffer from the theme's. - Click it —
areBreakpointsCompatiblerejects, so theBreakpointMappingDialogopens. - Choose how each source breakpoint maps onto a target breakpoint; confirm.
createImportContextbuilds the mapping,transformImportedBlockrewrites every responsive style, and the block is inserted with theme-aligned breakpoints. The mapping is cached inconfirmedImportOptionsfor the next drag of the same block.
5. Set page-level SEO via Block Settings
- Open the gear icon on a
page-type block (in PageSelector or via the Layers row's Edit Block action). - The dialog opens (
BlockSettingsDialog); pick the SEO tab. - Fill in title, description, keywords (Antd tags input), OG image; the search preview re-renders live.
- Save —
useBlockSettingswrites the mergedBlockSettingsDataback toblock_version_settingsvia the project's save endpoint, andsetBlockVersionSettingspropagates the new value into the editor store.
6. Replace the header section with a curated variant
- Open the Component panel and click Headers.
- Pick a variant (e.g. "Mega Menu Header") from the flyout — its
block_typeiscomponent. - The header section is automatically targeted;
replaceSection(rootChildId, blockNode)swaps it without touching any other section. - Any
jsCodeattached to the header (e.g. dropdown logic) is concatenated to the page's existingjsCodeviasetJsCode.
Tips & gotchas
- Default blocks loaded from
json/components/always appear first in each type group —Blocks.tsxprepends them after the API merge. To override a default with a saved version, save it with the sameblock_slugbut a differentblock_id; the local de-dupe check is byblock_slugonly. - 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.updateUniqueIdsAndClassesremapsdiv/sectionon non-root nodes tocontainer_viagetClassPrefixForTag, so don't expect a nestedcommmerce_id after insertion. setBlockDataresets 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 appPopconfirm 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
BlockLibrarySdkTabare 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'andblock_version_slugdown to the root child insetBlockData; the parser uses this to emitdata-dialog-*attrs in SSR. Don't strip those metadata fields when manually editingBaseNodetrees. - Local blocks from
json/components/ALWAYS appear regardless ofblock_idcollisions with API blocks — the de-dupe inside the local set usesblock_slug. To prevent a local block from showing, remove its JSON file or change itsblock_slug.