← スキル一覧に戻る

creating-components
by Nymfarious
StickerNest V3 - A sticker canvas application
⭐ 0🍴 0📅 2025年12月28日
SKILL.md
name: creating-components description: Creating React components for StickerNest. Use when the user asks to create a component, build a UI component, add a panel, create a toolbar, build a dialog, or implement any React UI element. Covers component structure, Zustand integration, theme tokens, hooks, and common UI patterns.
Creating StickerNest React Components
This skill covers creating React components that follow StickerNest's patterns, including Zustand store integration, theme tokens, and custom hooks.
Component Location
| Type | Location |
|---|---|
| UI Components | src/components/ |
| Shared UI | src/shared-ui/ |
| Hooks | src/hooks/ |
Basic Component Template
// src/components/MyComponent.tsx
import React, { useState, useCallback, useEffect } from 'react';
import { useCanvasStore } from '../state/useCanvasStore';
interface MyComponentProps {
/** Component property */
value?: string;
/** Callback when value changes */
onChange?: (value: string) => void;
/** Optional CSS class */
className?: string;
}
export const MyComponent: React.FC<MyComponentProps> = ({
value = '',
onChange,
className,
}) => {
// Local state
const [localValue, setLocalValue] = useState(value);
// Zustand store state (select only what you need)
const canvasId = useCanvasStore((s) => s.canvasId);
const addWidget = useCanvasStore((s) => s.addWidget);
// Handlers
const handleChange = useCallback((newValue: string) => {
setLocalValue(newValue);
onChange?.(newValue);
}, [onChange]);
// Effects
useEffect(() => {
setLocalValue(value);
}, [value]);
return (
<div
className={className}
style={{
backgroundColor: 'var(--sn-bg-secondary)',
borderRadius: 'var(--sn-radius-md)',
padding: 'var(--sn-space-4)',
color: 'var(--sn-text-primary)',
}}
>
{/* Component content */}
</div>
);
};
Zustand Store Integration
Selecting State (Optimized)
// GOOD: Select only what you need
const canvasId = useCanvasStore((s) => s.canvasId);
const widgets = useCanvasStore((s) => s.widgets);
// BAD: Selecting entire state causes unnecessary re-renders
const store = useCanvasStore(); // Avoid this
Multiple Selectors
// Option 1: Multiple hooks (recommended for unrelated data)
const canvasId = useCanvasStore((s) => s.canvasId);
const mode = useCanvasStore((s) => s.mode);
// Option 2: Single selector for related data
const { canvasId, mode } = useCanvasStore((s) => ({
canvasId: s.canvasId,
mode: s.mode,
}));
Accessing Actions
// Actions are stable, safe to access directly
const addWidget = useCanvasStore((s) => s.addWidget);
const removeWidget = useCanvasStore((s) => s.removeWidget);
// Or destructure multiple
const { addWidget, removeWidget, updateWidget } = useCanvasStore((s) => ({
addWidget: s.addWidget,
removeWidget: s.removeWidget,
updateWidget: s.updateWidget,
}));
Available Stores
| Store | Purpose | Key State |
|---|---|---|
useCanvasStore | Canvas & widgets | canvasId, widgets, mode, selection |
useLibraryStore | Widget library | searchQuery, selectedWidgets, filters |
usePanelsStore | Panel states | isPanelOpen, panelPositions |
useToolStore | Active tools | activeTool, shapeDefaults |
useThemeStore | Theme settings | theme, customColors |
useAssetStore | Asset management | assets, uploadProgress |
useApiSettingsStore | API config | apiKey, endpoint |
Theme Tokens
Color Tokens
/* Backgrounds */
--sn-bg-primary: #0f0f19;
--sn-bg-secondary: #1a1a2e;
--sn-bg-tertiary: #252538;
--sn-bg-elevated: #2a2a42;
/* Text */
--sn-text-primary: #e2e8f0;
--sn-text-secondary: #94a3b8;
--sn-text-muted: #64748b;
/* Accents */
--sn-accent-primary: #8b5cf6;
--sn-accent-secondary: #a78bfa;
--sn-success: #22c55e;
--sn-error: #ef4444;
--sn-warning: #f59e0b;
--sn-info: #3b82f6;
/* Borders */
--sn-border-primary: rgba(139, 92, 246, 0.2);
--sn-border-secondary: rgba(255, 255, 255, 0.1);
Spacing Tokens
--sn-space-1: 4px;
--sn-space-2: 8px;
--sn-space-3: 12px;
--sn-space-4: 16px;
--sn-space-5: 20px;
--sn-space-6: 24px;
--sn-space-8: 32px;
Border Radius
--sn-radius-sm: 4px;
--sn-radius-md: 6px;
--sn-radius-lg: 8px;
--sn-radius-xl: 12px;
--sn-radius-full: 9999px;
Elevation (Shadows)
--sn-elevation-1: 0 1px 2px rgba(0, 0, 0, 0.3);
--sn-elevation-2: 0 2px 4px rgba(0, 0, 0, 0.4);
--sn-elevation-3: 0 4px 8px rgba(0, 0, 0, 0.5);
--sn-elevation-panel: 0 4px 16px rgba(0, 0, 0, 0.5);
Transitions
--sn-transition-fast: 150ms ease;
--sn-transition-normal: 200ms ease;
--sn-transition-slow: 300ms ease;
--sn-ease-spring-snappy: cubic-bezier(0.34, 1.56, 0.64, 1);
Glass Effect
--sn-glass-bg: rgba(15, 15, 25, 0.85);
--sn-glass-blur: blur(12px);
--sn-glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
Common UI Patterns
Panel Component
interface PanelProps {
title: string;
isOpen: boolean;
onClose: () => void;
children: React.ReactNode;
}
export const Panel: React.FC<PanelProps> = ({
title,
isOpen,
onClose,
children,
}) => {
if (!isOpen) return null;
return (
<div
style={{
position: 'absolute',
right: 0,
top: 0,
bottom: 0,
width: 320,
background: 'var(--sn-glass-bg)',
backdropFilter: 'var(--sn-glass-blur)',
borderLeft: '1px solid var(--sn-border-primary)',
boxShadow: 'var(--sn-elevation-panel)',
display: 'flex',
flexDirection: 'column',
}}
>
{/* Header */}
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: 'var(--sn-space-4)',
borderBottom: '1px solid var(--sn-border-secondary)',
}}
>
<span style={{ fontWeight: 600, color: 'var(--sn-text-primary)' }}>
{title}
</span>
<button
onClick={onClose}
style={{
background: 'transparent',
border: 'none',
color: 'var(--sn-text-secondary)',
cursor: 'pointer',
padding: 'var(--sn-space-1)',
}}
>
×
</button>
</div>
{/* Content */}
<div style={{ flex: 1, overflow: 'auto', padding: 'var(--sn-space-4)' }}>
{children}
</div>
</div>
);
};
Button Component
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children,
}) => {
const baseStyles: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: 'var(--sn-radius-md)',
fontWeight: 500,
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition: 'var(--sn-transition-fast)',
border: 'none',
};
const variantStyles: Record<string, React.CSSProperties> = {
primary: {
background: 'var(--sn-accent-primary)',
color: 'white',
},
secondary: {
background: 'var(--sn-bg-tertiary)',
color: 'var(--sn-text-primary)',
border: '1px solid var(--sn-border-primary)',
},
ghost: {
background: 'transparent',
color: 'var(--sn-text-secondary)',
},
};
const sizeStyles: Record<string, React.CSSProperties> = {
sm: { padding: '6px 12px', fontSize: 12 },
md: { padding: '8px 16px', fontSize: 14 },
lg: { padding: '12px 24px', fontSize: 16 },
};
return (
<button
onClick={onClick}
disabled={disabled}
style={{
...baseStyles,
...variantStyles[variant],
...sizeStyles[size],
}}
>
{children}
</button>
);
};
Input Component
interface InputProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
type?: 'text' | 'number' | 'password';
disabled?: boolean;
}
export const Input: React.FC<InputProps> = ({
value,
onChange,
placeholder,
type = 'text',
disabled = false,
}) => {
return (
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
style={{
width: '100%',
padding: 'var(--sn-space-2) var(--sn-space-3)',
background: 'var(--sn-bg-tertiary)',
border: '1px solid var(--sn-border-primary)',
borderRadius: 'var(--sn-radius-md)',
color: 'var(--sn-text-primary)',
fontSize: 14,
outline: 'none',
transition: 'var(--sn-transition-fast)',
}}
/>
);
};
List Item Component
interface ListItemProps {
selected?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export const ListItem: React.FC<ListItemProps> = ({
selected = false,
onClick,
children,
}) => {
return (
<div
onClick={onClick}
style={{
padding: 'var(--sn-space-3)',
borderRadius: 'var(--sn-radius-md)',
background: selected ? 'var(--sn-accent-primary)' : 'transparent',
color: selected ? 'white' : 'var(--sn-text-primary)',
cursor: 'pointer',
transition: 'var(--sn-transition-fast)',
}}
>
{children}
</div>
);
};
Custom Hooks
Creating a Custom Hook
// src/hooks/useMyFeature.ts
import { useState, useCallback, useEffect } from 'react';
import { useCanvasStore } from '../state/useCanvasStore';
interface UseMyFeatureOptions {
initialValue?: string;
autoSave?: boolean;
}
interface UseMyFeatureReturn {
value: string;
setValue: (value: string) => void;
isLoading: boolean;
error: string | null;
}
export function useMyFeature(
options: UseMyFeatureOptions = {}
): UseMyFeatureReturn {
const { initialValue = '', autoSave = false } = options;
// Local state
const [value, setValueState] = useState(initialValue);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// Store integration
const canvasId = useCanvasStore((s) => s.canvasId);
// Handlers
const setValue = useCallback((newValue: string) => {
setValueState(newValue);
setError(null);
if (autoSave) {
// Auto-save logic
}
}, [autoSave]);
// Effects
useEffect(() => {
// Initialization or subscription logic
return () => {
// Cleanup
};
}, [canvasId]);
return {
value,
setValue,
isLoading,
error,
};
}
Existing Hooks Reference
| Hook | Purpose | File |
|---|---|---|
useCanvasController | Canvas control logic | src/hooks/useCanvasController.ts |
useCanvasGestures | Pan, zoom, touch | src/hooks/useCanvasGestures.ts |
useCanvasKeyboardShortcuts | Keyboard bindings | src/hooks/useCanvasKeyboardShortcuts.ts |
useResponsive | Viewport detection | src/hooks/useResponsive.ts |
useWidgetCapabilities | Capability checking | src/hooks/useWidgetCapabilities.ts |
useWidgetSync | Widget synchronization | src/hooks/useWidgetSync.ts |
usePermission | Permission checking | src/hooks/usePermission.ts |
Component Best Practices
1. Memoization
// Memoize expensive computations
const processedData = useMemo(() => {
return data.map(item => expensiveProcess(item));
}, [data]);
// Memoize callbacks passed to children
const handleClick = useCallback(() => {
doSomething(value);
}, [value]);
// Memoize components that receive stable props
const MemoizedChild = React.memo(ChildComponent);
2. Error Boundaries
// Wrap components that might throw
<ErrorBoundary fallback={<ErrorFallback />}>
<RiskyComponent />
</ErrorBoundary>
3. Loading States
if (isLoading) {
return (
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--sn-text-secondary)',
}}>
Loading...
</div>
);
}
4. Conditional Rendering with Visibility
// For panels that need to maintain state, use CSS instead of unmounting
<div style={{ display: isVisible ? 'block' : 'none' }}>
<ExpensivePanel />
</div>
Reference Files
- Theme tokens:
src/styles/theme-tokens.css - Global styles:
src/index.css - Example components:
src/components/PropertiesPanel.tsx,src/components/LayerPanel.tsx - Hooks:
src/hooks/ - Stores:
src/state/
スコア
総合スコア
50/100
リポジトリの品質指標に基づく評価
✓SKILL.md
SKILL.mdファイルが含まれている
+20
○LICENSE
ライセンスが設定されている
0/10
○説明文
100文字以上の説明がある
0/10
○人気
GitHub Stars 100以上
0/15
○最近の活動
3ヶ月以内に更新がある
0/10
○フォーク
10回以上フォークされている
0/5
✓Issue管理
オープンIssueが50未満
+5
✓言語
プログラミング言語が設定されている
+5
○タグ
1つ以上のタグが設定されている
0/5
レビュー
💬
レビュー機能は近日公開予定です