Duck Workbench
The editor shell: an icon tool rail, a pan-and-zoom canvas, an inspector that becomes an overlay when the container narrows, and a zoom read-out that costs no renders.
BlockClient componentlucide-react
Preview
sticker.duck
duck
Installation
One command writes the section and every component it renders.
$
pnpm dlx shadcn@latest add @duck/duck-workbenchThe block lands in components/blocks/duck-workbench.tsx. It builds on duck-viewport, duck-button-group, quack-button and hud-label, which the CLI installs alongside it. Already installed items are skipped.
Usage
import { DuckWorkbench } from "@/components/blocks/duck-workbench"A block is a starting point, not a widget. The props exist so the example renders with real content — once the file is in your project, hard-code what never changes and delete the rest.
Props
Everything not listed here is forwarded to the root element.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | React.ReactNode | - | Document name in the top bar. |
| tools | DuckWorkbenchTool[] | - | icon, label, active, disabled, onSelect. Rendered icon-only on the left edge as one toolbar, so the whole rail costs one tab stop. |
| actions | React.ReactNode | - | Top bar, right side: save, export, a theme switcher. |
| inspector | React.ReactNode | - | The right rail's rows. A slot, not a schema — the controls are the application's own, on the instrument scale. |
| inspectorLabel | string | "Inspector" | Names the rail and its toggle. |
| inspectorWidth | number | 288 | Rail width in px, once it is permanent. |
| status | React.ReactNode | - | Bottom strip, left side: selection, dimensions, a hint. |
| grid | boolean | true | Dot grid inside the transform, so the paper pans and scales with the artwork. |
| viewportProps | Pick<DuckViewportProps, "min" | "max" | "initial" | "zoomStep" | "panStep" | "wheelZoom"> | - | Passed straight to DuckViewport. initial.scale also seeds the zoom read-out's first paint. |
| viewportRef | React.RefObject<DuckViewportHandle | null> | - | Take the handle to drive zoom and pan from the application's own controls. |
| children | React.ReactNode | - | The artwork. This is what gets translated and scaled. |
Rules
What keeps this section from turning into noise.
- The zoom controls are a sibling of the viewport, never a child. A child sits inside the transform and pans away with the content.
- The read-out is written to the DOM, not rendered. Piping onTransformChange into setState re-renders the canvas sixty times a second to update two glyphs, which is exactly what DuckViewport avoids by writing the transform to element.style.
- It is also aria-live="off". A percentage that changes on every frame of a wheel gesture is noise, not an announcement.
- The rails are chrome, so they carry no holo. The shell is on screen all session; spend the budget inside the canvas.
- A tool is a state, not an action. The rail sets aria-pressed, which is the whole difference between draw and drawing.
