npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

momoi-layer-ui

v0.1.4

Published

お絵かき用レイヤーシステム(momoi-layer)のReact UIコンポーネント

Readme

momoi-layer-ui

お絵かき用レイヤーシステム momoi-layer の React UI コンポーネントパッケージ。

Photoshop / ClipStudio 風のレイヤーパネルを提供し、ドラッグ&ドロップによるレイヤー並べ替え、ブレンドモード・不透明度変更、右クリックコンテキストメニュー、キーボードショートカットをサポートします。

アーキテクチャ

Controlled mode

LayerPanel は LayerStack直接変更しません。全ての操作はコールバック (onSelect, onMove, onToggleVisible 等) で通知されるため、プロジェクト側が変更を行い ref.sync() で UI を更新します。この設計により Undo/Redo の導入が容易です。

ユーザー操作 → コールバック発火 → プロジェクト側で変更 → sync() → UI 更新

主要モジュール

| モジュール | 役割 | |---|---| | LayerPanel | メインUIコンポーネント。レイヤーリスト、ブレンドモード、不透明度スライダーを含む | | LayerToolbar | パネル下部のツールバー(新規レイヤー、グループ作成、削除ボタン) | | ContextMenu | 右クリックメニュー。ペイントソフト風ダークテーマ | | useLayerStack | 読み取り専用hook。スナップショット生成と展開状態管理 | | useLayerDnD | Pointer Events ベースの DnD。グループ内外の移動対応 | | useKeybindings | momoi-keybind 連携。ショートカットキー登録 | | cloneLayer | レイヤーのディープコピー(ピクセル・パス・子レイヤー含む) |

インストール

npm install momoi-layer-ui momoi-layer react react-dom tailwindcss

peer dependencies

| パッケージ | バージョン | 必須 | |---|---|---| | react | ^18.0.0 || ^19.0.0 | 必須 | | react-dom | ^18.0.0 || ^19.0.0 | 必須 | | momoi-layer | >=0.1.0 | 必須 | | tailwindcss | ^4.0.0 | 必須 | | momoi-keybind | >=1.0.0 | オプション |

Tailwind CSS v4 が導入先プロジェクトで使われている前提です。コンポーネントは Tailwind クラスをそのまま使用します。

momoi-keybind(オプション)

キーボードショートカットを使う場合のみインストールしてください。

npm install momoi-keybind

クイックスタート

import { useRef } from 'react'
import { LayerStack, RasterLayer } from 'momoi-layer'
import { LayerPanel, LayerToolbar, type LayerPanelHandle } from 'momoi-layer-ui'

// LayerStack を作成
const stack = new LayerStack()
stack.root.addChild(new RasterLayer('レイヤー1'))
stack.root.addChild(new RasterLayer('背景'))

function App() {
  const panelRef = useRef<LayerPanelHandle>(null)

  const handleToggleVisible = (id: number) => {
    const layer = stack.findById(id)
    if (layer) layer.visible = !layer.visible
    panelRef.current?.sync()
  }

  const handleAddRaster = () => {
    stack.root.addChild(new RasterLayer('新規レイヤー'))
    panelRef.current?.sync()
  }

  return (
    <div className="w-[260px]">
      <LayerPanel
        ref={panelRef}
        stack={stack}
        onSelect={(id) => {
          stack.activeLayerId = id
          panelRef.current?.sync()
        }}
        onToggleVisible={handleToggleVisible}
      />
      <LayerToolbar
        activeLayerId={stack.activeLayerId}
        onAddRaster={handleAddRaster}
      />
    </div>
  )
}

API リファレンス

コンポーネント

LayerPanel

メインのレイヤーパネルコンポーネント。forwardRef を使用し、ref.sync() で UI を更新します。

<LayerPanel ref={panelRef} stack={stack} onSelect={handleSelect} />

Props: LayerPanelProps

Ref: LayerPanelHandle


LayerToolbar

パネル下部に配置するツールバー。新規レイヤー作成、グループ作成、削除ボタンを含みます。

<LayerToolbar
  activeLayerId={activeLayerId}
  onAddRaster={() => { /* ... */ }}
  onAddGroup={() => { /* ... */ }}
  onDelete={() => { /* ... */ }}
/>

Props: LayerToolbarProps


ContextMenu

右クリックコンテキストメニュー。LayerPanel 内部で使用されていますが、単独でも利用可能です。

<ContextMenu
  x={event.clientX}
  y={event.clientY}
  items={[
    { label: '複製', action: () => duplicate() },
    { separator: true },
    { label: '削除', action: () => remove() },
  ]}
  onClose={() => setOpen(false)}
/>

Props: ContextMenuProps

Hooks

useLayerStack(stack)

LayerStack を React state として追跡する hook。読み取り専用で、レイヤーのプロパティ変更は行いません。

const {
  layers,           // LayerSnapshot[] - フラットなスナップショット配列
  activeLayerId,    // number | null
  sync,             // () => void - UIを再描画
  selectLayer,      // (id: number | null) => void
  toggleExpanded,   // (id: number) => void
  setExpanded,      // (id: number, expanded: boolean) => void
  isExpanded,       // (id: number) => boolean
} = useLayerStack(stack)

| 返り値 | 型 | 説明 | |---|---|---| | layers | LayerSnapshot[] | レイヤーツリーのスナップショット | | activeLayerId | number \| null | 現在アクティブなレイヤーID | | sync | () => void | LayerStack の現在の状態で UI を再描画する | | selectLayer | (id: number \| null) => void | アクティブレイヤーを設定 | | toggleExpanded | (id: number) => void | グループの展開/折りたたみをトグル | | setExpanded | (id: number, expanded: boolean) => void | グループの展開状態を明示的に設定 | | isExpanded | (id: number) => boolean | 指定IDが展開中かどうか |


useLayerDnD(onDrop)

Pointer Events ベースのドラッグ&ドロップ hook。レイヤーアイテムの並べ替えに使用します。

グループの before / after / inside 判定、カーソル X 座標によるインデントレベル推定に対応しています。

const {
  draggingId,     // number | null - ドラッグ中のレイヤーID
  dropTarget,     // DropTarget | null - 現在のドロップ先
  isDragging,     // boolean
  onPointerDown,  // (e: PointerEvent, layerId: number) => void
  onPointerMove,  // (e: PointerEvent) => void
  onPointerUp,    // (e: PointerEvent) => void
} = useLayerDnD((dragId, target) => {
  console.log('dropped', dragId, target)
})

useKeybindings(inputService, handlers, options?)

momoi-keybind と連携し、レイヤー操作のキーボードショートカットを登録する hook。

momoi-keybind がインストールされていない場合、inputServiceundefined を渡すと何もしません。

import { useKeybindings, DEFAULT_LAYER_KEYBINDINGS } from 'momoi-layer-ui'

useKeybindings(inputService, {
  onDelete: () => deleteActiveLayer(),
  onDuplicate: () => duplicateActiveLayer(),
  onMoveUp: () => moveLayerUp(),
  onMoveDown: () => moveLayerDown(),
  onToggleVisible: () => toggleVisible(),
  onToggleLock: () => toggleLock(),
  onToggleAlphaLock: () => toggleAlphaLock(),
  onToggleClipping: () => toggleClipping(),
  onRename: () => startRename(),
  onGroup: () => groupSelected(),
  onDeselect: () => deselectAll(),
}, { applyDefaults: true })

| パラメータ | 型 | 説明 | |---|---|---| | inputService | InputService \| undefined | momoi-keybind の InputService インスタンス | | handlers | object | 各コマンドのハンドラー(下表参照) | | options.applyDefaults | boolean | デフォルトキーバインドを自動登録する(デフォルト: true) |

handlers の各プロパティ:

| プロパティ | コマンドID | |---|---| | onDelete | layer.delete | | onDuplicate | layer.duplicate | | onRename | layer.rename | | onMoveUp | layer.moveUp | | onMoveDown | layer.moveDown | | onToggleVisible | layer.toggleVisible | | onToggleLock | layer.toggleLock | | onToggleAlphaLock | layer.toggleAlphaLock | | onToggleClipping | layer.toggleClipping | | onGroup | layer.group | | onDeselect | layer.deselect |


DEFAULT_LAYER_KEYBINDINGS

Photoshop / ClipStudio 風のデフォルトキーバインド定義。layerPanelFocus コンテキストが有効なときのみ動作します。

import { DEFAULT_LAYER_KEYBINDINGS } from 'momoi-layer-ui'

// 他のキーバインドと合わせて登録
inputService.setDefaultKeybindings([
  ...otherKeybindings,
  ...DEFAULT_LAYER_KEYBINDINGS,
])

// コンテキストを有効にする
inputService.setContext('layerPanelFocus', true)

| キー | コマンド | |---|---| | Delete | layer.delete | | Ctrl+J | layer.duplicate | | F2 | layer.rename | | Ctrl+] | layer.moveUp | | Ctrl+[ | layer.moveDown | | Ctrl+Shift+H | layer.toggleVisible | | Ctrl+L | layer.toggleLock | | Ctrl+Shift+L | layer.toggleAlphaLock | | Ctrl+Shift+C | layer.toggleClipping | | Ctrl+G | layer.group | | Escape | layer.deselect |

ユーティリティ

cloneLayer(layer)

レイヤーのディープコピーを作成します。ピクセルデータ、ベクターパスデータ、子レイヤーを全て含む完全なコピーです。新しい ID が自動採番されます。

import { cloneLayer } from 'momoi-layer-ui'

const original = stack.findById(layerId)
if (original) {
  const copy = cloneLayer(original)
  stack.root.addChild(copy)
}

| パラメータ | 型 | 説明 | |---|---|---| | layer | Layer | 複製元のレイヤー | | 戻り値 | Layer | 複製されたレイヤー(新規ID) |

型定義

LayerPanelProps

interface LayerPanelProps {
  /** momoi-layer の LayerStack インスタンス */
  stack: LayerStack
  /** 追加の CSS クラス */
  className?: string
  /** レイヤー選択時 */
  onSelect?: (layerId: number) => void
  /** 可視性トグル時 */
  onToggleVisible?: (layerId: number) => void
  /** ロックトグル時 */
  onToggleLock?: (layerId: number) => void
  /** アルファロックトグル時 */
  onToggleAlphaLock?: (layerId: number) => void
  /** クリッピングトグル時 */
  onToggleClipping?: (layerId: number) => void
  /** レイヤー移動時(DnD) */
  onMove?: (payload: LayerMovePayload) => void
  /** レイヤーリネーム時 */
  onRename?: (layerId: number, newName: string) => void
  /** ブレンドモード変更時 */
  onChangeBlendMode?: (layerId: number, mode: BlendMode) => void
  /** 不透明度変更時 (0-1) */
  onChangeOpacity?: (layerId: number, opacity: number) => void
  /** レイヤー複製時 */
  onDuplicate?: (layerId: number) => void
  /** レイヤー削除時 */
  onDelete?: (layerId: number) => void
}

LayerPanelHandle

interface LayerPanelHandle {
  /** LayerStack の現在の状態で UI を再描画する */
  sync: () => void
}

LayerSnapshot

interface LayerSnapshot {
  id: number
  name: string
  type: LayerType           // 'raster' | 'vector' | 'group'
  opacity: number           // 0-1
  visible: boolean
  blendMode: BlendMode
  locked: boolean
  alphaLock: boolean
  clipping: boolean
  depth: number             // ネストの深さ(0 = ルート直下)
  expanded: boolean         // グループの展開状態
  children: LayerSnapshot[] // グループの子レイヤー
  raw: Layer                // 元の Layer オブジェクトへの参照
}

LayerMovePayload

interface LayerMovePayload {
  layerId: number
  targetParent: GroupLayer  // 移動先の親グループ
  targetIndex: number       // 移動先のインデックス
}

LayerToolbarProps

interface LayerToolbarProps {
  activeLayerId: number | null
  onAddRaster?: () => void
  onAddVector?: () => void
  onAddGroup?: () => void
  onDelete?: () => void
  onDuplicate?: () => void
}

DropPosition / DropTarget

type DropPosition = 'before' | 'after' | 'inside'

interface DropTarget {
  layerId: number
  position: DropPosition
  /** after 判定時、カーソル X 座標から推定したインデントレベル */
  cursorDepth?: number
}

ContextMenuEntry / ContextMenuProps

interface ContextMenuItem {
  label: string
  action: () => void
  disabled?: boolean
}

interface ContextMenuSeparator {
  separator: true
}

type ContextMenuEntry = ContextMenuItem | ContextMenuSeparator

interface ContextMenuProps {
  x: number
  y: number
  items: ContextMenuEntry[]
  onClose: () => void
}

ライセンス

MIT