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

dock-libr

v0.1.13

Published

Custom dockable panel library for React

Readme

DockLibr

This project was built with the assistance of AI / large language models.

A customizable dockable panel library for React. Provides an IDE-like layout with draggable tab sets, splitters, collapsible panels, and drag-to-edge docking.

Repository: gitlab.com/dumsoft/docklibr

Install

npm install dock-libr

Requires react and react-dom as peer dependencies (v18 or v19).

Usage

import { DockLayout, DockModel } from 'dock-libr'
import 'dock-libr/styles/dock.css'

const model = DockModel.fromJson({
  layout: {
    type: 'row',
    children: [
      {
        type: 'tabset', id: 'left', weight: 30,
        children: [
          { type: 'tab', id: 'tab1', name: 'Explorer', component: 'explorer' },
          { type: 'tab', id: 'tab2', name: 'Search', component: 'search' },
        ],
      },
      {
        type: 'tabset', id: 'right', weight: 70,
        children: [
          { type: 'tab', id: 'tab3', name: 'Editor', component: 'editor' },
        ],
      },
    ],
  },
})

function factory(component: string) {
  switch (component) {
    case 'explorer': return <FileExplorer />
    case 'search':   return <SearchPanel />
    case 'editor':   return <CodeEditor />
  }
}

export function App() {
  return <DockLayout model={model} factory={factory} />
}

Examples

Nested layout (row inside column)

const model = DockModel.fromJson({
  layout: {
    type: 'column',
    children: [
      {
        type: 'tabset', id: 'top', weight: 30,
        children: [{ type: 'tab', id: 't1', name: 'Top', component: 'panel' }],
      },
      {
        type: 'row', weight: 70,
        children: [
          {
            type: 'tabset', id: 'left', weight: 50,
            children: [{ type: 'tab', id: 't2', name: 'Left', component: 'panel' }],
          },
          {
            type: 'tabset', id: 'right', weight: 50,
            children: [{ type: 'tab', id: 't3', name: 'Right', component: 'panel' }],
          },
        ],
      },
    ],
  },
})

Collapsed panel

{
  type: 'tabset', id: 'toolbox', weight: 15, collapsed: true,
  children: [{ type: 'tab', id: 'tools', name: 'Toolbox', component: 'toolbox' }],
}

A collapsed tabset shows only its header bar with a ▶ expand button. Clicking it expands the panel; clicking ▼ collapses it back.

Adding / removing tabs dynamically

const model = DockModel.fromJson({ ... })
const editorTabset = model.getTabSetById('editor-tabset')
if (editorTabset) {
  const tab = new TabNode('editor:vars', 'Variables', 'editor')
  editorTabset.addTab(tab)
  model.notifyListeners()
}

// Remove a tab
editorTabset.removeTab('editor:vars')
model.notifyListeners()

Listening for model changes

const unsubscribe = model.addListener(() => {
  // called whenever the layout changes (tab move, resize, collapse, etc.)
})
// later: unsubscribe()

Keeping an empty tabset visible

Set preventDelete: true on a tabset so it stays visible even when it has no tabs:

{
  type: 'tabset', id: 'editor', weight: 70, preventDelete: true,
  children: [],
}

Preventing resize / collapse / drag per tabset

{
  type: 'tabset', id: 'locked', weight: 30,
  disableResize: true,      // splitters adjacent to this tabset are disabled
  disableCollapse: true,    // collapse button hidden
  disableTabDrag: true,     // tabs cannot be dragged out
  disablePanelDrag: true,   // panel header cannot be dragged
  disableClose: true,       // close button hidden on all tabs
  children: [
    { type: 'tab', id: 't1', name: 'Locked', component: 'panel' },
  ],
}

Disable plain text drop

By default, dragging a tab sets only a custom MIME type (application/x-dock-tab-id) on the data transfer, so browsers won't auto-insert the tab name into text editors. Pass enableTextDrop to also set text/plain:

<DockLayout model={model} factory={factory} enableTextDrop />

Middle-click to close

Middle-clicking a tab closes it (respects enableClose and disableClose).

API

DockModel

Main model class. Holds the root layout node, manages actions, and notifies listeners.

| Method | Description | |--------|-------------| | DockModel.fromJson(json) | Create a model from a JSON layout definition | | model.getRoot() | Get the root RowNode or TabSetNode | | model.getTabSetById(id) | Find a tabset node by its string ID | | model.doAction(action) | Execute a layout action (move tab, toggle collapse, dock panel, etc.) | | model.addListener(fn) | Subscribe to layout changes. Returns an unsubscribe function | | model.notifyListeners() | Force a re-render by triggering all listeners |

RowNode / ColumnNode

Container nodes that lay out their children horizontally (row) or vertically (column).

| Method | Description | |--------|-------------| | node.getChildren() | Get child ContainerNode[] (tabsets, rows, or columns) | | node.getWeight() | Get the proportional size weight | | node.setWeight(n) | Resize the node's proportional weight | | node.getOrientation() | Returns 'horizontal' for rows, 'vertical' for columns |

TabSetNode

A tabbed container that holds multiple TabNode children.

| Method | Description | |--------|-------------| | tabset.getId() | Get the tabset's string ID | | tabset.getWeight() | Get the proportional size weight (returns 0 when collapsed) | | tabset.getCollapsed() / setCollapsed(bool) | Get or set collapsed state | | tabset.getChildren() | Get TabNode[] | | tabset.getActiveTabId() / setActiveTabId(id) | Get or set the active tab | | tabset.getActiveTab() | Get the active TabNode or null | | tabset.getTabById(id) | Find a tab by ID | | tabset.addTab(tab) | Append a tab | | tabset.removeTab(tabId) | Remove and return a tab | | tabset.moveTab(tabId, targetIndex) | Reorder a tab to a new index | | tabset.isEmpty() | Check if the tabset has no tabs |

Per-tabset flags (set via JSON or constructor, all default to false):

| Flag | Effect | |------|--------| | preventDelete | Tabset is not removed when its last tab is closed | | disableResize | Splitters adjacent to this tabset are disabled | | disableCollapse | Collapse button is hidden | | disableTabDrag | Tabs cannot be dragged individually | | disablePanelDrag | Panel header cannot be dragged | | disableClose | Close button hidden on all tabs in this tabset |

TabNode

A single tab inside a tabset.

| Method | Description | |--------|-------------| | tab.getId() | Get the tab's string ID | | tab.getName() / setName(name) | Get or set the display name | | tab.getComponent() | Get the component string (passed to the factory) | | tab.isEnableClose() / setEnableClose(bool) | Whether the tab can be closed by the user |

DockLayout

The React component that renders the layout.

| Prop | Type | Description | |------|------|-------------| | model | DockModel | The model instance | | factory | (component: string, nodeId: string) => ReactNode | Function that returns a React element for each component name | | onModelChange? | () => void | Optional callback fired on every model change | | enableTextDrop? | boolean | When true, also sets text/plain on drag data transfer (default false) |

useDockContext()

React hook (must be used inside a DockLayout) that returns:

  • model, factory
  • dragInfo, setDragInfo — drag state for panel drag-and-drop
  • dropZone, setDropZone — drop target zone during drag
  • rootDropZone, setRootDropZone — root edge zone during drag
  • hoveredTabSetId, setHoveredTabSetId — tracks which tabset the cursor is hovering during drag
  • enableTextDrop — whether text/plain is set on drag data transfer

Layout Action Types

Passed to model.doAction():

| Action | Data | Description | |--------|------|-------------| | MOVE_TAB | { tabId, fromTabSetId, toTabSetId } | Move a tab between tabsets | | TOGGLE_COLLAPSE | { tabSetId } | Toggle collapsed state of a tabset | | DOCK_PANEL | { tabId, sourceTabSetId, targetTabSetId, location, isPanelDrag } | Dock a tab to the edge of another tabset | | DOCK_PANEL_ROOT | { tabId, sourceTabSetId, location, isPanelDrag } | Dock a tab to the edge of the root layout | | DELETE_TABSET | { tabSetId } | Remove an empty tabset | | SET_ACTIVE_TAB | { tabSetId, tabId } | Set which tab is active in a tabset | | CLOSE_TAB | { tabId, tabSetId } | Close a tab (removes it and cleans up empty tabsets) | | LOAD_MODEL | { layout: IJsonModel } | Load a new layout |

JSON Model Interface

interface IJsonModel {
  layout: IJsonRow | IJsonTabSet
}

interface IJsonRow {
  type: 'row' | 'column'
  weight?: number
  children: (IJsonRow | IJsonTabSet)[]
}

interface IJsonTabSet {
  type: 'tabset'
  id: string
  weight?: number
  collapsed?: boolean
  preventDelete?: boolean
  disableResize?: boolean
  disableCollapse?: boolean
  disableTabDrag?: boolean
  disablePanelDrag?: boolean
  disableClose?: boolean
  activeTabId?: string
  children: IJsonTab[]
}

interface IJsonTab {
  type: 'tab'
  id: string
  name: string
  component: string
  enableClose?: boolean       // default true
}

Event Details

Drop zone locations (used in DOCK_PANEL and DOCK_PANEL_ROOT actions):

| Location | Description | |----------|-------------| | 'center' | Drop into the center (move tab into target tabset) | | 'top' | Dock above | | 'bottom' | Dock below | | 'left' | Dock to the left | | 'right' | Dock to the right |

Drag during drop: While dragging a tab, hovering over a tabset shows a 75×75px plus-shaped overlay at the center with 5 zones (center + 4 edges). At the root layout edges, 50×25px square indicators appear. All highlights turn blue when active.

Tab reorder: Dragging a tab within the same tabset shows a thin 2px blue vertical line indicating the insertion position.

Styling

Import the default stylesheet:

import 'dock-libr/styles/dock.css'

The CSS defines the following classes that you can override:

| Class | Purpose | |-------|---------| | .dock-layout | Root layout container (fills parent height) | | .dock-container-horizontal | Horizontal flex container for row children | | .dock-container-vertical | Vertical flex container for column children | | .dock-tabset | A tab set panel | | .dock-tabset-collapsed | Applied when a tabset is collapsed | | .dock-panel-header | The header bar of a tabset (tabs + collapse button) | | .dock-tab-bar | Row of tab buttons inside the header | | .dock-tab-button | Individual tab button | | .dock-tab-button-active | Active tab button | | .dock-tab-button-content | Tab button label text | | .dock-tab-close-btn | Close button on a tab | | .dock-tabset-content | Content area below the header | | .dock-tabset-content-empty | Empty content area (no active tab) | | .dock-splitter | Draggable splitter between panels | | .dock-splitter-horizontal | Horizontal splitter (between rows in a column) | | .dock-splitter-vertical | Vertical splitter (between columns in a row) | | .dock-drop-indicator | Drop zone indicator overlay | | .dock-root-edge-indicator | Root edge docking indicator | | .dock-root-edge-indicator-active | Active root edge indicator | | .dock-collapse-btn | Collapse/expand button | | .dock-collapse-btn-hidden | Hidden collapse button (when disableCollapse is true) |

Customizing theme

Override the default colors by targeting the classes in your own CSS:

/* Dark theme (default) */
.dock-panel-header { background: #1e1e1e; color: #ccc; }
.dock-tab-button-active { background: #2a4a7a; }
.dock-splitter { background: #3a3a3a; }

/* Light theme example */
.dock-panel-header { background: #f0f0f0; color: #222; }
.dock-tab-button-active { background: #e0e8f0; }
.dock-splitter { background: #ccc; }

Development & CI/CD

Branching

  • main: stable, published. Only accepts merges from dev via merge requests.
  • dev: active development. Push all changes here.

Release Pipeline

The repository includes a .gitlab-ci.yml pipeline that:

  1. On push to dev: builds the package to verify it compiles.
  2. On push to main (from a merged MR): builds, creates a GitLab Release with a v<version> tag, and publishes to npm.

No tokens or keys are stored in the repository. Publishing uses npm Trusted Publisher (OIDC) — no CI/CD variables are required.

See AGENTS.md for the full development workflow.

License

AGPL-3.0-only — see the LICENSE file for details.