> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cskn.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Desktop kit

> Build a desktop surface that reads as a native macOS or Windows app: window layout, toolbar, sidebar, selection lists, inspector, commands and drag and drop.

On the desktop shell your app gets a whole window, a mouse and a keyboard. The desktop kit lays it out the way macOS and Windows apps do: a source list down the left, a toolbar over the content, an inspector on the right for the selection, and a status bar along the bottom. It draws the macOS idiom on a Mac and Windows 11's Fluent idiom elsewhere, including Linux.

Use it for the `desktop` entry of [`appSurfaces`](https://docs.cskn.dev/concepts/surfaces#one-ui-per-surface). For a phone-style app that also runs on desktop, [`StackNavigator`](https://docs.cskn.dev/ui/stack/overview) adapts to desktop by itself.

```tsx theme={null}
import {
  DesktopWindow, Toolbar, ToolbarButton, ToolbarSearch, SourceList, SourceListSection,
  SourceListItem, SelectionList, InspectorHeader, InspectorSection, InspectorRow, useDesktopCommands,
} from '@cskn/ui';

export default function DesktopApp() {
  const [selected, setSelected] = useState<string[]>([]);
  const item = items.find((i) => i.id === selected[0]);

  useDesktopCommands([
    { id: 'new', label: 'New item', shortcut: 'mod+n', run: addItem },
    { separator: true },
    { id: 'delete', label: 'Delete', shortcut: 'delete', enabled: !!item, run: () => remove(item!) },
  ]);

  return (
    <DesktopWindow
      id="main"
      sidebar={
        <SourceList label="Lists">
          <SourceListSection title="Library">
            <SourceListItem icon="home" label="All items" count={items.length} selected onSelect={showAll} />
          </SourceListSection>
        </SourceList>
      }
      toolbar={
        <Toolbar title="All items" subtitle={`${items.length} items`}>
          <ToolbarSearch value={query} onChange={setQuery} />
          <ToolbarButton icon="plus" label="New item" primary shortcut="mod+n" bindShortcut={false} onPress={addItem} />
        </Toolbar>
      }
      inspector={item && (
        <>
          <InspectorHeader media={<img src={item.image} alt="" />} title={item.name} subtitle={item.price} />
          <InspectorSection title="Details">
            <InspectorRow label="Added">{item.added}</InspectorRow>
          </InspectorSection>
        </>
      )}
    >
      <SelectionList
        label="Items"
        items={items}
        getKey={(i) => i.id}
        selected={selected}
        onSelectionChange={setSelected}
        multiple
        onOpen={openItem}
        textOf={(i) => i.name}
        renderItem={(i) => <span>{i.name}</span>}
      />
    </DesktopWindow>
  );
}
```

## DesktopWindow

The window layout. Only the content scrolls; the chrome around it stays put. It hides the shell's own header (the toolbar takes its place) and leaves room for the macOS window buttons.

**sidebar** `ReactNode`

The source list down the left, usually a `SourceList`.

**toolbar** `ReactNode`

The bar over the content, usually a `Toolbar`.

**inspector** `ReactNode`

The pane on the right for the selection. Pass nothing to close it.

**status** `ReactNode`

A thin bar along the bottom: counts, sync state.

**id** `string`

Remember pane widths and whether the sidebar is shown, in `localStorage`. Leave it out to start fresh every time.

**sidebarWidth** `\{ initial?, min?, max? \}` (default: 220 / 170 / 340)

Sidebar width limits in px.

**inspectorWidth** `\{ initial?, min?, max? \}` (default: 300 / 240 / 460)

Inspector width limits in px.

Both panes can be dragged wider or narrower. ⌘⌥S / Ctrl+Alt+S hides the sidebar. In a window narrower than 700 px, the sidebar floats over the content instead of squeezing it.

Inside the window, `useDesktopWindow()` returns `{ os, sidebarHidden, toggleSidebar, hasSidebar }`. Outside one, `useDesktopOS()` still tells you the idiom: `'macos'` or `'windows'`.

## Toolbar

**title** `ReactNode`

The window's title: the place or the thing open.

**subtitle** `ReactNode`

A quieter line beside it, such as "42 items".

**leading** `ReactNode`

Controls before the title, such as back and forward.

**children** `ReactNode`

Buttons, a search field and segmented controls, at the trailing end in order.

When your app runs in the desktop shell, the toolbar also carries the shell's window controls (open in a new window, open in the browser, minimize, close), using the SDK's [window API](https://docs.cskn.dev/sdk/native/app-control.md#desktop-window).

**ToolbarButton**

**label** `string` *required*

Its name: the tooltip, and the text with `showLabel` or without an icon.

**onPress** `() =\> void` *required*

Click handler.

**icon** `IconName`

Button icon.

**showLabel** `boolean`

Text beside the icon, for the window's main action.

**primary** `boolean`

The main action, in the accent color.

**active** `boolean`

A toggle that's on, such as a view mode.

**shortcut** `string`

A key combination that presses it, shown in the tooltip, such as `mod+n`.

**bindShortcut** `boolean` (default: true)

Bind `shortcut` here. Turn it off when `useDesktopCommands` already binds the same key.

**disabled** `boolean`

Greyed out.

**ToolbarSearch** takes `value`, `onChange`, `placeholder?` (default "Search"), `onSubmit?` and `shortcut?` (default `mod+f`; `false` for none). `ToolbarGroup` (`label?`) groups related buttons, and `ToolbarSeparator` draws a divider.

## SourceList

The sidebar's list of places, like Finder's or Mail's. The list is a single Tab stop; ↑ and ↓ move between rows, and Return or Space opens one.

```tsx theme={null}
<SourceList label="Lists" footer={<SourceListItem icon="plus" label="New list" onSelect={createList} />}>
  <SourceListSection title="Library">
    <SourceListItem icon="home" label="All items" count={42} selected shortcut="mod+1" onSelect={showAll} />
  </SourceListSection>
  <SourceListSection title="My lists" collapsible>
    {lists.map((l) => (
      <SourceListItem key={l.id} icon="list" label={l.name} count={l.count} onSelect={() => showList(l.id)} />
    ))}
  </SourceListSection>
</SourceList>
```

**SourceList**: `label` (required, for screen readers), `footer?` (pinned under the list).

**SourceListSection**: `title?`, `collapsible?` (a disclosure on the title, as in Finder), `defaultCollapsed?`, `action?` (shown on hover at the title's end, such as a "+").

**SourceListItem**: `label`, `onSelect` (required), `icon?` or `leading?` (an avatar or cover), `selected?`, `count?`, `shortcut?` (shown on hover), `disabled?`. For a right-click menu, wrap it in a [`ContextMenu`](https://docs.cskn.dev/ui/components/overlays#contextmenu).

## SelectionList

A list (or grid) with desktop selection: click selects, ⌘ / Ctrl-click and Shift-click extend it, ⌘A selects all, double-click or Return opens. Typing a few letters jumps to a matching row.

**items** `T[]` *required*

The rows' data.

**getKey** `(item) =\> string` *required*

A stable key per item.

**selected** `string[]` *required*

The selected keys.

**onSelectionChange** `(keys: string[]) =\> void` *required*

The selection changed.

**renderItem** `(item, \{ selected \}) =\> ReactNode` *required*

Draws a row or tile.

**label** `string` *required*

What the list is, for screen readers.

**multiple** `boolean`

Allow more than one selected item.

**onOpen** `(item) =\> void`

Double-click, or Return on the selection.

**contextMenu** `(item, selection) =\> ContextMenuItem[]`

Right-click menu. A row outside the selection is selected first, so the menu always acts on what's highlighted.

**textOf** `(item) =\> string`

Text for type-to-jump.

**empty** `ReactNode`

Shown when there are no items.

**rowHeight** `number`

Row height in px. Defaults to the platform's: 28 on macOS, 32 on Windows.

**layout** `'list' | 'grid'` (default: 'list')

`grid` lays items out as tiles, like a photo library: ← and → step through tiles, ↑ and ↓ move a row.

**tileSize** `number` (default: 160)

The narrowest a tile gets in `grid`, in px.

## Inspector

The pieces of an inspector pane, shaped like Finder's Get Info:

| Component | Props | Use |
| - | - | - |
| `InspectorHeader` | `media?`, `title`, `subtitle?` | A picture, the name and a line under it |
| `InspectorSection` | `title?` | A group of rows |
| `InspectorRow` | `label` | A label and value |
| `InspectorActions` | — | A row of buttons |

## useDesktopCommands

Declare the window's commands once. On macOS they appear in your app's menu in the system menu bar, where the shell turns their keys into menu clicks. Everywhere else (the Windows shell, a browser) they are bound as keyboard shortcuts in the page.

```tsx theme={null}
useDesktopCommands([
  { id: 'new', label: 'New item', shortcut: 'mod+n', run: addItem },
  { id: 'grid', label: 'Show as grid', checked: layout === 'grid', run: toggleLayout },
  { separator: true },
  { id: 'delete', label: 'Delete', shortcut: 'delete', enabled: selected.length > 0, run: removeSelected },
]);
```

Each command has `id`, `label`, `run`, and optionally `shortcut`, `enabled` (default `true`; a disabled command's key does nothing) and `checked`. A key without a modifier, such as `delete`, is always bound in the page, because the menu bar never carries one. A toolbar button for the same command should pass `bindShortcut={false}` so the key isn't handled twice.

This builds on the SDK's [`useAppMenu`](https://docs.cskn.dev/sdk/native/app-control#menu-bar).

## Drag and drop

Mouse and trackpad drag and drop, desktop style. None of it gets in the way of touch; phones use [swipe rows](https://docs.cskn.dev/ui/stack/lists.md#swiperow) and context menus for the same jobs.

**Files from the desktop**: wrap the target in `DropZone`. While files hover, it gets `data-dropping` and shows `hint`.

```tsx theme={null}
import { DropZone } from '@cskn/ui';

<DropZone accept="image/*" single hint="Drop to set the cover" onFiles={([file]) => setCover(file)}>
  <CoverImage />
</DropZone>
```

`DropZone` takes `onFiles` (required), `accept?` (MIME patterns, as in `<input accept>`), `single?`, `disabled?`, `hint?`, `className?` and `style?`.

**Your own items**: make an element draggable with `useDragSource(kind, data)` and a target with `useDropTarget(kind, onDrop, { canDrop? })`. Spread the result on the element. While something droppable hovers, the target has `data-drop-over`.

```tsx theme={null}
import { useDragSource, useDropTarget } from '@cskn/ui';

function ItemRow({ item, list }) {
  return <div {...useDragSource('item', { id: item.id, from: list.id })}>{item.name}</div>;
}

function ListRow({ list }) {
  const drop = useDropTarget<{ id: string; from: string }>(
    'item',
    (d) => moveItem(d.id, list.id),
    { canDrop: (d) => d.from !== list.id },
  );
  return <div {...drop}>{list.name}</div>;
}
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.