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
37 changes: 2 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ Building a chat widget, floating toolbar, debug panel, or side dock? You want th
| [`<SnapDock>`](#snapdock) | An edge-pinned dock that slides along any side of the viewport and flips orientation automatically between horizontal and vertical. |
| [`<DraggableSheet>`](#draggablesheet) | A pull-up / pull-down sheet pinned to an edge with named snap points (`peek`, `half`, `full`) or arbitrary pixel / percentage stops. |
| [`<ResizableSplitPane>`](#resizablesplitter) | An N-pane resizable split layout with draggable handles, min/max constraints, and localStorage-persisted ratios. |
| [`<InspectorBubble>`](#inspectorbubble) | A Chrome-DevTools-style element picker overlay for design QA — hover to see tag, selector, dimensions, font, colors + WCAG contrast, box model, ARIA role, and accessible name. |
| [`<ZoomLens>`](#zoomlens) | A draggable magnifier circle that zooms into whatever it hovers — free-drag over the whole page or scope it to one element (product-image-zoom style). Wheel to zoom, hotkey or Escape to dismiss. |
| [`<FlickDeck>`](#flickdeck) | A stack of cards where each back card peeks from one edge. Click the peek to flick that card to the front, or optionally swipe the front card off to dismiss it. |

Expand Down Expand Up @@ -63,7 +62,7 @@ Every component is available two ways — both tree-shake to the same code.

```tsx
// 1. Barrel import — grab multiple components at once
import { MovableLauncher, SnapDock, InspectorBubble } from 'react-driftkit';
import { MovableLauncher, SnapDock, ZoomLens } from 'react-driftkit';

// 2. Per-component subpath — named or default, pick the style you prefer
import { SnapDock } from 'react-driftkit/SnapDock';
Expand All @@ -81,7 +80,6 @@ import {
SnapDock,
DraggableSheet,
ResizableSplitPane,
InspectorBubble,
ZoomLens,
FlickDeck,
} from 'react-driftkit';
Expand Down Expand Up @@ -115,9 +113,6 @@ function App() {
<MainContent />
</ResizableSplitPane>

{/* Chrome-DevTools-style element picker — press ⌘⇧C to inspect. */}
<InspectorBubble behavior={{ hotkey: 'cmd+shift+c' }} />

{/* Magnifier scoped to one element — hover to zoom. */}
<img ref={productRef} src="/product.jpg" alt="Product" />
<ZoomLens defaultActive target={productRef} defaultZoom={3} />
Expand Down Expand Up @@ -184,7 +179,7 @@ import SnapDock from 'react-driftkit/SnapDock';

## DraggableSheet

A pull-up / pull-down sheet pinned to an edge, with snap points like `peek`, `half`, and `full`. Built for mobile detail drawers, filter panels, and inspector flyouts — but works at any edge on any screen size.
A pull-up / pull-down sheet pinned to an edge, with snap points like `peek`, `half`, and `full`. Built for mobile detail drawers, filter panels, and dev-tool flyouts — but works at any edge on any screen size.

- **Named presets + raw values** — mix `'peek'`, `'half'`, `'full'`, pixel numbers, and `'40%'` strings in one `snapPoints` list
- **Any edge** — `bottom` (default), `top`, `left`, or `right`; percentages resolve against the drag axis
Expand Down Expand Up @@ -226,29 +221,6 @@ import ResizableSplitPane from 'react-driftkit/ResizableSplitPane';

---

## InspectorBubble

A Chrome-DevTools-style element picker overlay for design QA. Turn it on, hover any DOM element, and the picker draws a highlight plus an info bubble showing tag, short CSS selector, dimensions, typography, effective colors with WCAG contrast, padding/margin, ARIA role, accessible name, and a11y state flags. Click to select; Escape or a hotkey to exit.

- **DevTools-style box model** — 4-layer margin / border / padding / content overlay, or a single outline
- **Rich info bubble** with tag, selector, dimensions, font + rendered family, WCAG contrast, spacing, ARIA role, accessible name, and a11y state
- **Custom render** — take over the bubble with `bubble.render` and use the full `ElementInfo`
- **Hotkey toggle**, ignore rules, and self-skipping overlay chrome

```tsx
import InspectorBubble from 'react-driftkit/InspectorBubble';

<InspectorBubble
defaultActive
behavior={{ hotkey: 'cmd+shift+c' }}
on={{ select: (el, info) => console.log(info) }}
/>
```

**Full API, more examples, and live demo →** <https://react-driftkit.saktichourasia.dev/inspector-bubble>

---

## ZoomLens

A draggable magnifier circle that zooms into whatever it hovers. Great for design review, image inspection, or dense data tables. Drag it anywhere on the page, scroll to change zoom, or pass a `target` to scope it to a single element (product-image-zoom style — hover the element to magnify, leave to hide).
Expand Down Expand Up @@ -303,15 +275,12 @@ import FlickDeck from 'react-driftkit/FlickDeck';
- **Floating toolbars** — draggable formatting bars or quick-action panels
- **Side docks** — VS Code / Figma-style side rails that snap to any edge
- **Mobile detail sheets** — pull-up drawers for details, filters, or carts
- **Inspector panels** — developer tool drawers that expand between peek and full
- **Code editors** — resizable file tree + editor + preview split layouts
- **Admin dashboards** — adjustable sidebar and content regions
- **Debug panels** — dev tool 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
- **Design QA tooling** — hover-inspect contrast, typography, spacing, ARIA role, and accessible name on any element
- **In-house devtools** — a built-in element picker for style audits, a11y audits, or click-to-log workflows
- **Product image zoom** — magnifier scoped to a single image, follows the cursor, hides on leave
- **Data table inspection** — drag a magnifier over dense tables, charts, or heatmaps to read small values without re-flowing the page
- **Tip / onboarding stacks** — a deck of coachmark cards the user can flick through and swipe off one by one
Expand All @@ -325,8 +294,6 @@ Under the hood all components use the [Pointer Events API](https://developer.moz

`ResizableSplitPane` uses a flexbox layout with `calc()` sizing. Dragging a handle only redistributes space between the two adjacent panes, leaving all others unchanged. Window resize events trigger re-clamping against min/max constraints.

`InspectorBubble` renders its overlay into `document.body` via a React portal. Pointer tracking uses `document.elementFromPoint` and skips anything with `pointer-events: none` — so the box-model layers, outline, and bubble never block hit-testing. Computed values come from `getComputedStyle`; WCAG contrast is computed from the element's own `color` and the first non-transparent `background-color` walking up its ancestors. The "rendered font" is the first entry from the declared `font-family` list that `document.fonts.check()` reports as loaded — the same heuristic tools like WhatFont use.

`ZoomLens` live-clones either `document.body` (free mode) or a target element (target mode) into a portalled host, then applies a `translate()` + `scale()` transform so the point under the lens center maps to target-local — or document — coords. A `MutationObserver` rebuilds the clone when the real DOM changes, debounced to 150 ms and skipped during drag. In target mode, pointer tracking attaches directly to the target, and the lens overlay is `pointer-events: none` so hover state keeps passing through to the real element underneath.

`FlickDeck` lays every card in a single CSS grid cell so the container auto-sizes to the largest card, then offsets back cards with pure `transform` — `translate` along the peek axis, plus `scale` (top/bottom peek) or `rotate` around the attached edge (left/right peek). Depth fade uses `opacity`. The front card's `key` is its id; a click or keyboard activation on a peek swaps ids with a CSS transition. Swipe-to-dismiss tracks pointer movement on the front card and fires `on.dismiss(id)` once the drag crosses `dismissThreshold` in the direction opposite the peek, leaving the consumer to remove that child.
Expand Down
Binary file removed demo/public/og/inspector-bubble.png
Binary file not shown.
95 changes: 10 additions & 85 deletions demo/scripts/gen-og.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ const COLORS = {
muted: '#94a3b8',
accent: '#818cf8',
accentMuted: 'rgba(129,140,248,0.25)',
layerMargin: 'rgba(249,168,37,0.45)',
layerBorder: 'rgba(252,211,77,0.55)',
layerPadding: 'rgba(134,239,172,0.55)',
layerContent: 'rgba(147,197,253,0.60)',
};

/* ── illustrations ───────────────────────────────────────────────────── */
Expand Down Expand Up @@ -195,57 +191,6 @@ const illustrations = {
),
),

inspector: () => {
const layer = (color, label, inset) =>
box(
{
position: 'absolute',
top: inset,
left: inset,
right: inset,
bottom: inset,
border: `2px dashed ${color}`,
borderRadius: 8,
display: 'flex',
alignItems: 'flex-start',
justifyContent: 'flex-start',
padding: 6,
color,
fontSize: 13,
fontWeight: 700,
letterSpacing: '0.06em',
textTransform: 'uppercase',
},
label,
);
return viewport(
box(
{ width: '100%', height: '100%', position: 'relative', display: 'flex' },
layer(COLORS.layerMargin, 'margin', 20),
layer(COLORS.layerBorder, 'border', 50),
layer(COLORS.layerPadding, 'padding', 80),
box(
{
position: 'absolute',
top: 110,
left: 110,
right: 110,
bottom: 110,
background: COLORS.layerContent,
borderRadius: 6,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#0b1020',
fontSize: 14,
fontWeight: 700,
},
'content',
),
),
);
},

zoomlens: () => {
// Geometric illustration in the same flat style as the other OG cards:
// a rainbow grid of small tiles in the background, with a circular
Expand Down Expand Up @@ -376,7 +321,7 @@ const illustrations = {
box(
{ gap: 14, flex: 1 },
miniTile('Splitter', '#f9a8d4', miniSplitter()),
miniTile('Inspector', '#7dd3fc', miniInspector()),
miniTile('ZoomLens', '#7dd3fc', miniZoomLens()),
miniTile('+ more', COLORS.muted, miniWordmark(), true),
),
),
Expand Down Expand Up @@ -468,30 +413,16 @@ function miniSplitter() {
box({ width: '25%', background: 'rgba(34,197,94,0.25)' }),
);
}
function miniInspector() {
const layer = (color, inset) =>
box({
position: 'absolute',
top: inset,
left: inset,
right: inset,
bottom: inset,
border: `1px dashed ${color}`,
borderRadius: 3,
});
function miniZoomLens() {
return box(
{ width: '100%', height: '100%', position: 'relative' },
layer(COLORS.layerMargin, 2),
layer(COLORS.layerBorder, 8),
layer(COLORS.layerPadding, 14),
{ width: '100%', height: '100%', position: 'relative', alignItems: 'center', justifyContent: 'center' },
box({
position: 'absolute',
top: 20,
left: 20,
right: 20,
bottom: 20,
background: COLORS.layerContent,
borderRadius: 2,
width: 28,
height: 28,
borderRadius: 999,
border: `2px solid ${COLORS.accent}`,
background: 'rgba(129,140,248,0.18)',
boxShadow: '0 4px 12px rgba(0,0,0,0.35)',
}),
);
}
Expand Down Expand Up @@ -614,7 +545,7 @@ const pages = [
slug: 'home',
kind: 'home',
title: 'Floating UI primitives for React',
tagline: 'Draggable launchers, docks, sheets, split panes, a DevTools-style inspector, and a zoom lens. Tree-shakable and unstyled.',
tagline: 'Draggable launchers, docks, sheets, split panes, a zoom lens, and a flick deck. Tree-shakable and unstyled.',
},
{
slug: 'movable-launcher',
Expand All @@ -640,12 +571,6 @@ const pages = [
title: 'ResizableSplitPane',
tagline: 'An N-pane resizable layout with draggable handles, min/max constraints, and localStorage persistence.',
},
{
slug: 'inspector-bubble',
kind: 'inspector',
title: 'InspectorBubble',
tagline: 'A Chrome-DevTools-style element picker. Tag, selector, font, WCAG contrast, ARIA role and accessible name.',
},
{
slug: 'zoom-lens',
kind: 'zoomlens',
Expand Down
2 changes: 1 addition & 1 deletion demo/src/components/Nav.astro
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import ComponentDropdown from './islands/ComponentDropdown';

export interface Props {
activeComponent?: 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector' | 'zoomlens' | 'flickdeck' | null;
activeComponent?: 'launcher' | 'dock' | 'sheet' | 'splitter' | 'zoomlens' | 'flickdeck' | null;
}

const { activeComponent = null } = Astro.props;
Expand Down
5 changes: 2 additions & 3 deletions demo/src/components/islands/ComponentDropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import { MoveIcon, DockGlyphIcon, SheetIcon, SplitterIcon, InspectorIcon, ZoomLensIcon, FlickDeckIcon, ChevronDownIcon } from './Icons';
import { MoveIcon, DockGlyphIcon, SheetIcon, SplitterIcon, ZoomLensIcon, FlickDeckIcon, ChevronDownIcon } from './Icons';

export type ComponentKey = 'launcher' | 'dock' | 'sheet' | 'splitter' | 'inspector' | 'zoomlens' | 'flickdeck';
export type ComponentKey = 'launcher' | 'dock' | 'sheet' | 'splitter' | 'zoomlens' | 'flickdeck';

const items: { key: ComponentKey; label: string; href: string; icon: ReactNode }[] = [
{ key: 'inspector', label: 'InspectorBubble', href: '/inspector-bubble', icon: <InspectorIcon size={16} /> },
{ key: 'zoomlens', label: 'ZoomLens', href: '/zoom-lens', icon: <ZoomLensIcon size={16} /> },
{ key: 'flickdeck', label: 'FlickDeck', href: '/flick-deck', icon: <FlickDeckIcon size={16} /> },
{ key: 'launcher', label: 'MovableLauncher', href: '/movable-launcher', icon: <MoveIcon size={16} strokeWidth={2.2} /> },
Expand Down
10 changes: 0 additions & 10 deletions demo/src/components/islands/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,6 @@ export function SplitterIcon({ size = 16, strokeWidth = 2 }: IconProps) {
);
}

export function InspectorIcon({ size = 16, strokeWidth = 2 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 40 40" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="6" y="6" width="20" height="20" rx="2" opacity="0.35" />
<path d="M22 22l5 5 3-2-5-5z" />
<path d="M16 10v2M10 16h2" />
</svg>
);
}

export function ZoomLensIcon({ size = 16, strokeWidth = 2 }: IconProps) {
return (
<svg width={size} height={size} viewBox="0 0 40 40" fill="none" stroke="currentColor" strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
Expand Down
Loading
Loading