KQ-2105 · TRAY/WINDOWS

Tray Windows

A workspace of floating windows where any record can be parked in a tray: minimising sends a ghost of the window — title bar and body lines — flying into its chip's slot on springs.glide, position, size and corner radius from one value, and the chip lands with a small drop on springs.recoil; restoring flies it back to exactly where the window was and returns focus to it. Chips keep an unsaved marker and ask before discarding, and a full tray puts the rest behind a +N menu. Cmd or Ctrl+M minimises the focused window and Cmd or Ctrl with a number restores that chip.

KQ-2105 · TRAY/WINDOWS

Support desk

3 open · 2 parked
Re: Refund for order 3195
To Ines Duarte · Coldbrook Café
Unsaved changes
Invoice INV-2048 shows the wrong depot
Ada Mensah · Basinworks LogisticsWaiting on us
The invoice says Hull but the goods went to Wakefield. Our finance team can’t pay it until the address matches.
Nothing waiting to send
Stock update for order 3201
To Tomas Berg · Gaugeworks Depot
Saved

3 open · 2 parked · 1 unsaved

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/tray-windows

Props

records
TrayRecord[]
default defaultTrayRecords
Every record with a window: drafts (an editable body) and tickets (the last message and a reply).
tray / defaultTray
string[]
default — / ["t-2210", "t-2219"]
The ids parked in the tray, in the order they were parked. Controlled or uncontrolled.
onTrayChange
(ids: string[]) => void
default —
Fires with the whole tray after a minimise, a restore or a close.
unsaved / defaultUnsaved
string[]
default — / ["d-3195"]
The ids with unsaved changes. Controlled or uncontrolled.
onUnsavedChange
(ids: string[]) => void
default —
Fires when typing or saving changes which ids are unsaved.
positions
Record<string, { x, y, w, h }>
default defaultTrayPositions
Where each window sits, as fractions of the workspace; a restore flies back here.
onMinimise
(id: string) => void
default —
A window was minimised into the tray.
onRestore
(id: string) => void
default —
A chip was restored to its window.
onCloseItem
(id: string) => void
default —
A window or a chip was closed, after the unsaved check.
onSave
(id: string, body: string) => void
default —
Save (a draft) or Send (a ticket reply) was pressed, with the text.
chipSize
"sm" | "md" | "lg"
default "md"
Chip length and height; large adds a meta line.
trayPosition
"bottom" | "floating" | "side"
default "bottom"
A bar docked along the bottom edge, a dock floating above it, or a column on the right.
accent
string
default "var(--accent-bright)"
Unsaved markers, focus and the primary buttons — any CSS colour.
trayLimit
number
default 6
The most chips shown before the rest go behind the overflow menu (fewer when the tray is shorter).
status
"ready" | "loading" | "error"
default "ready"
Whether the records have arrived.
onRetry
() => void
default —
Try again was pressed after the records failed to load.
workspace
string
default "Fernworks"
The workspace's name.
title
string
default "Support desk"
The frame's title.
height
number
default 520
The frame's height, px.
className
string
default —
Classes for the outer frame.