{"_id":"panes-ui","name":"panes-ui","dist-tags":{"latest":"1.1.0"},"versions":{"1.1.0":{"name":"panes-ui","version":"1.1.0","description":"Tiny, dependency-free movable & resizable in-app modal panes with first-class touch support. Framework-agnostic. TypeScript.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","unpkg":"./dist/panes.global.js","jsdelivr":"./dist/panes.global.js","style":"./dist/pane.css","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./style.css":"./dist/pane.css","./pane.css":"./dist/pane.css","./package.json":"./package.json"},"sideEffects":["**/*.css"],"scripts":{"build":"tsup && npm run build:css","build:css":"node -e \"require('fs').copyFileSync('src/pane.css','dist/pane.css')\"","dev":"tsup --watch","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build","clean":"node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""},"keywords":["modal","dialog","draggable","resizable","movable","pane","panel","window","floating-window","floating-panel","popup","popover","overlay","touch","touch-friendly","pointer-events","mobile","ipad","pen","stylus","zero-dependency","no-dependencies","lightweight","tiny","typescript","esm","umd","cdn","framework-agnostic","react","vue","svelte","solid","angular","widget","desktop-ui","multi-window","window-manager","accessibility","a11y"],"author":{"name":"ChrisAdams","email":"hello@chrisadams.io"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/silicondaydream/panes.git"},"bugs":{"url":"https://github.com/silicondaydream/panes/issues"},"homepage":"https://github.com/silicondaydream/panes#readme","funding":"https://github.com/sponsors/silicondaydream","engines":{"node":">=16"},"devDependencies":{"tsup":"^8.0.0","typescript":"^5.4.0"},"publishConfig":{"access":"public"},"_id":"panes-ui@1.1.0","gitHead":"c954eb4ea2ac1bc59d014080e6b64c9b7ca98a7f","_nodeVersion":"24.8.0","_npmVersion":"11.6.0","dist":{"integrity":"sha512-pfSqMvPiAb+vwXM/u1loiqTnPll9HvwSxMbykZZVQAqpMWuV1DfhoOW/eWqfGwlI86Im2ghmuXyXwOewdWgUUA==","shasum":"a54fc606496a395da58353d42efb014d8c08b97c","tarball":"https://registry.npmjs.org/panes-ui/-/panes-ui-1.1.0.tgz","fileCount":15,"unpackedSize":179124,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIBJnnohbNta8REQ43xoumgLERqahH9Q3rQchsa7JGPxLAiEA3IbREyoviwgCScA5gX0V0fzsmFc+mycJits+pzpPiYk="}]},"_npmUser":{"name":"chrisadams.io","email":"accounts@chrisadams.io"},"directories":{},"maintainers":[{"name":"chrisadams.io","email":"accounts@chrisadams.io"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/panes-ui_1.1.0_1776878545124_0.059379112138877144"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-22T17:22:24.982Z","1.1.0":"2026-04-22T17:22:25.271Z","modified":"2026-04-22T17:22:25.464Z"},"maintainers":[{"name":"chrisadams.io","email":"accounts@chrisadams.io"}],"description":"Tiny, dependency-free movable & resizable in-app modal panes with first-class touch support. Framework-agnostic. TypeScript.","homepage":"https://github.com/silicondaydream/panes#readme","keywords":["modal","dialog","draggable","resizable","movable","pane","panel","window","floating-window","floating-panel","popup","popover","overlay","touch","touch-friendly","pointer-events","mobile","ipad","pen","stylus","zero-dependency","no-dependencies","lightweight","tiny","typescript","esm","umd","cdn","framework-agnostic","react","vue","svelte","solid","angular","widget","desktop-ui","multi-window","window-manager","accessibility","a11y"],"repository":{"type":"git","url":"git+https://github.com/silicondaydream/panes.git"},"author":{"name":"ChrisAdams","email":"hello@chrisadams.io"},"bugs":{"url":"https://github.com/silicondaydream/panes/issues"},"license":"MIT","readme":"# panes-ui\n\n> Tiny, dependency-free **movable & resizable in-app modal panes** for the web — written in TypeScript, unified mouse/touch/pen input via Pointer Events.\n\n[![npm](https://img.shields.io/npm/v/panes-ui.svg)](https://www.npmjs.com/package/panes-ui)\n[![types](https://img.shields.io/npm/types/panes-ui.svg)](https://www.npmjs.com/package/panes-ui)\n[![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n[![bundle size](https://img.shields.io/bundlephobia/minzip/panes-ui)](https://bundlephobia.com/package/panes-ui)\n\n- ~5 KB min+gzip, **zero dependencies**\n- Works with **mouse, touch, and pen** via [Pointer Events](https://developer.mozilla.org/docs/Web/API/Pointer_events)\n- Ships **ESM + CJS + IIFE** with `.d.ts` types\n- Framework-agnostic — drop-in for **plain HTML, React, Vue, Svelte, Solid, Angular**\n- Accessible: `role=\"dialog\"` + `aria-labelledby` wiring, keyboard close\n- `requestAnimationFrame`-coalesced drag/resize, pointer capture, and proper teardown\n\n---\n\n## Table of contents\n\n- [Prerequisites](#prerequisites)\n- [Install](#install)\n- [Quickstart](#quickstart)\n- [CDN (no build step)](#cdn-no-build-step)\n- [Framework examples](#framework-examples)\n- [DOM contract](#dom-contract)\n- [API](#api)\n- [Styling](#styling)\n- [Accessibility](#accessibility)\n- [Browser support](#browser-support)\n- [Building from source](#building-from-source)\n- [Security](#security)\n- [Changelog](CHANGELOG.md)\n- [License](#license)\n\n---\n\n> The package is named **`panes-ui`** on npm (the bare name `panes` was taken). The JavaScript global exposed by the CDN/IIFE build is still `Panes`.\n\n## Prerequisites\n\n`panes-ui` is a **browser-only** library.\n\n| Requirement  | Minimum                                                             |\n| ------------ | ------------------------------------------------------------------- |\n| Runtime      | Any modern browser with [Pointer Events](https://caniuse.com/pointer) (Chrome 55+, Firefox 59+, Safari 13+, Edge 79+) |\n| Node (build) | Node 16+ (only needed if you consume via a bundler)                 |\n| TypeScript   | 4.5+ (optional; JS works fine)                                      |\n\nThere is **no SSR runtime** — construct panes from browser code (`useEffect`, `onMount`, etc. in frameworks).\n\n---\n\n## Install\n\n```bash\nnpm install panes-ui\n# or\npnpm add panes-ui\n# or\nyarn add panes-ui\n```\n\nThis installs both the JavaScript and the types. Also import the stylesheet once:\n\n```ts\nimport \"panes-ui/style.css\";\n```\n\nIf your bundler doesn't handle CSS imports, copy `node_modules/panes-ui/dist/pane.css` into your static assets and link it with a `<link rel=\"stylesheet\">`.\n\n---\n\n## Quickstart\n\n### 1. Add markup\n\n```html\n<div id=\"my-pane\" style=\"width:360px;height:240px\">\n  <div class=\"pane-title\">Hello</div>\n  <button class=\"pane-close\" aria-label=\"Close\">&times;</button>\n  <div class=\"pane-content\">\n    Drag my titlebar. Resize from any edge or corner. Touch works too.\n  </div>\n</div>\n```\n\n### 2. Import and open\n\n```ts\nimport { Pane } from \"panes-ui\";\nimport \"panes-ui/style.css\";\n\nconst pane = new Pane(\"#my-pane\", { title: \"Hello\" });\npane.open();\n```\n\nOr use the one-line shortcut:\n\n```ts\nimport { open } from \"panes-ui\";\nopen(\"#my-pane\", { title: \"Hello\" }); // lazy-creates + opens\n```\n\n---\n\n## CDN (no build step)\n\n```html\n<link rel=\"stylesheet\" href=\"https://unpkg.com/panes-ui/dist/pane.css\" />\n<script src=\"https://unpkg.com/panes-ui\"></script>\n<script>\n  Panes.open(\"#my-pane\", { title: \"Hello\" });\n</script>\n```\n\nPin a version for production: `https://unpkg.com/panes-ui@1.1.0/...`.\n\n---\n\n## Framework examples\n\n### React\n\n```tsx\nimport { useEffect, useRef } from \"react\";\nimport { Pane } from \"panes-ui\";\nimport \"panes-ui/style.css\";\n\nexport function MyModal({ open }: { open: boolean }) {\n  const ref = useRef<HTMLDivElement>(null);\n  const paneRef = useRef<Pane | null>(null);\n\n  useEffect(() => {\n    if (!ref.current) return;\n    paneRef.current = new Pane(ref.current, { title: \"Report\" });\n    return () => paneRef.current?.destroy();\n  }, []);\n\n  useEffect(() => {\n    if (!paneRef.current) return;\n    open ? paneRef.current.open() : paneRef.current.close();\n  }, [open]);\n\n  return (\n    <div ref={ref} style={{ width: 420, height: 300 }}>\n      <div className=\"pane-title\" />\n      <button className=\"pane-close\" aria-label=\"Close\">×</button>\n      <div className=\"pane-content\">…</div>\n    </div>\n  );\n}\n```\n\n### Vue 3\n\n```vue\n<script setup lang=\"ts\">\nimport { onMounted, onBeforeUnmount, ref } from \"vue\";\nimport { Pane } from \"panes-ui\";\nimport \"panes-ui/style.css\";\n\nconst root = ref<HTMLDivElement>();\nlet pane: Pane | null = null;\n\nonMounted(() => { if (root.value) pane = new Pane(root.value, { title: \"Report\" }).open(); });\nonBeforeUnmount(() => pane?.destroy());\n</script>\n\n<template>\n  <div ref=\"root\" style=\"width:420px;height:300px\">\n    <div class=\"pane-title\" />\n    <button class=\"pane-close\" aria-label=\"Close\">×</button>\n    <div class=\"pane-content\"><slot /></div>\n  </div>\n</template>\n```\n\n### Svelte\n\n```svelte\n<script lang=\"ts\">\n  import { onMount, onDestroy } from \"svelte\";\n  import { Pane } from \"panes\";\n  import \"panes/style.css\";\n\n  let el: HTMLDivElement;\n  let pane: Pane;\n\n  onMount(() => { pane = new Pane(el, { title: \"Report\" }).open(); });\n  onDestroy(() => pane?.destroy());\n</script>\n\n<div bind:this={el} style=\"width:420px;height:300px\">\n  <div class=\"pane-title\" />\n  <button class=\"pane-close\" aria-label=\"Close\">×</button>\n  <div class=\"pane-content\"><slot /></div>\n</div>\n```\n\n---\n\n## DOM contract\n\nAny element can become a pane. Its optional children are discovered by selector:\n\n| Selector         | Role                                               |\n| ---------------- | -------------------------------------------------- |\n| `.pane-title`    | Drag handle. Click-and-drag here to move the pane. |\n| `.pane-content`  | Scrollable body. Text is selectable.               |\n| `.pane-close`    | Element (usually a `<button>`) that closes it.     |\n\nSelectors are configurable per instance (see `titleSelector`, `contentSelector`, `closeSelector`).\n\nThe pane element must have a non-static position. Panes defaults to `position: fixed` if the element is statically positioned.\n\n---\n\n## API\n\n### `new Pane(target, options?, manager?)`\n\n- `target` — an `Element`, an element `id`, or a CSS selector.\n- `options` — see below.\n- `manager` — an optional [`PaneManager`](#stacking--panemanager) for isolated stacking contexts.\n\n```ts\ninterface PaneOptions {\n  title?: string | Element;\n  titleSelector?: string;     // default \".pane-title\"\n  contentSelector?: string;   // default \".pane-content\"\n  closeSelector?: string;     // default \".pane-close\"\n  movable?: boolean;          // default true\n  resizable?: boolean;        // default true\n  minWidth?: number;          // default 120\n  minHeight?: number;         // default 80\n  constrain?: boolean;        // default true — keep inside viewport\n  center?: boolean;           // default true — center on first open()\n  closeOnEscape?: boolean;    // default true\n  onOpen?:   (pane: Pane) => void;\n  onClose?:  (pane: Pane) => void;\n  onMove?:   (pane: Pane, left: number, top: number) => void;\n  onResize?: (pane: Pane, width: number, height: number) => void;\n  onFocus?:  (pane: Pane) => void;\n}\n```\n\n### Methods\n\n| Method                            | Purpose                                          |\n| --------------------------------- | ------------------------------------------------ |\n| `open()`                          | Show the pane (centers on first open).           |\n| `close()`                         | Hide the pane.                                   |\n| `toggle()`                        | Open if hidden, close if visible.                |\n| `isOpen()`                        | Whether the pane is currently visible.           |\n| `focus()`                         | Raise above other panes.                         |\n| `center()`                        | Re-center in the viewport.                       |\n| `setPosition(left, top)`          | Move the pane (respects `constrain`).            |\n| `setSize(width, height)`          | Resize the pane (respects min sizes).            |\n| `getRect()`                       | Current viewport-relative geometry.              |\n| `destroy()`                       | Detach listeners and unregister. Idempotent.     |\n\n### Stacking — `PaneManager`\n\nA singleton `manager` assigns z-index. Calling `focus()` (or any pointerdown on a pane) raises it to the top. For isolated stacking, construct your own:\n\n```ts\nimport { Pane, PaneManager } from \"panes-ui\";\nconst myManager = new PaneManager();\nconst p = new Pane(\"#a\", {}, myManager);\n```\n\nz-index values are automatically rebased when they grow large, so long sessions don't drift upward.\n\n---\n\n## Styling\n\nImport `panes-ui/style.css` for the default look, or write your own — the library only reads geometry, never your colors.\n\nInteractive state is exposed as classes on the root element:\n\n| Class            | When                       |\n| ---------------- | -------------------------- |\n| `.pane`          | Always, after construction |\n| `.pane--drag`    | During a drag              |\n| `.pane--resize`  | During a resize            |\n\n---\n\n## Accessibility\n\n`panes-ui` sets the following on the root on construction (unless you've set them yourself):\n\n- `role=\"dialog\"`\n- `aria-modal=\"false\"` — panes are *non-blocking* in-app windows, not modal dialogs. If you want a true modal, implement a backdrop + focus trap above it.\n- `tabindex=\"-1\"` — allows programmatic focus\n- `aria-labelledby` — wired to the title element when one exists\n\nEscape key closes the pane (disable via `closeOnEscape: false`).\n\n---\n\n## Browser support\n\nChrome 55+, Firefox 59+, Safari 13+, Edge 79+ (anything with Pointer Events). No IE support.\n\n---\n\n## Building from source\n\n```bash\ngit clone https://github.com/silicondaydream/panes.git\ncd panes\nnpm install\nnpm run build      # ESM + CJS + IIFE + .d.ts in dist/\nnpm run typecheck\n```\n\n---\n\n## Security\n\n- `panes-ui` never executes user-supplied strings, never evaluates event-handler strings, and never injects HTML. `options.title` is set via `textContent` / `appendChild`, not `innerHTML`.\n- If you embed user-generated content inside `.pane-content`, **you** are responsible for sanitizing it.\n- The library does not make any network requests.\n\nFound a vulnerability? See [SECURITY.md](SECURITY.md).\n\n---\n\n## Why this library\n\n- **Pointer Events** — one unified code path for mouse, touch, and pen.\n- **`requestAnimationFrame` coalescing** — drag/resize stays smooth under load.\n- **Pointer capture** — no lost drags when the cursor leaves the pane.\n- **Viewport constraints on resize + window-resize reflow** — panes can't get stranded off-screen.\n- **Proper teardown** (`destroy()`) — no leaked listeners.\n- **Keyboard**: Escape closes; close button is focusable.\n- **Defensive errors**: descriptive throws on bad input.\n- **TypeScript** types out of the box.\n\n---\n\n## License\n\n[MIT](LICENSE) © Chris Adams\n","readmeFilename":"README.md","_rev":"1-21f4ddd38e7b76d4b1e6541ad1cd4367"}