@turnix-co/konva-editor
v4.0.2
Published
Turnix Konva Editor - A powerful multi-slide canvas editor with drawing, media, and recording capabilities
Downloads
488
Maintainers
Readme
@turnix-co/konva-editor
A powerful, private multi-slide canvas editor built with React, Konva, and Redux. Features drawing tools, media support, recording capabilities, and more.
🔒 Private Package
This package is private and only available to authorized Turnix team members via GitHub Packages.
📦 Installation
For Team Members
Step 1: Install the package from GitHub Packages:
npm install @turnix-co/konva-editor --registry=https://npm.pkg.github.com --legacy-peer-depsStep 2: Install peer dependencies:
npm install konva react-konva react-konva-utilsWhy separate? Konva has platform-specific code (browser vs Node.js). Installing it separately ensures Next.js correctly resolves the browser version and avoids SSR errors.
Setup Authentication
Contact your team lead for authentication credentials (.npmrc file).
⚠️ CRITICAL SETUP REQUIREMENTS
Before you start, understand these requirements or features WON'T work:
Layout Container Required: Components MUST be wrapped in a div with:
width: '100vw'andheight: '100vh'display: 'flex'andflexDirection: 'column'- Canvas wrapper needs
flex: 1andposition: 'relative'
stageRef Required: For video recording, you MUST:
- Create a
stageRefwithuseRef<Konva.Stage | null>(null) - Pass it to both
<Toolbar stageRef={stageRef} />and<Canvas onStageReady={...} />
- Create a
CSS Import Required: Import the package's CSS in your root layout
❌ Common Mistakes (these will break video recording):
// ❌ WRONG - No layout wrapper, no stageRef
<ReduxProvider store={store}>
<Toolbar />
<Canvas />
<SlideNavigation />
</ReduxProvider>✅ Correct Setup (see Quick Start below)
🚀 Quick Start
1. Import the CSS styles
IMPORTANT: You must import the package's CSS file for components to display properly.
In your root layout or main CSS file (e.g., app/layout.tsx or app/globals.css):
// In your layout.tsx or _app.tsx
import '@turnix-co/konva-editor/styles.css';Or in your CSS file:
@import '@turnix-co/konva-editor/styles.css';2. Create your editor page with auto-save
IMPORTANT: To enable IndexedDB persistence (auto-save), you must call the useSlidesPersistence() hook.
CRITICAL FOR VIDEO RECORDING: You MUST wrap components in a properly styled container for video recording to work correctly!
'use client';
import { useRef } from 'react';
import {
Canvas,
Toolbar,
SlideNavigation,
ReduxProvider,
store,
useSlidesPersistence
} from '@turnix-co/konva-editor';
import '@turnix-co/konva-editor/styles.css';
import type Konva from 'konva';
// Wrapper component to load persisted slides
function PersistenceLoader({ children }: { children: React.ReactNode }) {
useSlidesPersistence(); // ← This loads slides from IndexedDB on mount
return <>{children}</>;
}
export default function EditorPage() {
const stageRef = useRef<Konva.Stage | null>(null);
return (
<ReduxProvider store={store}>
<PersistenceLoader>
{/* CRITICAL: This wrapper is REQUIRED for video recording to work */}
<div style={{
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden' // Prevents scrollbars
}}>
<Toolbar stageRef={stageRef} />
{/* CRITICAL: Canvas wrapper needs flex: 1 and position: relative */}
<div style={{ flex: 1, position: 'relative' }}>
<Canvas onStageReady={(ref) => { stageRef.current = ref.current; }} />
</div>
<SlideNavigation />
</div>
</PersistenceLoader>
</ReduxProvider>
);
}Important:
- Always add
'use client'at the top! - Call
useSlidesPersistence()insideReduxProviderto enable auto-save - MUST have the wrapper div with
width: '100vw'andheight: '100vh' - MUST pass
stageRefto both Toolbar and Canvas for recording to work
3. Add Publishing (Optional)
The package includes automatic data persistence via IndexedDB + Redux:
- ✅ Auto-save: Slides are automatically saved to browser storage on every change
- ✅ Auto-restore: Slides are automatically loaded when the page refreshes
- ✅ No backend needed: Data persists in the user's browser
How it works:
useSlidesPersistence()hook loads slides from IndexedDB on mount- Redux middleware automatically saves changes to IndexedDB (500ms debounce)
- Works completely offline - no server required!
If you want to publish slides to your backend:
'use client';
import { useRef } from 'react';
import {
Canvas,
Toolbar,
SlideNavigation,
PublishButton,
ReduxProvider,
store,
useSlidesPersistence,
type PublishProgress,
type PublishResponse,
type Slide
} from '@turnix-co/konva-editor';
import '@turnix-co/konva-editor/styles.css';
import type Konva from 'konva';
// Wrapper component for persistence
function PersistenceLoader({ children }: { children: React.ReactNode }) {
useSlidesPersistence();
return <>{children}</>;
}
export default function EditorPage() {
const stageRef = useRef<Konva.Stage | null>(null);
// Your custom publish logic
const handlePublish = async (
slides: Slide[],
onProgress?: (progress: PublishProgress) => void
): Promise<PublishResponse> => {
// Update progress
onProgress?.({
current: 0,
total: slides.length,
status: 'Preparing slides...'
});
// Call your API endpoint
const response = await fetch('/api/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ slides })
});
const data = await response.json();
onProgress?.({
current: slides.length,
total: slides.length,
status: 'Published successfully!'
});
return {
success: true,
message: 'Slides published successfully!',
projectId: data.id,
url: data.url
};
};
return (
<ReduxProvider store={store}>
<PersistenceLoader>
{/* CRITICAL: This wrapper is REQUIRED */}
<div style={{
width: '100vw',
height: '100vh',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden'
}}>
<Toolbar stageRef={stageRef} />
<div style={{ flex: 1, position: 'relative' }}>
<Canvas onStageReady={(ref) => { stageRef.current = ref.current; }} />
</div>
<SlideNavigation />
{/* Add PublishButton with your custom publish function */}
<PublishButton
onPublish={handlePublish}
label="Save to Cloud"
/>
</div>
</PersistenceLoader>
</ReduxProvider>
);
}📚 Components
Core Components
<Canvas />- Main canvas component with drawing, images, videos, and text<Toolbar />- Toolbar with drawing tools, colors, and actions<SlideNavigation />- Multi-slide navigation and management<ScreenRecorder />- Screen recording functionality<PublishButton />- Customizable publish button with progress tracking
Redux Setup
ReduxProvider- Redux provider (from react-redux)store- Pre-configured Redux store
🎥 Using the Recording Feature
The Toolbar component includes a built-in recording button. To enable it, you need to pass the stage reference from the Canvas component to the Toolbar:
'use client';
import { useState, useRef } from 'react';
import {
Canvas,
Toolbar,
SlideNavigation,
ReduxProvider,
store,
type CanvasProps,
type ToolbarProps
} from '@turnix-co/konva-editor';
import Konva from 'konva';
export default function EditorPage() {
const stageRef = useRef<Konva.Stage | null>(null);
return (
<ReduxProvider store={store}>
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}>
{/* Pass stageRef to Toolbar to enable recording */}
<Toolbar stageRef={stageRef} />
<div style={{ flex: 1, position: 'relative' }}>
{/* Canvas provides the stageRef via onStageReady callback */}
<Canvas
onStageReady={(ref) => {
stageRef.current = ref.current;
}}
/>
</div>
<SlideNavigation />
</div>
</ReduxProvider>
);
}Key points:
- Create a
stageRefusinguseRef<Konva.Stage | null>(null) - Pass the
stageRefto the<Toolbar>component - Use the
onStageReadycallback on<Canvas>to populate the stageRef - IMPORTANT: Wrap Canvas in a container with
flex: 1andposition: 'relative' - IMPORTANT: The parent container MUST have
width: '100vw'andheight: '100vh'for recorded videos to display in full screen - The recording button in the Toolbar will now work correctly
Alternatively, you can handle recording externally:
'use client';
import { useState, useRef } from 'react';
import {
Canvas,
Toolbar,
ScreenRecorder,
ReduxProvider,
store
} from '@turnix-co/konva-editor';
import Konva from 'konva';
export default function EditorPage() {
const stageRef = useRef<Konva.Stage | null>(null);
const [showRecorder, setShowRecorder] = useState(false);
const handleRecordingComplete = (videoBlob: Blob, thumbnailDataUrl: string) => {
// Handle the recorded video
console.log('Recording completed', videoBlob);
};
return (
<ReduxProvider store={store}>
<div style={{ width: '100vw', height: '100vh' }}>
{/* Toolbar with custom recording handler */}
<Toolbar
onScreenRecord={() => setShowRecorder(true)}
/>
<Canvas
onStageReady={(ref) => {
stageRef.current = ref.current;
}}
/>
{/* External ScreenRecorder component */}
{showRecorder && (
<ScreenRecorder
onClose={() => setShowRecorder(false)}
stageRef={stageRef}
onRecordingComplete={handleRecordingComplete}
/>
)}
</div>
</ReduxProvider>
);
}🎨 Features
- ✏️ Drawing tools - Pen and eraser with customizable colors and sizes
- 🖼️ Image support - Drag & drop, resize, rotate with context menu
- 🎥 Video support - Upload and playback with thumbnail previews
- 📝 Text elements - Add and edit text on canvas
- 🎙️ Audio recording - Record voice notes for individual objects
- 📹 Screen recording - Record entire canvas with audio
- 📊 Multi-slide presentations - Create up to 20 slides
- ↩️ Undo/Redo - Full history management
- 💾 Auto-save - Automatic persistence with IndexedDB + Redux
- 📤 Export - Export slides as images
- 🚀 Publishing - Customizable publish button with progress tracking
🔧 Advanced Usage
Custom Actions
import { useDispatch, addImage, setTool } from '@turnix/konva-editor';
function CustomControls() {
const dispatch = useDispatch();
const handleAddImage = () => {
dispatch(addImage({
id: 'custom-id',
src: 'https://example.com/image.jpg',
x: 100,
y: 100,
width: 200,
height: 200,
}));
};
return <button onClick={handleAddImage}>Add Image</button>;
}TypeScript Support
Full TypeScript support with exported types:
import type { RootState, ImageElement, CanvasState } from '@turnix/konva-editor';📖 API Reference
PublishButton Props
interface PublishButtonProps {
// Custom publish function (required)
onPublish?: (
slides: Slide[],
onProgress?: (progress: PublishProgress) => void
) => Promise<PublishResponse>;
// Custom button label (optional, default: "Publish Slides")
label?: string;
// Custom className for positioning/styling (optional)
className?: string;
}
interface PublishProgress {
current: number; // Current progress (e.g., 5)
total: number; // Total items (e.g., 10)
status: string; // Status message (e.g., "Uploading...")
}
interface PublishResponse {
success: boolean; // Whether publish succeeded
message: string; // Success/error message
projectId?: string; // Optional: ID from your backend
url?: string; // Optional: URL to published project
}Example API Endpoint (app/api/publish/route.ts):
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const { slides } = await request.json();
// Save to your database
const project = await db.project.create({
data: {
slides: JSON.stringify(slides),
createdAt: new Date(),
}
});
return NextResponse.json({
id: project.id,
url: `/projects/${project.id}`,
});
}Redux Actions
Canvas Actions:
addImage(payload)- Add image to canvasaddVideo(payload)- Add video to canvasaddText(payload)- Add text to canvasclearCanvas()- Clear current slideundo()- Undo last actionredo()- Redo undone actiondeleteElement(id)- Delete element by ID
Slide Actions:
addSlide()- Create new slidedeleteSlide(id)- Delete slidesetCurrentSlide(id)- Switch to slideduplicateSlide(id)- Duplicate slide
Toolbar Actions:
setTool(tool)- Set active tool ('pen' | 'eraser' | 'text')setPenColor(color)- Set pen colorsetStrokeWidth(width)- Set stroke width
Utilities
exportSlideAsImage(slideId)- Export slide as PNGexportSlideAsPDF(slideId)- Export slide as PDF
🏗️ Architecture
The package uses:
- React 19 for UI
- Konva for canvas rendering
- Redux Toolkit for state management
- IndexedDB for persistence
- TypeScript for type safety
🔐 Security
⚠️ This is a private package. Do not:
- Share authentication credentials
- Publish package code publicly
- Include in public repositories
📝 License
UNLICENSED - Private and proprietary software for Turnix team use only.
🆘 Support
For issues or questions, contact the Turnix development team.
🐛 Troubleshooting
Recorded Video Not Displaying Full Screen
If your recorded video doesn't display in full screen when played:
Check Container Styling: Ensure the Canvas is wrapped in a container that fills the viewport:
<div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column' }}> <Toolbar stageRef={stageRef} /> <div style={{ flex: 1, position: 'relative' }}> <Canvas onStageReady={(ref) => stageRef.current = ref.current} /> </div> </div>Verify stageRef is Passed: Make sure you're passing the
stageRefto the Toolbar component:<Toolbar stageRef={stageRef} />Check onStageReady Callback: Ensure the Canvas component's
onStageReadycallback is properly setting the stageRef:<Canvas onStageReady={(ref) => { stageRef.current = ref.current; }} />No Fixed Dimensions: Avoid setting fixed width/height on the Canvas container. Use
flex: 1instead to allow it to fill available space.
Download and Re-record Buttons Not Visible
After recording stops, you should see a preview dialog with three buttons:
- Add to Canvas - Adds the video to the canvas (clears existing content)
- Download - Downloads the video file locally
- Re-record - Discards the recording and starts over
If you don't see these buttons, ensure you're using version 1.3.3 or later:
npm install @turnix-co/konva-editor@latest --registry=https://npm.pkg.github.comBuilt with ❤️ by the Turnix team
