Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build
run: npm run build
117 changes: 117 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
name: Release

on:
push:
branches: [main]
workflow_dispatch:
inputs:
release_type:
description: 'Release type (patch, minor, major)'
required: true
type: choice
options:
- patch
- minor
- major

permissions:
contents: write

jobs:
release:
runs-on: ubuntu-latest
# Skip release commits to avoid infinite loops
if: "!startsWith(github.event.head_commit.message, 'release:')"

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org

- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Install dependencies
run: npm ci

- name: Run tests
run: npm test

- name: Build
run: npm run build

- name: Determine version bump
id: bump
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "type=${{ inputs.release_type }}" >> "$GITHUB_OUTPUT"
exit 0
fi

# Get all commit messages since the last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -z "$LAST_TAG" ]; then
COMMITS=$(git log --pretty=format:"%s" --no-merges)
else
COMMITS=$(git log "${LAST_TAG}..HEAD" --pretty=format:"%s" --no-merges)
fi

echo "Commits since last release:"
echo "$COMMITS"

# Determine bump type from commit messages
# Conventional Commits: feat: = minor, fix: = patch, BREAKING CHANGE = major
BUMP="none"

if echo "$COMMITS" | grep -qiE "BREAKING CHANGE|^[a-z]+(\(.+\))?!:"; then
BUMP="major"
elif echo "$COMMITS" | grep -qE "^feat(\(.+\))?:"; then
BUMP="minor"
elif echo "$COMMITS" | grep -qE "^fix(\(.+\))?:"; then
BUMP="patch"
fi

echo "type=$BUMP" >> "$GITHUB_OUTPUT"

- name: Bump version
id: version
if: steps.bump.outputs.type != 'none'
run: |
npm version ${{ steps.bump.outputs.type }} --no-git-tag-version
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"

- name: Commit and tag
if: steps.bump.outputs.type != 'none'
run: |
git add package.json package-lock.json
git commit -m "release: v${{ steps.version.outputs.version }}"
git tag "v${{ steps.version.outputs.version }}"
git push origin main --tags

- name: Publish to npm
if: steps.bump.outputs.type != 'none'
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create GitHub Release
if: steps.bump.outputs.type != 'none'
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.version.outputs.version }}
name: v${{ steps.version.outputs.version }}
generate_release_notes: true

- name: Skip release
if: steps.bump.outputs.type == 'none'
run: echo "No release-triggering commits found (feat:/fix:/BREAKING CHANGE). Skipping release."
124 changes: 106 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,48 @@
<div align="center">

# react-driftkit

A lightweight, draggable floating widget wrapper for React — snap to corners or drag anywhere.
**A lightweight, draggable floating widget wrapper for React.**
Snap to corners, drag anywhere, stay in bounds — all under 3KB.

[![npm version](https://img.shields.io/npm/v/react-driftkit)](https://www.npmjs.com/package/react-driftkit)
[![npm downloads](https://img.shields.io/npm/dm/react-driftkit)](https://www.npmjs.com/package/react-driftkit)
[![bundle size](https://img.shields.io/bundlephobia/minzip/react-driftkit)](https://bundlephobia.com/package/react-driftkit)
[![license](https://img.shields.io/npm/l/react-driftkit)](./LICENSE)

[Live Demo](https://react-driftkit.saktichourasia.dev/) · [NPM](https://www.npmjs.com/package/react-driftkit) · [GitHub](https://github.com/shakcho/react-drift)

</div>

---

## Why react-driftkit?

Building a chat widget, floating toolbar, or debug panel? You need it to be draggable, stay on screen, and not fight your existing styles. Most draggable libraries are either too heavy, too opinionated, or don't handle edge cases like viewport resizing and touch input.

**react-driftkit** solves exactly this — one component, zero config, works everywhere.

## Features

- **Drag anywhere** — smooth pointer-based dragging (mouse, touch, pen)
- **Snap to corners** — optional snapping to the nearest viewport corner on release
- **Flexible positioning** — named corners or custom `{ x, y }` coordinates
- **Viewport-aware** — auto-repositions on window resize, stays within bounds
- **Zero styling opinions** — fully customizable via `className` and `style` props
- **Tiny footprint** — under 3KB gzipped, zero dependencies beyond React
- **Drag anywhere** — smooth pointer-based dragging with mouse, touch, and pen support
- **Snap to corners** — optional bounce-animated snapping to the nearest viewport corner
- **Smart positioning** — named corners (`top-left`, `bottom-right`, ...) or custom `{ x, y }` coordinates
- **Viewport-aware** — auto-repositions on window resize and content size changes
- **5px drag threshold** — distinguishes clicks from drags, so child buttons still work
- **Zero dependencies** — only React as a peer dependency
- **Tiny bundle** — under 3KB gzipped
- **TypeScript-first** — fully typed props and exports
- **Works with React 18 and 19**

## Installation

```bash
npm install react-driftkit
```

<details>
<summary>yarn / pnpm / bun</summary>

```bash
yarn add react-driftkit
```
Expand All @@ -28,6 +51,12 @@ yarn add react-driftkit
pnpm add react-driftkit
```

```bash
bun add react-driftkit
```

</details>

## Quick Start

```tsx
Expand All @@ -36,37 +65,66 @@ import { MovableLauncher } from 'react-driftkit';
function App() {
return (
<MovableLauncher defaultPosition="bottom-right">
<button>💬 Chat</button>
<button>Chat with us</button>
</MovableLauncher>
);
}
```

That's it. Your button is now a draggable floating widget pinned to the bottom-right corner.

## Examples

### Snap to Corners

Release the widget and it bounces to the nearest corner:

```tsx
<MovableLauncher defaultPosition="bottom-right" snapToCorners>
<div className="my-widget">Drag me!</div>
</MovableLauncher>
```

### Custom Position
### Free Positioning

Place the widget at exact coordinates:

```tsx
<MovableLauncher defaultPosition={{ x: 100, y: 200 }}>
<div className="toolbar">Toolbar</div>
</MovableLauncher>
```

## API
### Styled Widget

Combine with your own styles and classes:

```tsx
<MovableLauncher
defaultPosition="top-right"
snapToCorners
className="my-launcher"
style={{ borderRadius: 12, boxShadow: '0 4px 20px rgba(0,0,0,0.15)' }}
>
<div className="floating-panel">
<h3>Quick Actions</h3>
<button>New Task</button>
<button>Settings</button>
</div>
</MovableLauncher>
```

## API Reference

### `<MovableLauncher>`

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `children` | `ReactNode` | | Content to render inside the draggable wrapper |
| `defaultPosition` | `Corner \| { x: number, y: number }` | `'bottom-right'` | Initial position of the widget |
| `snapToCorners` | `boolean` | `false` | Snap to the nearest corner when released |
| `style` | `CSSProperties` | `{}` | Inline styles for the wrapper |
| `className` | `string` | `''` | CSS class for the wrapper |
| `children` | `ReactNode` | *required* | Content to render inside the draggable wrapper |
| `defaultPosition` | `Corner \| { x, y }` | `'bottom-right'` | Initial position — a named corner or pixel coordinates |
| `snapToCorners` | `boolean` | `false` | Snap to the nearest viewport corner on release |
| `style` | `CSSProperties` | `{}` | Inline styles merged with the wrapper |
| `className` | `string` | `''` | CSS class added to the wrapper |

### Types

Expand All @@ -79,10 +137,40 @@ interface Position {
}
```

## Requirements
### CSS Classes

The wrapper element exposes these classes for styling:

| Class | When |
|-------|------|
| `movable-launcher` | Always present |
| `movable-launcher--dragging` | While the user is actively dragging |

- React 18+ or 19+
## Use Cases

- **Chat widgets** — floating support/chat buttons that stay accessible
- **Floating toolbars** — draggable formatting bars or quick-action panels
- **Debug panels** — dev tools overlays that can be moved out of the way
- **Media controls** — picture-in-picture style video or audio controls
- **Notification centers** — persistent notification panels users can reposition
- **Accessibility helpers** — movable assistive overlays

## How It Works

Under the hood, react-driftkit uses the [Pointer Events API](https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events) for universal input handling and a `ResizeObserver` to keep the widget positioned correctly when its content changes size. The widget renders as a `position: fixed` div at the highest possible z-index (`2147483647`), so it floats above everything without interfering with your layout.

## Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request.

```bash
git clone https://github.com/shakcho/react-drift.git
cd react-drift
npm install
npm run dev # Start the demo app
npm test # Run the test suite
```

## License

MIT © Sakti Kumar Chourasia
MIT © [Sakti Kumar Chourasia](https://github.com/shakcho)
Loading
Loading