window-set

Describe a desk's windows, layouts, and geometry as data, then open and close them through your shell's adapter. The controller tracks the windows it opens; your application owns native integration and persistence.

import { useEffect, useRef, useState } from "react"
import { createWindowSet, windowSetOf, type WindowAdapter, type WindowRecord, type WindowSet, type WindowSetController } from "@/lib/window-set"

const DESK: WindowSet = windowSetOf([
  { id: "blotters", layoutId: "blotters", bounds: { x: 1600, y: 0, width: 800, height: 1000 } },
  { id: "main", layoutId: "desk", bounds: { x: 0, y: 0, width: 1600, height: 1000 }, main: true },
  { id: "charts", layoutId: "charts", bounds: { x: 0, y: 1000, width: 2400, height: 600 }, display: "second" },
])

interface Card {
  id: string
  url: string
  record: WindowRecord
}

export default function WindowSetDemo() {
  const [cards, setCards] = useState<Card[]>([])
  const [snapshot, setSnapshot] = useState("")
  const [shell] = useState(() => {
    const closed = new Set<(id: string) => void>()
    const adapter: WindowAdapter = {
      open: (id, url, record) => setCards((list) => list.some((card) => card.id === id) ? list : [...list, { id, url, record }]),
      close: (id) => setCards((list) => list.filter((card) => card.id !== id)),
      onClosed: (cb) => {
        closed.add(cb)
        return () => closed.delete(cb)
      },
      bounds: (id) => {
        const record = DESK.windows.find((w) => w.id === id)
        return record?.bounds ? { ...record.bounds, x: record.bounds.x + 24, y: record.bounds.y + 24 } : null
      },
    }
    const closeFromShell = (id: string) => {
      setCards((list) => list.filter((card) => card.id !== id))
      for (const cb of closed) cb(id)
    }
    return { adapter, closeFromShell }
  })
  const windows = useRef<WindowSetController | null>(null)
  useEffect(() => {
    const controller = createWindowSet(shell.adapter, { url: (w) => `tradecn://desk/?window=${w.id}&layout=${w.layoutId}` })
    windows.current = controller
    return () => {
      windows.current = null
      controller.dispose()
    }
  }, [shell])
  return (
    <>
      <div data-demo-controls className="flex flex-wrap gap-2 text-xs">
        <button type="button" className="rounded-md border border-border px-2 py-1" onClick={() => void windows.current?.restore(DESK)}>Restore the desk</button>
        <button type="button" className="rounded-md border border-border px-2 py-1" onClick={() => void windows.current?.closeAll()}>Close all</button>
        <button type="button" className="rounded-md border border-border px-2 py-1" onClick={() => void windows.current?.snapshot().then((set) => setSnapshot(JSON.stringify(set, null, 2)))}>Snapshot</button>
      </div>
      <div className="w-2xl max-w-full space-y-3 text-xs lining-nums tabular-nums">
        <p className="text-muted-foreground">A simulated shell: restore three windows, close a card, then take a snapshot. No native windows open.</p>
        {cards.length > 0 ? (
          <ul role="list" className="grid grid-cols-1 gap-2 sm:grid-cols-3">
            {cards.map((card) => (
              <li key={card.id} data-window={card.id} className="flex min-w-0 flex-col gap-1 rounded-md border border-border bg-card p-2">
                <div className="flex items-center justify-between gap-2">
                  <span className="font-medium">{card.id}{card.record.main && <span className="ml-1 text-muted-foreground"> (main)</span>}</span>
                  <button type="button" aria-label={`Close ${card.id} from the shell`} className="px-1 text-muted-foreground hover:text-foreground" onClick={() => shell.closeFromShell(card.id)}>×</button>
                </div>
                <span className="break-all font-(family-name:--tradecn-font-mono) text-muted-foreground">{card.url}</span>
                <span className="text-muted-foreground">layout {card.record.layoutId}</span>
              </li>
            ))}
          </ul>
        ) : (
          <p className="flex min-h-28 items-center justify-center text-muted-foreground">No windows open. Restore the desk to open main first.</p>
        )}
        {snapshot && <pre tabIndex={0} role="region" aria-label="Window set snapshot" className="max-h-48 overflow-auto rounded-md border border-border bg-muted/40 p-2 font-(family-name:--tradecn-font-mono) text-xs">{snapshot}</pre>}
      </div>
    </>
  )
}

Installation

pnpm dlx shadcn@latest add tradecn/ui/window-set#v2.0.0
npx shadcn@latest add tradecn/ui/window-set#v2.0.0
yarn dlx shadcn@latest add tradecn/ui/window-set#v2.0.0
bunx --bun shadcn@latest add tradecn/ui/window-set#v2.0.0

Usage

This is a native-shell integration fragment. Supply the application's shell service, loaded prefs envelope, and durable persistPreferences function; the library does not create them.

import { createWindowSet, readWindowSet, windowSetOf, writeWindowSet, type WindowAdapter } from "@/lib/window-set"

// Application-owned shell service; open and close confirm native completion.
const adapter: WindowAdapter = {
  open: (id, url, record) => shell.openWindow(id, url, record),
  close: (id) => shell.closeWindow(id),
  onClosed: (cb) => shell.onWindowClosed(cb), // Returns unsubscribe synchronously.
  bounds: (id) => shell.windowBounds(id),
}
const windows = createWindowSet(adapter)

// On launch: the stored set, or a first desk of one window.
await windows.restore(readWindowSet(prefs) ?? windowSetOf([{ id: "main", layoutId: "desk", main: true }]))

// Before closing windows: snapshot, then durably save through your storage service.
prefs = writeWindowSet(prefs, await windows.snapshot())
await persistPreferences(prefs)

Create one controller in the desk's owner and await one startup restore. restore stops at the first window that fails to open and does not roll back, so the windows after it never open and drop out of the next snapshot; a launch that should survive one bad window opens each record itself, as Desktop shells shows. The controller starts empty and does not discover native windows. Make the adapter's open idempotent so it can adopt an existing initial window. Before a quit snapshot, stop new operations and wait for pending ones to finish; persist before destroying any windows.

Every secondary window loads the app and reads ?window=<id>&layout=<layoutId> to choose its identity and layout; the initial main window receives its record at startup instead. Desktop shells covers owner placement, initial-window adoption, entry URLs, geometry, and close/quit coordination for Tauri and Electron.

The preview uses cards as a simulated shell. Restore opens the missing records, main first; each card's close button reports a shell-initiated closure, while Close all asks the controller to close them. Snapshot reads synthetic bounds offset by 24 pixels from the saved positions. It stays unchanged until you take another snapshot, and nothing is saved across reloads.

API Reference

For separate JavaScript contexts, mount one Workspace per window. Keep layouts under each layoutId, in a layout-manager template or your own storage. This library imports no shell package and stores no layouts.

The set

WindowSet fields are all required:

Field Type Value or purpose
version 1 WINDOW_SET_VERSION.
kind "tradecn-window-set" WINDOW_SET_KIND; distinguishes a set from a layout or preferences envelope.
windows WindowRecord[] Records in restore order, except that the main record goes first.
boundaries WindowSetBoundaries Ownership metadata, defaulting to WINDOW_SET_BOUNDARIES in the helpers.

Each WindowRecord:

Field Type Required Purpose
id string Yes The shell's window name, such as a Tauri label or a key in your Electron window map.
layoutId string Yes The key used to load this window's layout.
bounds WindowBounds No Saved geometry in the units and frame convention your adapter uses.
display string No Application metadata naming the display; the controller does not discover or move displays.
main boolean No Selects the first record to restore. Closing it does not automatically close the desk.

WindowBounds has four required numbers. Choose physical or logical pixels, and outer or content size, consistently between the adapter's reads and writes; see the shell recipes.

Field Type Purpose
x, y number Window position. Negative coordinates are allowed by the parser.
width, height number Window size. The parser requires both to be positive.

windowSetOf keeps the first record for each id and the first retained main: true marker. It shallow-copies records, leaving nested bounds shared, and does not validate typed inputs. mainWindow returns the marked record, otherwise the first, or undefined for an empty set.

Boundaries

WindowSetBoundary is "template" | "user" | "session". WindowSetBoundaries has these three required, readonly arrays of strings; WINDOW_SET_BOUNDARIES supplies their defaults:

Boundary Default entries Intended ownership
template window-ids, layout-ids, main-window The desk's window and layout identities.
user bounds, display The person's geometry and screens.
session open-state, focus Live state, rebuilt during use rather than persisted as fields.

These labels do not remove fields. preferences exports whole slots: marking the windows slot as template also exports any saved bounds and display. Remove those fields from a copy before writing a shareable template. An unclassified preferences slot defaults to user.

readWindowSet and writeWindowSet use WINDOW_SET_SLOT ("windows") unless given another slot name. Writing puts the set in a version 1 slot without changing the envelope's boundary assignment or saving it to storage.

Parsing

Use parsers for untrusted values. parseWindowRecord accepts an object with nonblank string id and layoutId, preserving their original spelling and whitespace. It keeps only complete, finite bounds with positive width and height; fractional values are valid. Invalid bounds are omitted without dropping the record. It keeps a nonempty string display and only main: true, and drops unknown fields. It does not parse JSON text on its own.

parseWindowSet accepts an object or JSON text with kind "tradecn-window-set", version 1, and a windows array. It drops invalid records, normalizes duplicates and main markers, and returns null for an invalid envelope or invalid JSON. Boundaries must supply all three arrays of strings; otherwise the entire boundary value falls back to WINDOW_SET_BOUNDARIES. Empty arrays and empty strings in those arrays are accepted.

Helper Inputs and defaults Returns
windowSetOf(windows?, boundaries?) windows: readonly WindowRecord[] = []; boundaries: WindowSetBoundaries = WINDOW_SET_BOUNDARIES WindowSet; normalizes trusted records.
mainWindow(set) set: WindowSet WindowRecord | undefined.
parseWindowRecord(value) value: unknown WindowRecord | null.
parseWindowSet(value) value: unknown WindowSet | null.
readWindowSet(prefs, slot?) prefs: Preferences; slot: string = WINDOW_SET_SLOT WindowSet | null; parses the slot's value, or returns null if absent or invalid.
writeWindowSet(prefs, set, slot?) prefs: Preferences; set: WindowSet; slot: string = WINDOW_SET_SLOT Preferences; writes a JSON copy of the set into the slot.
defaultWindowUrl(record) record: WindowRecord string; the current location.pathname plus ?window=<id>&layout=<layoutId>, with both values URL-encoded. Without a pathname, returns the query alone.

The adapter

Supply a WindowAdapter to createWindowSet. Only bounds is optional.

Method Inputs Returns Contract
open(id, url, record) id: string; url: string; record: WindowRecord void | Promise<void> Adopt or create the window and apply its record. Complete after successful native setup; reject failures.
close(id) id: string void | Promise<void> Complete after confirmed closure. Reject cancellation so the controller can keep tracking the window.
onClosed(cb) cb: (id: string) => void () => void Subscribe to completed native closures and return unsubscribe synchronously.
bounds(id) id: string WindowBounds | null | undefined | Promise<WindowBounds | null | undefined> Read current geometry for a snapshot; null or undefined keeps the saved bounds.

Prepare any asynchronous native event registration before constructing the controller. A close request alone does not fulfill close: once it resolves, the controller removes the record even if the native window still exists. Forward completed native closures through onClosed, including ones the controller did not request.

The default URL is a path and query, not an absolute shell entry URL. Supply options.url or resolve the record to your shell's entry point in open. The controller passes geometry and display metadata to the adapter; it does not position windows itself.

The controller

createWindowSet(adapter: WindowAdapter, options?: WindowSetOptions) returns a WindowSetController. Options default to {}:

Option Type Default Purpose
url (record: WindowRecord) => string defaultWindowUrl Build the URL passed to adapter.open.
boundaries WindowSetBoundaries WINDOW_SET_BOUNDARIES Metadata for snapshots. restore does not adopt the restored set's boundaries.
Method Inputs Returns Behavior
restore(set) set: WindowSet Promise<WindowRecord[]> Try the main record first, then the rest in order, awaiting each open it starts. Returns the input records it opened; skips tracked ids and ids already opening.
open(record) record: WindowRecord Promise<boolean> true after successful adapter completion; false if the id is tracked or already opening.
close(id) id: string Promise<boolean> true after adapter completion; false if the id is untracked or already closing.
closeAll() None Promise<void> Close the currently tracked ids sequentially, in their insertion order.
windows() None readonly WindowRecord[] A new array of tracked records, in successful-open order. The records themselves are shared.
isOpen(id) id: string boolean Whether this controller tracks the id; does not query the shell.
snapshot() None Promise<WindowSet> Read bounds sequentially for tracked records and build a normalized set using the controller's boundaries.
subscribe(cb) cb: () => void () => void Listen for tracked opens and closes; returns unsubscribe. No initial callback.
dispose() None void Unsubscribe from the shell and clear current subscribers. Does not close windows or clear records.

A pending open is not yet tracked, so isOpen is false and close returns false for that id. Duplicate opens and closes return false without waiting for the original operation. Concurrent restores can therefore open a secondary while another restore is still opening main. Coordinate startup, snapshots, and shutdown in the owner; the controller does not serialize all operations or expose a separate readiness signal.

An adapter.open failure rejects without adding the record; an adapter.close failure leaves it tracked unless a native close notification already removed it. restore and closeAll stop at the first rejection without rolling back earlier successes. A rejected bounds read rejects snapshot; bounds returned successfully are trusted without parser validation.

Subscriber callbacks run synchronously. A throw stops later callbacks and can reject open or close after its tracked state has changed.

Native closure and requested-close completion notify only when they remove a tracked record. Wait for a pending close to finish before reopening its id: the old close can otherwise remove the new record. dispose leaves pending operations running and methods callable, so finish operations before disposing and stop using that controller afterward.

What it does not do

Native discovery, window creation and positioning, main-window shutdown policy, layout loading, and durable storage belong to the application. The controller calls your adapter and tracks the results. Mount a Workspace in each window and load the layout identified by its record or query.

This page is docs/window-set.md at v2.0.0.