labellife-design-tool
v1.3.9
Published
Professional canvas editor built with React, TypeScript, and Konva
Maintainers
Readme
LabelLife Design Tool
Professional canvas editor built with React, TypeScript, and Konva.js. A powerful design tool similar to Canva/Polotno for creating graphics, posters, and designs.
Features
- 🎨 Rich Canvas Editor - Full-featured design editor with text, images, shapes, and more
- 📐 Multi-page Support - Create designs with multiple pages
- 🖼️ Image Management - Upload images, use Unsplash, crop, mask, and apply filters
- ✏️ Text Editing - Rich text editing with fonts, colors, alignment, and styling
- 🔷 Shapes & Elements - Add various shapes (rectangles, circles, stars, polygons, etc.)
- 🎭 Image Masking - Apply masks to images (circle, star, triangle, etc.)
- 📤 Export Options - Export to PNG, JPG, or JSON
- 📥 Template Import - Import JSON templates with user input collection
- 🌐 i18n Support - Internationalization support (English, Dutch)
- 🧩 Customizable UI - Fully customizable navbar with support for custom sections
- ⚙️ Configurable - Customize features, panels, and behavior
Installation
npm install labellife-design-tool
# or
yarn add labellife-design-tool
# or
pnpm add labellife-design-toolUsage
Basic Usage
import { CanvasEditor } from 'labellife-design-tool';
import 'labellife-design-tool/styles'; // Import CSS
function App() {
return (
<CanvasEditor
name="My Design Editor"
config={{
export: {
png: true,
jpg: true,
json: true,
},
multiPage: true,
variables: true,
}}
/>
);
}Note on CSS: The CSS is pre-processed and ready to use. Simply import it - no Tailwind configuration needed!
Panel Configuration (Built-in + Custom)
Control which panels appear in the left sidebar and their order using a single list via config.panels. You can mix built-in panel ids and custom panel objects in one array.
Available Built-in Panels
| Panel ID | Description |
|---|---|
| "text" | Add and edit text elements |
| "elements" | Add shapes (rect, circle, star, polygon, etc.) |
| "image" | Upload images, use Unsplash, crop/mask/filter |
| "design" | Canvas size, presets, design settings |
| "background" | Page background color/image |
| "export" | Export to PNG/JPG/JSON (requires config.export) |
| "variables" | Template variables (requires config.variables) |
Show Only Selected Built-in Panels
Omit any panel id to hide it. The order you list them is the order they appear:
<CanvasEditor
name="Limited Editor"
config={{
panels: ["text", "elements", "image", "background"],
}}
/>This hides Design, Export, and Variables — only Text, Elements, Images, and Background are shown.
Add a Custom Panel
Create your own panel component and include it in the same list:
import { Sparkles } from "lucide-react";
// Your custom panel component — receives whatever props you pass
const MyAssetsPanel = ({ category }: { category: string }) => {
return (
<div style={{ padding: 16, color: "white" }}>
<h3>My Assets</h3>
<p>Showing assets for: {category}</p>
</div>
);
};
function App() {
return (
<CanvasEditor
name="Custom Panel Editor"
config={{
export: { png: true, jpg: true, json: true },
panels: [
"text",
"elements",
{
id: "my-assets",
title: "My Assets",
tooltip: "Browse my assets",
icon: <Sparkles className="w-5 h-5" />,
component: MyAssetsPanel,
props: { category: "stickers" },
},
"image",
"background",
"export",
],
}}
/>
);
}Custom Panel Definition
interface CustomPanelDefinition {
id: string; // Unique panel id
title: string; // Panel title
tooltip?: string; // Tooltip on hover (defaults to title)
icon: React.ReactElement; // Icon shown in the left sidebar
component: React.ComponentType<any>; // Your panel React component
props?: Record<string, any>; // Props passed to your component
actionType?: "setPanel" | "setToolAndPanel"; // Click behavior (default: "setPanel")
toolValue?: ToolType; // Tool to activate (if actionType is "setToolAndPanel")
}Add Elements from a Custom Panel
Option 1: Use standalone store (Polotno-like - recommended)
Create a standalone store that you can use anywhere:
import { createSimpleStore } from 'labellife-design-tool';
// Create store instance
const store = createSimpleStore({
name: "My Design",
width: 800,
height: 600,
});
// Use store anywhere
store.addPage();
// Add elements - both APIs work
store.addElement({
type: "image",
src: "https://example.com/img.jpg",
x: 100,
y: 100,
width: 200,
height: 200,
});
// Or use Polotno-like API
store.activePage.addElement({
type: "image",
src: "https://example.com/img.jpg",
x: 100,
y: 100,
width: 200,
height: 200,
});
// Resize canvas
store.setSize(1200, 800);
// Load templates
await store.loadJSON(templateData);
// Export to blob
const pngBlob = await store.toBlob('png');
const jpgBlob = await store.toBlob('jpg');
// Download the blob
const downloadBlob = (blob: Blob, filename: string) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
};
downloadBlob(pngBlob, 'design.png');
downloadBlob(jpgBlob, 'design.jpg');
// Use with CanvasEditor (optional)
<CanvasEditor
name="Editor"
store={store} // Pass the store (optional)
config={{ panels: ["text", "elements"] }}
/>
// Export store for use across your app
export const store = createSimpleStore();Backward Compatibility
If you don't pass a store prop, CanvasEditor will create one internally:
// This still works - creates internal store
<CanvasEditor name="Editor" config={{ panels: ["text", "elements"] }} />Option 2: Use the useCanvasStore hook (inside CanvasEditor only)
Any component inside CanvasEditor can access the store using the hook:
import { useCanvasStore } from 'labellife-design-tool';
const MyComponent = () => {
const store = useCanvasStore();
const handleAddImage = () => {
store.activePage.addElement({
type: "image",
src: "https://example.com/img.jpg",
x: 100,
y: 100,
width: 200,
height: 200,
});
};
const handleLoadTemplate = async () => {
try {
const templateData = {
name: "My Template",
width: 800,
height: 600,
pages: [
{
id: "page-1",
name: "Page 1",
elements: [
{
type: "text",
text: "Hello World",
x: 100,
y: 100,
fontSize: 24,
},
],
},
],
};
await store.loadJSON(templateData);
console.log("Template loaded successfully!");
} catch (error) {
console.error("Failed to load template:", error);
}
};
return (
<div style={{ color: "white", padding: 16 }}>
<p>Current page: {store.activePage.id}</p>
<p>Canvas size: {store.width} × {store.height}</p>
<button onClick={handleAddImage}>Add Image</button>
<button onClick={handleLoadTemplate}>Load Template</button>
</div>
);
};Option 2: Panel prop approach
Every panel (built-in or custom) also receives a store prop with a Polotno-like API:
// Your custom panel component
const MyImagesPanel = ({ store }) => {
const handleAddImage = (url: string) => {
store.activePage?.addElement({
type: "image",
src: url,
x: 100,
y: 100,
width: 200,
height: 200,
});
};
return (
<div style={{ color: "white", padding: 16 }}>
<h3>My Images</h3>
<button onClick={() => handleAddImage("https://example.com/img.jpg")}>
Add Image
</button>
<p>Canvas size: {store.width} × {store.height}</p>
</div>
);
};
// Register the panel
<CanvasEditor
config={{
panels: [
"text",
"elements",
{ id: "my-images", title: "My Images", icon: <MyIcon />, component: MyImagesPanel },
"image",
"background",
],
}}
/>SimpleStore interface (Standalone)
interface SimpleStore {
// Design properties
design: CanvasDesign;
width: number;
height: number;
// Page operations
activePage: (Page & { addElement: (element: Partial<CanvasElement>) => void }) | null;
activePageId: string;
addPage(page?: Partial<Page>): string;
deletePages(pageIds: string[]): void;
setActivePage(pageId: string): void;
// Element operations
addElement(element: Partial<CanvasElement>): void;
updateElement(elementId: string, updates: Partial<CanvasElement>): void;
deleteElement(elementId: string): void;
// Panel operations
openSidePanel(panelId: string | null): void;
activePanelId: string | null;
// Design operations
setDesign(design: CanvasDesign): void;
updateDesign(updates: Partial<CanvasDesign>): void;
setSize(width: number, height: number): void;
loadJSON(jsonData: any): Promise<void>;
// Export operations
toBlob(type?: 'png' | 'jpg'): Promise<Blob>;
// Events
on(event: 'designChanged' | 'activePageChanged' | 'activePanelChanged', callback: Function): void;
off(event: string): void;
}Open panels programmatically
const MyPanel = ({ store }) => {
const openTextPanel = () => store.openSidePanel("text");
const openCustomPanel = () => store.openSidePanel("my-custom-panel");
const closeAllPanels = () => store.openSidePanel(null);
const tryOpenUnknown = () => store.openSidePanel("nonexistent"); // closes all panels
const deletePage1 = () => store.deletePages(["page-1"]);
const deleteMultiplePages = () => store.deletePages(["page-1", "page-2", "page-3"]);
return (
<div style={{ color: "white", padding: 16 }}>
<p>Current page ID: {store.activePage.id}</p>
<p>Canvas size: {store.width} × {store.height}</p>
<button onClick={openTextPanel}>Open Text Panel</button>
<button onClick={openCustomPanel}>Open Custom Panel</button>
<button onClick={closeAllPanels}>Close All Panels</button>
<button onClick={tryOpenUnknown}>Try Unknown Panel (closes all)</button>
<button onClick={deletePage1}>Delete Page 1</button>
<button onClick={deleteMultiplePages}>Delete Multiple Pages</button>
</div>
);
};Default Behavior
If config.panels is not provided, the editor shows the default set:
text,elements,image,design,background- Plus
variablesifconfig.variablesis set - Plus
exportifconfig.exportis set
Configure Unsplash API Key
import { CanvasEditor, setUnsplashAccessKey } from 'labellife-design-tool';
// Set Unsplash API key programmatically
setUnsplashAccessKey('your-unsplash-access-key');
// Or use environment variable
// UNSPLASH_ACCESS_KEY=your-key npm startUsing Refs with CanvasEditor
The CanvasEditor component supports React refs, allowing you to access internal methods and properties:
import { useRef } from 'react';
import { CanvasEditor, CanvasEditorRef } from 'labellife-design-tool';
function MyEditor() {
const editorRef = useRef<CanvasEditorRef>(null);
const handleExport = () => {
// Access methods via the ref
if (editorRef.current) {
// Export to PNG
editorRef.current.exportToPNG();
// Or get the current design
const currentDesign = editorRef.current.getDesign();
console.log('Current design:', currentDesign);
// Access the Konva Stage directly
const stage = editorRef.current.stage;
// Do something with the stage...
}
};
return (
<div>
<CanvasEditor
ref={editorRef}
name="Editor with Ref"
config={{
export: { png: true, jpg: true, json: true },
multiPage: true
}}
/>
<button onClick={handleExport}>Export</button>
</div>
);
}The CanvasEditorRef interface provides the following properties and methods:
stage: Direct access to the Konva.Stage instanceexportToPNG(): Export the canvas to a PNG fileexportToJPG(): Export the canvas to a JPG fileexportToJSON(): Export the design to a JSON filegetDesign(): Get the current design objectsetCanvasSize(width, height): Set the canvas dimensions programmaticallyloadDesign(jsonData): Load a design from JSON data (Promise-based)
Export Helpers (JSON object + Base64)
In addition to the UI download helpers, the library also provides functions that return data directly (no file download):
import { exportToJSONObject, canvasToDataURL } from 'labellife-design-tool';Get a JSON-safe design object
const designObject = exportToJSONObject(editorRef.current.getDesign());
// designObject is a plain JSON-compatible object you can send to an APIGet a base64 data URL of the canvas
const stage = editorRef.current.stage;
if (stage) {
const dataUrl = canvasToDataURL(stage, 'png', { pixelRatio: 2 });
// dataUrl is like: "data:image/png;base64,iVBORw0..."
}Canvas Size Management
The setCanvasSize method allows you to programmatically change the canvas dimensions. This is useful for responsive designs, template switching, or custom size inputs.
import { useRef } from 'react';
import { CanvasEditor, CanvasEditorRef } from 'labellife-design-tool';
function CanvasSizeExample() {
const editorRef = useRef<CanvasEditorRef>(null);
const handleSizeChange = (width: number, height: number) => {
if (editorRef.current) {
// Set canvas to custom dimensions
editorRef.current.setCanvasSize(width, height);
}
};
const presetSizes = [
{ name: 'Instagram Post', width: 1080, height: 1080 },
{ name: 'Instagram Story', width: 1080, height: 1920 },
{ name: 'Facebook Post', width: 1200, height: 630 },
{ name: 'YouTube Thumbnail', width: 1280, height: 720 },
];
return (
<div>
<CanvasEditor
ref={editorRef}
name="Resizable Canvas"
config={{
export: { png: true, jpg: true, json: true },
}}
/>
<div className="size-controls">
<h3>Canvas Size Controls</h3>
{/* Preset sizes */}
<div>
<h4>Preset Sizes:</h4>
{presetSizes.map((preset) => (
<button
key={preset.name}
onClick={() => handleSizeChange(preset.width, preset.height)}
>
{preset.name} ({preset.width}x{preset.height})
</button>
))}
</div>
{/* Custom size input */}
<div>
<h4>Custom Size:</h4>
<input
type="number"
placeholder="Width"
id="custom-width"
onChange={(e) => {
const width = parseInt(e.target.value);
const height = parseInt((document.getElementById('custom-height') as HTMLInputElement)?.value || '600');
if (width > 0) handleSizeChange(width, height);
}}
/>
<input
type="number"
placeholder="Height"
id="custom-height"
onChange={(e) => {
const height = parseInt(e.target.value);
const width = parseInt((document.getElementById('custom-width') as HTMLInputElement)?.value || '800');
if (height > 0) handleSizeChange(width, height);
}}
/>
</div>
</div>
</div>
);
}Function Signature
setCanvasSize(width: number, height: number): voidParameters:
width(number): The desired canvas width in pixels. Minimum value is 1.height(number): The desired canvas height in pixels. Minimum value is 1.
Behavior:
- Updates the canvas dimensions immediately
- Ensures minimum dimensions of 1x1 pixel to prevent invalid canvas sizes
- The change is persisted in the design state and included in exports
- Elements on the canvas maintain their relative positions
- The canvas is automatically re-rendered with the new dimensions
Use Cases:
- Template switching with different aspect ratios
- Responsive design tools
- Custom size input forms
- Batch resizing operations
- Integration with external size selection tools
Advanced Usage with Types
import {
CanvasEditor,
CanvasDesign,
Config,
exportToPNG,
exportToJPG,
exportToJSON,
canvasToBlob,
importFromJSON,
importFromJSONData
} from 'labellife-design-tool';
import type { CanvasElement } from 'labellife-design-tool';
const config: Config = {
export: { png: true, jpg: true, json: true },
multiPage: true,
variables: true,
};
function MyEditor() {
return <CanvasEditor name="Editor" config={config} />;
}Template Import
The library provides two ways to import template designs:
File-Based Import
import { importFromJSON } from 'labellife-design-tool';
// Use with file input
function handleFileImport(event) {
importFromJSON(
event,
(design) => {
console.log('Design loaded:', design);
// Use the design
},
(error) => {
alert('Import failed: ' + error);
}
);
}Direct JSON Data Import
import { importFromJSONData } from 'labellife-design-tool';
// Import JSON data directly (no file needed)
const templateData = {
width: 800,
height: 600,
pages: [{
id: "1",
name: "Page 1",
elements: [
{
type: "text",
text: "Hello World",
x: 100,
y: 100,
fontSize: 24
}
],
background: "white"
}]
};
importFromJSONData(
templateData,
(design) => {
console.log('Design loaded:', design);
// Use the design in CanvasEditor
},
(error) => {
console.error('Import failed:', error);
}
);Import from API
// Load template from API
async function loadTemplateFromAPI(templateId: string) {
try {
const response = await fetch(`/api/templates/${templateId}`);
const templateData = await response.json();
importFromJSONData(
templateData,
(design) => {
// Template loaded successfully
setDesign(design);
},
(error) => {
alert('Failed to load template: ' + error);
}
);
} catch (error) {
console.error('API error:', error);
}
}Import with User Inputs
importFromJSONData(
templateData,
(design) => {
// Design loaded with user inputs applied
setDesign(design);
},
(error) => {
alert('Import failed: ' + error);
},
(inputs, onComplete) => {
// Show custom input modal
showInputModal(inputs, (values) => {
onComplete(values);
});
}
);Comparison:
| Feature | importFromJSON | importFromJSONData |
|---------|------------------|---------------------|
| Input Source | File upload event | JSON data object |
| File Reading | Built-in FileReader | Not needed |
| Use Case | User file uploads | API data, database, programmatic imports |
| Processing | Identical | Identical |
Simplified Template Loading (Recommended)
For the easiest template loading experience, we recommend using the new loadDesign method or loadTemplateFromJSON utility. These methods handle all the complexity internally and provide a clean, promise-based API.
Method 1: Using loadDesign with CanvasEditor Ref
This is the most straightforward approach - just pass the JSON data directly to the editor:
import { useRef } from 'react';
import { CanvasEditor, CanvasEditorRef } from 'labellife-design-tool';
function TemplateLoader() {
const canvasEditorRef = useRef<CanvasEditorRef>(null);
const loadTemplate = async (templateData: any) => {
try {
await canvasEditorRef.current?.loadDesign(templateData);
console.log('Template loaded successfully!');
} catch (error) {
console.error('Failed to load template:', error);
alert('Failed to load template: ' + error.message);
}
};
// Example: Load template from API
const loadTemplateFromAPI = async (templateId: string) => {
try {
const response = await fetch(`/api/templates/${templateId}`);
const templateData = await response.json();
await canvasEditorRef.current?.loadDesign(templateData);
console.log('Template loaded successfully!');
} catch (error) {
console.error('API error:', error);
alert('Failed to load template: ' + error.message);
}
};
return (
<div>
<CanvasEditor ref={canvasEditorRef} name="Template Editor" />
<button onClick={() => loadTemplateFromAPI('123')}>
Load Template
</button>
</div>
);
}Method 2: Using loadTemplateFromJSON Utility
This provides a convenient wrapper with additional error checking:
import { useRef } from 'react';
import { CanvasEditor, CanvasEditorRef, loadTemplateFromJSON } from 'labellife-design-tool';
function TemplateLoader() {
const canvasEditorRef = useRef<CanvasEditorRef>(null);
const loadTemplate = async (templateData: any) => {
try {
await loadTemplateFromJSON(canvasEditorRef, templateData);
console.log('Template loaded successfully!');
} catch (error) {
console.error('Failed to load template:', error);
}
};
return <CanvasEditor ref={canvasEditorRef} name="Template Editor" />;
}Your Colleague's Use Case
Here's how your colleague can now load templates with the simplified API:
const loadTemplate = async (id) => {
try {
const response = await fetch(`${window.wpDesignData.baseUrl}/api/polotno/get-template/${id}/`, {
headers: { 'Authorization': `Bearer ${window.wpDesignData.userToken}` }
});
const templateData = await response.json();
if (templateData) {
const detailFileJson = templateData.details_file;
const responseFile = await fetch(detailFileJson);
const templateDataJson = await responseFile.json();
// Simple one-line call - no callbacks needed!
await canvasEditorRef.current.loadDesign(templateDataJson);
console.log('Template loaded successfully');
}
} catch (error) {
console.error('Failed to load template:', error);
alert('Failed to load template: ' + error.message);
}
};Key Benefits:
- No callbacks required - uses promises for cleaner async handling
- Automatic error handling - throws descriptive errors
- Internal state management - handles
setDesignautomatically - History integration - automatically saves to undo/redo history
- Modal integration - uses existing TemplateInputModal for user inputs
- TypeScript support - full type safety
What is canvasEditorRef?
canvasEditorRef is a React ref object that provides access to the CanvasEditor component's internal methods. Here's how to create and use it:
import { useRef } from 'react';
import { CanvasEditor, CanvasEditorRef } from 'labellife-design-tool';
function MyComponent() {
// 1. Create the ref with proper typing
const canvasEditorRef = useRef<CanvasEditorRef>(null);
// 2. Pass it to the CanvasEditor component
return (
<CanvasEditor
ref={canvasEditorRef}
name="My Editor"
config={{ /* your config */ }}
/>
);
}Important Notes:
- The ref is
nulluntil the component mounts - Always check
canvasEditorRef.currentbefore using it - The ref provides access to methods like
loadDesign,exportToPNG, etc. - TypeScript provides full autocomplete and type checking
Function Signature
loadDesign(jsonData: any): Promise<void>Parameters:
jsonData(any): The raw JSON template data to load
Returns:
Promise<void>: Resolves when the design is loaded, rejects on error
Behavior:
- Converts template format to internal CanvasDesign format
- Shows the existing TemplateInputModal for required user inputs
- Automatically handles optional inputs with empty values
- Updates the editor state
- Saves to undo/redo history
- Throws descriptive errors for invalid data or user cancellation
- Supports both required and optional user input fields
Canvas to Blob Export
The library provides a flexible canvasToBlob function that allows converting the canvas to a Blob for various use cases:
import { canvasToBlob } from 'labellife-design-tool';
// Example usage in a custom component
const handleExport = async () => {
if (stageRef.current) {
try {
// Get canvas as PNG blob
const blob = await canvasToBlob(stageRef.current, 'png', { pixelRatio: 2 });
// Example: Convert to base64
const reader = new FileReader();
reader.readAsDataURL(blob);
reader.onloadend = () => {
const base64data = reader.result;
console.log('Base64:', base64data);
// Use base64 data with APIs, embed in other applications, etc.
};
// Example: Create File object for upload
const file = new File([blob], 'canvas-export.png', { type: 'image/png' });
// Example: Create object URL for display
const blobUrl = URL.createObjectURL(blob);
// Remember to revoke URL when done
// URL.revokeObjectURL(blobUrl);
} catch (error) {
console.error('Error exporting canvas:', error);
}
}
};The canvasToBlob function parameters:
stage: The Konva.Stage object to convertformat: Output format ('png' or 'jpg')options: Additional options objectquality: Quality for JPG format (0-1)pixelRatio: Resolution multiplier (default: 2)
Navbar Customization
You can fully customize the navbar by configuring the navbar property in the config object:
import { CanvasEditor } from 'labellife-design-tool';
function App() {
return (
<CanvasEditor
name="My Design Tool"
config={{
// Other configuration options...
navbar: {
// Control visibility of default sections
showAppName: true, // Show/hide the app name on the left
showHistoryControls: true, // Show/hide undo/redo controls
showZoomControls: true, // Show/hide zoom controls
// Override default sections
defaultSections: {
appName: {
label: "My Custom Tool Name", // Change the default app name
}
},
// Add custom sections to the navbar
customSections: [
{
id: "saveButton",
type: "custom",
position: "right", // 'left', 'center', or 'right'
label: "Save",
icon: "💾", // Optional icon
onClick: () => saveProject(),
order: 5 // Controls the ordering of sections (lower numbers come first)
},
{
id: "helpLink",
type: "custom",
position: "right",
label: "Help",
icon: "❓",
onClick: () => showHelpModal(),
order: 40
},
// For complex custom sections, you can provide a React component
{
id: "customDropdown",
type: "custom",
position: "right",
content: <CustomDropdownComponent />,
order: 50
}
]
}
}}
/>
);
}WordPress Integration
The library includes a special WordPress-compatible entry point that ensures proper compatibility with WordPress's React environment.
Using with WordPress
When using this library in a WordPress environment, import from the WordPress-specific entry point:
// Import from the WordPress-specific entry point
import { CanvasEditor } from 'labellife-design-tool/wordpress';
import 'labellife-design-tool/styles'; // Import CSS
// Use as normal
function MyWordPressComponent() {
return (
<CanvasEditor
name="WordPress Design Editor"
config={{
export: { png: true, jpg: true, json: true },
multiPage: true
}}
/>
);
}WordPress Helper Function
For simpler integration with vanilla WordPress plugins, a helper function is provided:
import { initWordPressCanvasEditor } from 'labellife-design-tool/wordpress';
import 'labellife-design-tool/styles'; // Import CSS
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
const containerId = 'editor-container'; // ID of your container element
initWordPressCanvasEditor(containerId, {
name: "WordPress Editor",
config: {
export: { png: true, jpg: true, json: true },
multiPage: true,
variables: true
}
});
});How the WordPress Compatibility Works
The WordPress integration automatically handles compatibility with WordPress's React environment by:
- Detecting when React is available globally (as in WordPress)
- Adding necessary JSX runtime compatibility functions
- Making the library work seamlessly in WordPress's React environment
Project Structure
src/lib/index.ts- Library entry point (npm package)src/wordpress.tsx- WordPress-specific entry pointsrc/CanvasEditor.tsx- Main editor componentsrc/types/- TypeScript type definitionssrc/components/- React componentssrc/panels/- Sidebar panelssrc/elements/- Canvas element componentssrc/utils/- Utility functions
License
MIT
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
