スキル一覧に戻る
Nymfarious

creating-components

by Nymfarious

StickerNest V3 - A sticker canvas application

0🍴 0📅 2025年12月28日
GitHubで見るManusで実行

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

TypeLocation
UI Componentssrc/components/
Shared UIsrc/shared-ui/
Hookssrc/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

StorePurposeKey State
useCanvasStoreCanvas & widgetscanvasId, widgets, mode, selection
useLibraryStoreWidget librarysearchQuery, selectedWidgets, filters
usePanelsStorePanel statesisPanelOpen, panelPositions
useToolStoreActive toolsactiveTool, shapeDefaults
useThemeStoreTheme settingstheme, customColors
useAssetStoreAsset managementassets, uploadProgress
useApiSettingsStoreAPI configapiKey, 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)',
          }}
        >
          &times;
        </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

HookPurposeFile
useCanvasControllerCanvas control logicsrc/hooks/useCanvasController.ts
useCanvasGesturesPan, zoom, touchsrc/hooks/useCanvasGestures.ts
useCanvasKeyboardShortcutsKeyboard bindingssrc/hooks/useCanvasKeyboardShortcuts.ts
useResponsiveViewport detectionsrc/hooks/useResponsive.ts
useWidgetCapabilitiesCapability checkingsrc/hooks/useWidgetCapabilities.ts
useWidgetSyncWidget synchronizationsrc/hooks/useWidgetSync.ts
usePermissionPermission checkingsrc/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

レビュー

💬

レビュー機能は近日公開予定です