Pullboard
A work board that lives in your git repo. Agents pick up the next task, another agent checks it, and you barely explain a thing. Local, open source, no account.
翻譯準備中
尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證Panes is a tiny, dependency-free movable & resizable in-app modal panes for the web
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Tiny, dependency-free movable & resizable in-app modal panes for the web — written in TypeScript, unified mouse/touch/pen input via Pointer Events.
.d.ts typesrole="dialog" + aria-labelledby wiring, keyboard closerequestAnimationFrame-coalesced drag/resize, pointer capture, and proper teardownThe package is named
panes-uion npm (the bare namepaneswas taken). The JavaScript global exposed by the CDN/IIFE build is stillPanes.
panes-ui is a browser-only library.
| Requirement | Minimum |
|---|---|
| Runtime | Any modern browser with Pointer Events (Chrome 55+, Firefox 59+, Safari 13+, Edge 79+) |
| Node (build) | Node 16+ (only needed if you consume via a bundler) |
| TypeScript | 4.5+ (optional; JS works fine) |
There is no SSR runtime — construct panes from browser code (useEffect, onMount, etc. in frameworks).
npm install panes-ui
# or
pnpm add panes-ui
# or
yarn add panes-ui
This installs both the JavaScript and the types. Also import the stylesheet once:
import "panes-ui/style.css";
If 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">.
<div id="my-pane" style="width:360px;height:240px">
<div class="pane-title">Hello</div>
<button class="pane-close" aria-label="Close">×</button>
<div class="pane-content">
Drag my titlebar. Resize from any edge or corner. Touch works too.
</div>
</div>
import { Pane } from "panes-ui";
import "panes-ui/style.css";
const pane = new Pane("#my-pane", { title: "Hello" });
pane.open();
Or use the one-line shortcut:
import { open } from "panes-ui";
open("#my-pane", { title: "Hello" }); // lazy-creates + opens
<link rel="stylesheet" href="https://unpkg.com/panes-ui@1/dist/pane.css" />
<script src="https://unpkg.com/panes-ui@1"></script>
<script>
Panes.open("#my-pane", { title: "Hello" });
</script>
The IIFE build exposes everything on the Panes global (Panes.Pane, Panes.open, Panes.manager, …). For production, pin an exact version (e.g. [email protected]) and consider adding an integrity attribute. jsDelivr works too: https://cdn.jsdelivr.net/npm/panes-ui@1.
import { useEffect, useRef } from "react";
import { Pane } from "panes-ui";
import "panes-ui/style.css";
export function MyModal({ open }: { open: boolean }) {
const ref = useRef<HTMLDivElement>(null);
const paneRef = useRef<Pane | null>(null);
useEffect(() => {
if (!ref.current) return;
paneRef.current = new Pane(ref.current, { title: "Report" });
return () => paneRef.current?.destroy();
}, []);
useEffect(() => {
if (!paneRef.current) return;
open ? paneRef.current.open() : paneRef.current.close();
}, [open]);
return (
<div ref={ref} style={{ width: 420, height: 300 }}>
<div className="pane-title" />
<button className="pane-close" aria-label="Close">×</button>
<div className="pane-content">…</div>
</div>
);
}
<script setup lang="ts">
import { onMounted, onBeforeUnmount, ref } from "vue";
import { Pane } from "panes-ui";
import "panes-ui/style.css";
const root = ref<HTMLDivElement>();
let pane: Pane | null = null;
onMounted(() => { if (root.value) pane = new Pane(root.value, { title: "Report" }).open(); });
onBeforeUnmount(() => pane?.destroy());
</script>
<template>
<div ref="root" style="width:420px;height:300px">
<div class="pane-title" />
<button class="pane-close" aria-label="Close">×</button>
<div class="pane-content"><slot /></div>
</div>
</template>
<script lang="ts">
import { onMount } from "svelte";
import { Pane } from "panes-ui";
import "panes-ui/style.css";
let el: HTMLDivElement;
onMount(() => {
const pane = new Pane(el, { title: "Report" }).open();
return () => pane.destroy();
});
</script>
<div bind:this={el} style="width:420px;height:300px">
<div class="pane-title"></div>
<button class="pane-close" aria-label="Close">×</button>
<div class="pane-content"><slot /></div>
</div>
import { onCleanup, onMount } from "solid-js";
import { Pane } from "panes-ui";
import "panes-ui/style.css";
export function Report() {
let el!: HTMLDivElement;
onMount(() => {
const pane = new Pane(el, { title: "Report" }).open();
onCleanup(() => pane.destroy());
});
return (
<div ref={el} style={{ width: "420px", height: "300px" }}>
<div class="pane-title" />
<button class="pane-close" aria-label="Close">×</button>
<div class="pane-content">…</div>
</div>
);
}
Importing panes-ui on the server is safe — only constructing a Pane needs window/document, and it throws a clear error if you try it outside a browser. In the App Router, put the component in a "use client" file and create the pane in useEffect, as in the React example.
Any element can become a pane. Its optional children are discovered by selector:
| Selector | Role |
|---|---|
.pane-title | Drag handle. Click-and-drag here to move the pane. |
.pane-content | Scrollable body. Text is selectable. |
.pane-close | Element (usually a <button>) that closes it. |
Selectors are configurable per instance (see titleSelector, contentSelector, closeSelector).
The pane element must have a non-static position. Panes defaults to position: fixed if the element is statically positioned.
new Pane(target, options?, manager?)target — an Element, an element id, or a CSS selector.options — see below.manager — an optional PaneManager for isolated stacking contexts.interface PaneOptions {
title?: string | Element;
titleSelector?: string; // default ".pane-title"
contentSelector?: string; // default ".pane-content"
closeSelector?: string; // default ".pane-close"
movable?: boolean; // default true
resizable?: boolean; // default true
minWidth?: number; // default 120
minHeight?: number; // default 80
constrain?: boolean; // default true — keep inside viewport
center?: boolean; // default true — center on first open()
closeOnEscape?: boolean; // default true
onOpen?: (pane: Pane) => void;
onClose?: (pane: Pane) => void;
onMove?: (pane: Pane, left: number, top: number) => void;
onResize?: (pane: Pane, width: number, height: number) => void;
onFocus?: (pane: Pane) => void;
}
| Method | Purpose |
|---|---|
open() | Show the pane (centers on first open). |
close() | Hide the pane. |
toggle() | Open if hidden, close if visible. |
isOpen() | Whether the pane is currently visible. |
focus() | Raise above other panes. |
center() | Re-center in the viewport. |
setPosition(left, top) | Move the pane (respects constrain). |
setSize(width, height) | Resize the pane (respects min sizes). |
getRect() | Current viewport-relative geometry. |
destroy() | Detach listeners and unregister. Idempotent. |
Methods that change state (open, close, toggle, focus, center, setPosition, setSize) return the pane, so calls chain: pane.setSize(400, 300).center().open().
setPosition() takes coordinates in the pane's offset parent (the viewport, for the default position: fixed). getRect() returns viewport coordinates.
| Property | Type | Description |
|---|---|---|
el | HTMLElement | The pane root. |
title | HTMLElement | null | The element matched by titleSelector. |
content | HTMLElement | null | The element matched by contentSelector. |
closeBtn | HTMLElement | null | The element matched by closeSelector. |
opts | Readonly<…> | The resolved options (defaults filled in). |
| Export | Description |
|---|---|
open(target, options?) | Get-or-create a pane on the element and open it. The instance is cached on the element, so options only apply on the first call. |
manager | The default PaneManager singleton. |
PaneManager | Class for an isolated stacking context (see below). list() returns a snapshot of registered panes. |
version | The library version string. |
| Types | PaneOptions, PaneTarget, PaneCallback, PaneMoveCallback, PaneResizeCallback, ResizeEdge, InteractionMode. |
PaneManagerA singleton manager assigns z-index. Calling focus() (or any pointerdown on a pane) raises it to the top. For isolated stacking, construct your own:
import { Pane, PaneManager } from "panes-ui";
const myManager = new PaneManager();
const p = new Pane("#a", {}, myManager);
z-index values start at 1001 and are automatically rebased when they grow large, so long sessions don't drift upward. If other UI on your page (toasts, menus) needs to sit above panes, give it a z-index above 10000.
const KEY = "pane:inspector";
const saved = JSON.parse(localStorage.getItem(KEY) ?? "null");
const save = (p: Pane) => localStorage.setItem(KEY, JSON.stringify(p.getRect()));
const pane = new Pane("#inspector", {
center: !saved, // only center when there's nothing to restore
onMove: save,
onResize: save,
}).open(); // open first: a hidden pane measures 0×0
if (saved) pane.setSize(saved.width, saved.height).setPosition(saved.left, saved.top);
By default, every open pane with closeOnEscape closes when Escape is pressed. To close one at a time:
import { Pane, manager } from "panes-ui";
const a = new Pane("#a", { closeOnEscape: false });
const b = new Pane("#b", { closeOnEscape: false });
document.addEventListener("keydown", (e) => {
if (e.key !== "Escape") return;
const top = manager.list()
.filter((p) => p.isOpen())
.sort((x, y) => +y.el.style.zIndex - +x.el.style.zIndex)[0];
top?.close();
});
function spawn(title: string, body: Node) {
const el = document.createElement("div");
el.style.cssText = "width:320px;height:200px";
const t = el.appendChild(document.createElement("div"));
t.className = "pane-title";
const c = el.appendChild(document.createElement("div"));
c.className = "pane-content";
c.appendChild(body);
document.body.appendChild(el);
return new Pane(el, {
title,
onClose: (p) => { p.destroy(); el.remove(); }, // one-shot pane
}).open();
}
new Pane("#win", {
titleSelector: ".win-header",
contentSelector: ".win-body",
closeSelector: "[data-close]",
});
Panes are non-blocking (aria-modal="false"). For a blocking dialog, render a backdrop behind the pane, set aria-modal="true" on the element yourself (the library won't overwrite it), and trap focus inside — or use the native <dialog> element.
open() to show one; the first open() centers it (unless center: false) and fires onOpen.position: fixed, and its CSS margin is zeroed so left/top match the visual position. Geometry is written as inline left/top/width/height..pane-content is position: absolute, its width/height are kept in sync with the pane, mirroring its own top/left as bottom/right insets.onMove fires for every position change (drag frames, setPosition, center, window-resize re-clamping). onResize fires during resize gestures and on setSize. onOpen/onClose fire only on actual state changes.destroy() removes every listener the pane added (including on window/document), but leaves the element and its inline styles in place.Import panes-ui/style.css for the default look, or write your own — the library only reads geometry, never your colors.
Interactive state is exposed as classes on the root element:
| Class | When |
|---|---|
.pane | Always, after construction |
.pane--drag | During a drag |
.pane--resize | During a resize |
panes-ui sets the following on the root on construction (unless you've set them yourself):
role="dialog"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.tabindex="-1" — allows programmatic focusaria-labelledby — wired to the title element when one existsEscape key closes the pane (disable via closeOnEscape: false).
Chrome 55+, Firefox 59+, Safari 13+, Edge 79+ (anything with Pointer Events). No IE support.
git clone https://github.com/silicondaydream/panes.git
cd panes
npm install
npm run build # ESM + CJS + IIFE + .d.ts in dist/
npm run typecheck
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..pane-content, you are responsible for sanitizing it.Found a vulnerability? See SECURITY.md.
requestAnimationFrame coalescing — drag/resize stays smooth under load.destroy()) — no leaked listeners.MIT © Chris Adams