Agent skill
canvas-component
Creates and extends Canvas UI components with Monaco editor, split views, and educational context. Use when building Canvas panel, editor, or preview features.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/canvas-component
SKILL.md
Canvas Component Development Skill
When to Use
Use this skill when:
- Creating Canvas panel or container components
- Adding Monaco editor features
- Building code preview/execution UI
- Implementing split-view layouts
- Adding toolbar actions (run, download, share)
Component Architecture
components/canvas/
├── canvas-container.tsx # Root container with state
├── canvas-panel.tsx # Full panel with editor + preview
├── canvas-editor.tsx # Monaco wrapper
├── canvas-preview.tsx # Execution preview
├── canvas-toolbar.tsx # Actions toolbar
├── canvas-editor-error-boundary.tsx # Error recovery
└── index.ts # Barrel exports
State Management (Zustand)
Canvas Store Pattern
import { create } from 'zustand';
import type { CanvasState, CanvasType, ViewMode } from '@/lib/canvas/types';
interface CanvasStore extends CanvasState {
// Actions
openCanvas: (config: CanvasConfig) => void;
closeCanvas: () => void;
updateContent: (content: string) => void;
setViewMode: (mode: ViewMode) => void;
undo: () => void;
redo: () => void;
// Generation
startGeneration: (prompt: string) => void;
completeGeneration: (content: string) => void;
}
export const useCanvasStore = create<CanvasStore>((set, get) => ({
// Initial state
isOpen: false,
content: '',
type: 'code',
title: 'Untitled',
language: 'python',
viewMode: 'split',
history: [],
historyIndex: -1,
generationPrompt: '',
isGenerating: false,
openCanvas: (config) => set({
isOpen: true,
type: config.type,
title: config.title,
language: config.language || getDefaultLanguage(config.type),
content: config.initialContent || '',
generationPrompt: config.generationPrompt || '',
viewMode: 'split',
history: [config.initialContent || ''],
historyIndex: 0,
}),
updateContent: (content) => {
const { history, historyIndex } = get();
const newHistory = [...history.slice(0, historyIndex + 1), content];
set({
content,
history: newHistory,
historyIndex: newHistory.length - 1,
});
},
// ...
}));
Monaco Editor Wrapper
Basic Setup
'use client';
import { useRef, useCallback } from 'react';
import MonacoEditor, { OnMount, OnChange } from '@monaco-editor/react';
import { getMonacoLanguage } from '@/lib/canvas/types';
import { CanvasEditorErrorBoundary } from './canvas-editor-error-boundary';
interface CanvasEditorProps {
content: string;
language: string;
onChange: (value: string) => void;
readOnly?: boolean;
height?: string;
}
export function CanvasEditor({
content,
language,
onChange,
readOnly = false,
height = '100%',
}: CanvasEditorProps) {
const editorRef = useRef<any>(null);
const handleMount: OnMount = (editor, monaco) => {
editorRef.current = editor;
// Configure Monaco for educational use
monaco.editor.defineTheme('canvas-theme', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#1a1a1a',
},
});
editor.updateOptions({
fontSize: 14,
lineHeight: 22,
minimap: { enabled: false },
scrollBeyondLastLine: false,
wordWrap: 'on',
tabSize: 4,
insertSpaces: true,
});
};
const handleChange: OnChange = (value) => {
onChange(value || '');
};
return (
<CanvasEditorErrorBoundary>
<MonacoEditor
height={height}
language={getMonacoLanguage(language)}
value={content}
onChange={handleChange}
onMount={handleMount}
theme="canvas-theme"
options={{
readOnly,
automaticLayout: true,
}}
loading={<EditorSkeleton />}
/>
</CanvasEditorErrorBoundary>
);
}
Error Boundary
'use client';
import { Component, ReactNode } from 'react';
import { AlertTriangle, RefreshCw } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { resetMonacoLoader } from '@/lib/canvas/monaco-loader';
interface Props {
children: ReactNode;
}
interface State {
hasError: boolean;
error: Error | null;
}
export class CanvasEditorErrorBoundary extends Component<Props, State> {
state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error };
}
handleReset = () => {
resetMonacoLoader();
this.setState({ hasError: false, error: null });
};
render() {
if (this.state.hasError) {
return (
<div className="flex flex-col items-center justify-center h-full gap-4 p-8 text-center">
<AlertTriangle className="h-8 w-8 text-destructive" />
<div>
<h3 className="font-medium">Editor failed to load</h3>
<p className="text-sm text-muted-foreground mt-1">
This usually resolves after a page refresh.
</p>
</div>
<Button onClick={this.handleReset} size="sm">
<RefreshCw className="h-4 w-4 mr-2" />
Try Again
</Button>
</div>
);
}
return this.props.children;
}
}
Split View Panel
Resizable Layout
'use client';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '@/components/ui/resizable';
import { CanvasEditor } from './canvas-editor';
import { CanvasPreview } from './canvas-preview';
import { CanvasToolbar } from './canvas-toolbar';
import type { ViewMode } from '@/lib/canvas/types';
interface CanvasPanelProps {
content: string;
language: string;
viewMode: ViewMode;
onContentChange: (content: string) => void;
onViewModeChange: (mode: ViewMode) => void;
onRun: () => void;
}
export function CanvasPanel({
content,
language,
viewMode,
onContentChange,
onViewModeChange,
onRun,
}: CanvasPanelProps) {
return (
<div className="flex flex-col h-full">
<CanvasToolbar
viewMode={viewMode}
onViewModeChange={onViewModeChange}
onRun={onRun}
language={language}
/>
<div className="flex-1 min-h-0">
{viewMode === 'split' ? (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={50} minSize={30}>
<CanvasEditor
content={content}
language={language}
onChange={onContentChange}
/>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50} minSize={30}>
<CanvasPreview
content={content}
language={language}
/>
</ResizablePanel>
</ResizablePanelGroup>
) : viewMode === 'code' ? (
<CanvasEditor
content={content}
language={language}
onChange={onContentChange}
/>
) : (
<CanvasPreview
content={content}
language={language}
/>
)}
</div>
</div>
);
}
Toolbar Actions
Standard Toolbar
'use client';
import { Play, Download, Copy, Code, Eye, Columns2, Undo, Redo } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { canExecute, getFileExtension } from '@/lib/canvas/types';
import type { ViewMode } from '@/lib/canvas/types';
interface CanvasToolbarProps {
viewMode: ViewMode;
onViewModeChange: (mode: ViewMode) => void;
onRun: () => void;
onUndo?: () => void;
onRedo?: () => void;
canUndo?: boolean;
canRedo?: boolean;
language: string;
content?: string;
isRunning?: boolean;
}
export function CanvasToolbar({
viewMode,
onViewModeChange,
onRun,
onUndo,
onRedo,
canUndo,
canRedo,
language,
content,
isRunning,
}: CanvasToolbarProps) {
const showRunButton = canExecute(language);
const handleDownload = () => {
const blob = new Blob([content || ''], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `code${getFileExtension(language)}`;
a.click();
URL.revokeObjectURL(url);
};
const handleCopy = async () => {
await navigator.clipboard.writeText(content || '');
// Show toast
};
return (
<div className="flex items-center justify-between px-2 py-1.5 border-b bg-muted/50">
<div className="flex items-center gap-1">
{/* View Mode Toggle */}
<ToggleGroup
type="single"
value={viewMode}
onValueChange={(v) => v && onViewModeChange(v as ViewMode)}
size="sm"
>
<ToggleGroupItem value="code" aria-label="Code only">
<Code className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem value="split" aria-label="Split view">
<Columns2 className="h-4 w-4" />
</ToggleGroupItem>
<ToggleGroupItem value="preview" aria-label="Preview only">
<Eye className="h-4 w-4" />
</ToggleGroupItem>
</ToggleGroup>
<div className="w-px h-4 bg-border mx-1" />
{/* Undo/Redo */}
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={onUndo}
disabled={!canUndo}
>
<Undo className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Undo (Ctrl+Z)</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={onRedo}
disabled={!canRedo}
>
<Redo className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Redo (Ctrl+Shift+Z)</TooltipContent>
</Tooltip>
</div>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleCopy}>
<Copy className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Copy code</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={handleDownload}>
<Download className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Download</TooltipContent>
</Tooltip>
{showRunButton && (
<Button
size="sm"
className="ml-2 gap-1"
onClick={onRun}
disabled={isRunning}
>
<Play className="h-3 w-3" />
{isRunning ? 'Running...' : 'Run'}
</Button>
)}
</div>
</div>
);
}
Keyboard Shortcuts
Hook Implementation
import { useHotkeys } from 'react-hotkeys-hook';
function CanvasWithShortcuts() {
const { content, updateContent, undo, redo, canUndo, canRedo } = useCanvasStore();
// Run code
useHotkeys('mod+enter', () => handleRun(), { enableOnFormTags: true });
// Undo/Redo (Monaco handles internal, this is for store)
useHotkeys('mod+z', () => undo(), { enabled: canUndo });
useHotkeys('mod+shift+z', () => redo(), { enabled: canRedo });
// Toggle view modes
useHotkeys('mod+1', () => setViewMode('code'));
useHotkeys('mod+2', () => setViewMode('split'));
useHotkeys('mod+3', () => setViewMode('preview'));
}
Educational Context Display
Learning Objective Header
interface EducationalContextProps {
context?: {
topic?: string;
difficulty?: 'beginner' | 'intermediate' | 'advanced';
learningObjective?: string;
};
}
function EducationalContextHeader({ context }: EducationalContextProps) {
if (!context?.learningObjective) return null;
const difficultyColors = {
beginner: 'bg-green-100 text-green-800',
intermediate: 'bg-amber-100 text-amber-800',
advanced: 'bg-red-100 text-red-800',
};
return (
<div className="px-4 py-2 border-b bg-muted/30">
<div className="flex items-center gap-2 text-sm">
{context.difficulty && (
<span className={cn(
'px-2 py-0.5 rounded-full text-xs font-medium',
difficultyColors[context.difficulty]
)}>
{context.difficulty}
</span>
)}
{context.topic && (
<span className="text-muted-foreground">
{context.topic}
</span>
)}
</div>
<p className="text-sm mt-1">{context.learningObjective}</p>
</div>
);
}
Accessibility Requirements
WCAG 2.1 AA Checklist
- Focus visible on all interactive elements
- Keyboard navigation for all actions
- Screen reader labels for icons
- Color contrast 4.5:1 minimum
- Announced status changes (execution results)
- Skip links for editor navigation
// Example: Screen reader announcement
import { useEffect } from 'react';
function useAnnounce() {
const announce = (message: string) => {
const el = document.createElement('div');
el.setAttribute('role', 'status');
el.setAttribute('aria-live', 'polite');
el.className = 'sr-only';
el.textContent = message;
document.body.appendChild(el);
setTimeout(() => el.remove(), 1000);
};
return announce;
}
// Usage
const announce = useAnnounce();
announce('Code executed successfully');
Testing Checklist
- Monaco loads without errors
- Split view resizing works
- View mode toggles correctly
- Keyboard shortcuts function
- Undo/redo maintains history
- Copy/download work
- Mobile responsive
- Error boundary catches Monaco failures
- Educational context displays
- Accessibility requirements met
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?