# DesktopShell

> A desktop for the browser with a wallpaper, menu bar, dock, launchpad and a window manager that drags, resizes, snaps, maximises and minimises. Windows are plain data.

Source: https://docs.nasaqui.com/components/desktop-os-shell

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/desktop-os-shell.json
```

A desktop metaphor for admin tools and demos. `DesktopShell` draws a wallpaper, a menu bar (built on
[`Menubar`](https://docs.nasaqui.com/components/menubar)), a dock and a launchpad, and manages floating windows. Apps are plain React
nodes and the window list is plain data (`DesktopWindowState[]`), controlled or not. `DesktopMenuBar`,
`DesktopDock` and `DesktopLaunchpad` are exported to build a different arrangement.

## When to use

- A multi-app workspace where users keep several tools open at once.
- A product demo or a kiosk that should feel like an operating system.

## When not to use

- A normal page layout: use the app shell and navigation components.
- One modal task: use [`Dialog`](https://docs.nasaqui.com/components/dialog).

## Import

```tsx
import { DesktopAppIcon, DesktopShell } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { FolderIcon } from "lucide-react";
import { DesktopAppIcon, DesktopShell } from "@fadymondy/nasaq/web";

const apps = [
  { id: "files", title: "Files", icon: <DesktopAppIcon><FolderIcon /></DesktopAppIcon>, content: <p>Files</p>, single: true },
];

export function Desktop() {
  return <div className="h-dvh"><DesktopShell apps={apps} /></div>;
}
```

## Anatomy

```
DesktopShell                    data-slot="desktop-shell"
├─ wallpaper                    a node or a token gradient
├─ DesktopMenuBar               app name, menus, start and end slots
├─ windows                      title bar, 8 resize handles, snap preview
├─ children                     desktop content behind the windows
├─ DesktopDock                  app icons, running dot, context-click actions
└─ DesktopLaunchpad             icon grid with search
```

## API

### `DesktopShell`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `apps` | `DesktopApp[]` | required | Id, title, icon, content, size, single, pinned, keywords. |
| `windows?` / `defaultWindows?` | `DesktopWindowState[]` | `[]` | Controlled or starting window list, back to front. |
| `onWindowsChange?` | `(windows) => void` | none | Fires on every change. |
| `menus?` | `DesktopMenu[] \| (focused) => DesktopMenu[]` | none | Menus after the app name. |
| `wallpaper?` | `ReactNode` | token gradient | Any node. |
| `menuBarStart?` / `menuBarEnd?` | `ReactNode` | none | Logo, clock, battery. |
| `launchpadOpen?` / `onLaunchpadOpenChange?` | `boolean` / `(open) => void` | uncontrolled | Launchpad state. |
| `compactBelow?` | `number` | `640` | Container width under which windows go full size. |
| `children?` | `ReactNode \| ({ open }) => ReactNode` | none | Desktop content behind the windows. As a function it gets `open(appId)`, so a [`DesktopIconGrid`](https://docs.nasaqui.com/components/desktop-icons) can launch apps. |
| `labels?` | `DesktopShellLabels` | built-in en/ar | String overrides. |

The pure window functions (`openWindow`, `snapWindow`, `resizeRect` and more) live in `desktop-math.ts` and are
covered by node tests.

### System menu: `desktopPowerMenu`

A pure helper in `desktop-power-menu.ts` that builds the menu a desktop keeps at its leading corner. Pass it in
`menus`, usually first.

```tsx
const confirm = useConfirm(); // inside a <ConfirmProvider>
const system = desktopPowerMenu({
  appName: "ToGO",
  locale,
  onAbout, onSettings, onSleep, onRestart, onShutDown, onLogOut,
  confirm,
});

<DesktopShell apps={apps} menus={(focused) => [system, ...appMenus(focused)]} />;
```

| Option | Type | Notes |
| --- | --- | --- |
| `onAbout`, `onSettings`, `onSleep`, `onRestart`, `onShutDown`, `onLogOut` | `() => void` | Only the actions with a handler appear, in that order. |
| `appName?` | `string` | About reads "About {appName}". |
| `confirm?` | `(prompt) => boolean \| Promise<boolean>` | Asked before Restart, Shut Down and Log out; `false` cancels. The prompt has `title`, `description`, `confirmLabel` and `danger`, so `useConfirm()` fits as is. Sleep never asks. |
| `locale?` | `string` | Built-in English and Arabic strings. Default English. |
| `id?`, `label?` | `string` | Default `"system"` and "System". |
| `labels?` | `Partial<DesktopPowerMenuLabels>` | Item names and the confirm texts. |

Sleep, Restart and Shut Down start after a separator, and Log out starts its own group and is marked danger. Also
exported: `desktopPowerLabels`, `desktopPowerConfirm`, `DESKTOP_POWER_ACTIONS`, `DESKTOP_POWER_CONFIRMED`.

## Examples

Control the list to persist it: keep `windows` in state and save it in `onWindowsChange`.

## Accessibility

- The dock is a `nav`; icons are buttons with the app name, and a dot marks running apps.
- Window controls have labels; the title bar drags with the pointer and double-click maximises.
- The launchpad closes with Escape.

## RTL & i18n

Window `x` is measured from the inline start edge, so a saved layout mirrors in Arabic. Snap zones are "start"
and "end", not left and right. Strings come from `useOptionalNasaq()` with a `labels` override.

## Styling & tokens

Only `--nq-*` tokens. Override the wallpaper with the `wallpaper` prop.

## Do / Don't

- Do give each app a stable `id`.
- Do set `single` for apps that should have one window.
- Don't put a full page router inside a window without a fixed size.

## Related

[`Menubar`](https://docs.nasaqui.com/components/menubar), [`ContextMenu`](https://docs.nasaqui.com/components/context-menu), [`Dialog`](https://docs.nasaqui.com/components/dialog), [`DesktopIconGrid`](https://docs.nasaqui.com/components/desktop-icons).

## Lab

`Pages / App / Desktop OS Shell` in the lab: Default, Arabic, Mobile, Power Menu, Power Menu Arabic.

## Code

### React

```tsx
import { FolderIcon } from "lucide-react";
import { DesktopAppIcon, DesktopShell } from "@fadymondy/nasaq/web";

const apps = [
  { id: "files", title: "Files", icon: <DesktopAppIcon><FolderIcon /></DesktopAppIcon>, content: <p>Files</p>, single: true },
];

export function Desktop() {
  return <div className="h-dvh"><DesktopShell apps={apps} /></div>;
}
```

### shadcn

```tsx
import { FolderIcon } from "lucide-react";
import { DesktopAppIcon, DesktopShell } from "@/components/ui/desktop-os-shell";

const apps = [
  { id: "files", title: "Files", icon: <DesktopAppIcon><FolderIcon /></DesktopAppIcon>, content: <p>Files</p>, single: true },
];

export function Desktop() {
  return <div className="h-dvh"><DesktopShell apps={apps} /></div>;
}
```

### Vue

```vue
<script setup lang="ts">
import { Folder } from "lucide-vue-next";
import { h } from "vue";
import { NqDesktopAppIcon, NqDesktopShell, type DesktopApp } from "@fadymondy/nasaq/vue";

const apps: DesktopApp[] = [
  { id: "files", title: "Files", icon: h(NqDesktopAppIcon, null, () => h(Folder)), content: h("p", { class: "p-4" }, "Files"), single: true },
];
</script>

<template>
  <div class="h-96"><NqDesktopShell :apps="apps" /></div>
</template>
```

### Blade

```blade
<div class="h-[32rem]">
    <x-nq::desktop-os-shell
        :apps="[
            ['id' => 'files', 'title' => 'Files', 'icon' => 'folder', 'content' => '<p class=\'p-4\'>Your files</p>', 'single' => true, 'keywords' => ['documents']],
            ['id' => 'notes', 'title' => 'Notes', 'icon' => 'notebook-pen', 'content' => '<p class=\'p-4\'>Notes</p>'],
            ['id' => 'mail', 'title' => 'Mail', 'icon' => 'mail', 'content' => '<p class=\'p-4\'>Inbox</p>', 'pinned' => false],
        ]"
        :menus="[\Nasaq\DesktopPowerMenu::make(['actions' => ['about', 'settings', 'sleep', 'restart', 'shutDown', 'logOut'], 'appName' => 'Nasaq', 'confirm' => true]), ['id' => 'file', 'label' => 'File', 'items' => [['id' => 'new', 'label' => 'New window', 'shortcut' => 'N'], ['id' => 'close', 'label' => 'Close', 'separated' => true, 'danger' => true]]]]"
    />
</div>
<x-nq::confirm-provider />
```

### HTML + Alpine

```html
<div class="h-[32rem]">
    <div data-slot="desktop-shell" x-data="nqDesktopShell(JSON.parse('[{\u0022id\u0022:\u0022files\u0022,\u0022title\u0022:\u0022Files\u0022,\u0022iconHtml\u0022:\u0022\\u003Cspan data-slot=\\u0022desktop-app-icon\\u0022 class=\\u0022grid size-full place-items-center rounded-[22%] bg-card text-primary shadow-sm ring-1 ring-border [\\u0026_svg]:size-1\\\/2\\u0022\\u003E\\n    \\u003Csvg aria-hidden=\\u0022true\\u0022 xmlns=\\u0022http:\\\/\\\/www.w3.org\\\/2000\\\/svg\\u0022 viewBox=\\u00220 0 24 24\\u0022 fill=\\u0022none\\u0022 stroke=\\u0022currentColor\\u0022 stroke-width=\\u00222\\u0022 stroke-linecap=\\u0022round\\u0022 stroke-linejoin=\\u0022round\\u0022\\u003E\\n  \\u003Cpath d=\\u0022M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\\u0022\\\/\\u003E\\n\\u003C\\\/svg\\u003E\\n\\u003C\\\/span\\u003E\\n\u0022,\u0022content\u0022:\u0022\\u003Cp class=\\u0027p-4\\u0027\\u003EYour files\\u003C\\\/p\\u003E\u0022,\u0022single\u0022:true,\u0022keywords\u0022:[\u0022documents\u0022]},{\u0022id\u0022:\u0022notes\u0022,\u0022title\u0022:\u0022Notes\u0022,\u0022iconHtml\u0022:\u0022\\u003Cspan data-slot=\\u0022desktop-app-icon\\u0022 class=\\u0022grid size-full place-items-center rounded-[22%] bg-card text-primary shadow-sm ring-1 ring-border [\\u0026_svg]:size-1\\\/2\\u0022\\u003E\\n    \\u003Csvg aria-hidden=\\u0022true\\u0022 xmlns=\\u0022http:\\\/\\\/www.w3.org\\\/2000\\\/svg\\u0022 viewBox=\\u00220 0 24 24\\u0022 fill=\\u0022none\\u0022 stroke=\\u0022currentColor\\u0022 stroke-width=\\u00222\\u0022 stroke-linecap=\\u0022round\\u0022 stroke-linejoin=\\u0022round\\u0022\\u003E\\n  \\u003Cpath d=\\u0022M13.4 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-7.4\\u0022\\\/\\u003E\\n  \\u003Cpath d=\\u0022M2 6h4\\u0022\\\/\\u003E\\n  \\u003Cpath d=\\u0022M2 10h4\\u0022\\\/\\u003E\\n  \\u003Cpath d=\\u0022M2 14h4\\u0022\\\/\\u003E\\n  \\u003Cpath d=\\u0022M2 18h4\\u0022\\\/\\u003E\\n  \\u003Cpath d=\\u0022M21.378 5.626a1 1 0 1 0-3.004-3.004l-5.01 5.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z\\u0022\\\/\\u003E\\n\\u003C\\\/svg\\u003E\\n\\u003C\\\/span\\u003E\\n\u0022,\u0022content\u0022:\u0022\\u003Cp class=\\u0027p-4\\u0027\\u003ENotes\\u003C\\\/p\\u003E\u0022},{\u0022id\u0022:\u0022mail\u0022,\u0022title\u0022:\u0022Mail\u0022,\u0022iconHtml\u0022:\u0022\\u003Cspan data-slot=\\u0022desktop-app-icon\\u0022 class=\\u0022grid size-full place-items-center rounded-[22%] bg-card text-primary shadow-sm ring-1 ring-border [\\u0026_svg]:size-1\\\/2\\u0022\\u003E\\n    \\u003Csvg aria-hidden=\\u0022true\\u0022 xmlns=\\u0022http:\\\/\\\/www.w3.org\\\/2000\\\/svg\\u0022 viewBox=\\u00220 0 24 24\\u0022 fill=\\u0022none\\u0022 stroke=\\u0022currentColor\\u0022 stroke-width=\\u00222\\u0022 stroke-linecap=\\u0022round\\u0022 stroke-linejoin=\\u0022round\\u0022\\u003E\\n  \\u003Cpath d=\\u0022m22 7-8.991 5.727a2 2 0 0 1-2.009 0L2 7\\u0022\\\/\\u003E\\n  \\u003Crect x=\\u00222\\u0022 y=\\u00224\\u0022 width=\\u002220\\u0022 height=\\u002216\\u0022 rx=\\u00222\\u0022\\\/\\u003E\\n\\u003C\\\/svg\\u003E\\n\\u003C\\\/span\\u003E\\n\u0022,\u0022content\u0022:\u0022\\u003Cp class=\\u0027p-4\\u0027\\u003EInbox\\u003C\\\/p\\u003E\u0022,\u0022pinned\u0022:false}]'), {})" x-modelable="launchpad"
    x-bind:data-compact="compact() ? '' : null" x-on:nq-desktop-icon-open="openById($event.detail.id)"
    class="relative isolate flex h-full min-h-96 w-full flex-col overflow-hidden bg-background text-foreground">
    <div aria-hidden="true" class="absolute inset-0 -z-10">
                    <div class="size-full bg-[radial-gradient(120%_90%_at_20%_0%,color-mix(in_oklab,var(--nq-action)_28%,transparent),transparent_60%),radial-gradient(90%_80%_at_100%_100%,color-mix(in_oklab,var(--nq-success)_22%,transparent),transparent_60%)]"></div>
            </div>
    <div data-slot="desktop-menu-bar" role="presentation" aria-label="Menu bar"
        class="flex h-8 shrink-0 items-center gap-1 border-b border-border/60 bg-card/70 px-2 text-caption backdrop-blur-md">
        <span x-show="focusedTitle()" x-text="focusedTitle()" class="px-2 text-label font-semibold text-foreground"></span>
                    <div data-slot="menubar" x-data="nqMenubar()" x-bind="bar"
    class="flex w-fit items-center gap-0.5 rounded-control border-border text-foreground h-6 min-h-0 border-0 bg-transparent p-0"><div data-slot="menubar-menu" x-data="nqMenubarMenu(false)" x-modelable="open" class="contents"><button type="button" data-slot="menubar-trigger" x-ref="trigger" x-bind="trigger"
        class="inline-flex h-full cursor-default select-none items-center rounded-control text-foreground outline-none hover:bg-nq-hover data-popup-open:bg-nq-selected focus-visible:bg-nq-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 min-h-0 px-2 text-caption">System</button>
                        <template x-teleport="body">
    <div data-slot="menubar-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-start.offset.6="$refs.trigger"
        class="z-50 overflow-hidden rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none max-h-[var(--available-height)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 min-w-56"><div role="none">
                                                                        <div data-slot="menubar-item" role="menuitem" data-variant="default" x-bind="item"
        data-id="system.about" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">About Nasaq</div>
                                </div>
                                                            <div role="none">
                                                                        <div data-slot="menubar-item" role="menuitem" data-variant="default" x-bind="item"
        data-id="system.settings" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">System Settings…</div>
                                </div>
                                                            <div role="none">
                                    <div data-slot="menubar-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
                                    <div data-slot="menubar-item" role="menuitem" data-variant="default" x-bind="item"
        data-id="system.sleep" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">Sleep</div>
                                </div>
                                                            <div role="none">
                                                                        <div data-slot="menubar-item" role="menuitem" data-variant="default" x-bind="item"
        data-id="system.restart" data-confirm="%7B%22action%22%3A%22restart%22%2C%22title%22%3A%22Restart%20now%3F%22%2C%22description%22%3A%22Open%20apps%20close.%20Unsaved%20work%20may%20be%20lost.%22%2C%22confirmLabel%22%3A%22Restart%22%2C%22danger%22%3Afalse%7D" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">Restart…</div>
                                </div>
                                                            <div role="none">
                                                                        <div data-slot="menubar-item" role="menuitem" data-variant="default" x-bind="item"
        data-id="system.shutDown" data-confirm="%7B%22action%22%3A%22shutDown%22%2C%22title%22%3A%22Shut%20down%20now%3F%22%2C%22description%22%3A%22Open%20apps%20close.%20Unsaved%20work%20may%20be%20lost.%22%2C%22confirmLabel%22%3A%22Shut%20Down%22%2C%22danger%22%3Atrue%7D" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">Shut Down…</div>
                                </div>
                                                            <div role="none">
                                    <div data-slot="menubar-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
                                    <div data-slot="menubar-item" role="menuitem" data-variant="danger" x-bind="item"
        data-id="system.logOut" data-confirm="%7B%22action%22%3A%22logOut%22%2C%22title%22%3A%22Log%20out%20now%3F%22%2C%22description%22%3A%22You%20will%20need%20to%20sign%20in%20again.%22%2C%22confirmLabel%22%3A%22Log%20Out%22%2C%22danger%22%3Atrue%7D" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-nq-danger-text [&_svg]:text-current">Log Out</div>
                                </div></div>
</template></div>
                                    <div data-slot="menubar-menu" x-data="nqMenubarMenu(false)" x-modelable="open" class="contents"><button type="button" data-slot="menubar-trigger" x-ref="trigger" x-bind="trigger"
        class="inline-flex h-full cursor-default select-none items-center rounded-control text-foreground outline-none hover:bg-nq-hover data-popup-open:bg-nq-selected focus-visible:bg-nq-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus data-disabled:pointer-events-none data-disabled:opacity-50 min-h-0 px-2 text-caption">File</button>
                        <template x-teleport="body">
    <div data-slot="menubar-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-start.offset.6="$refs.trigger"
        class="z-50 overflow-hidden rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none max-h-[var(--available-height)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 min-w-56"><div role="none">
                                                                        <div data-slot="menubar-item" role="menuitem" data-variant="default" x-bind="item"
        data-id="file.new" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground">New window<span data-slot="menubar-shortcut" dir="ltr" class="ms-auto inline-flex items-center gap-1 ps-6">
            <kbd data-slot="kbd"  dir="ltr"
    class="inline-flex h-5 min-w-5 items-center justify-center rounded-[4px] border border-border bg-card px-1 font-mono text-[11px] text-muted-foreground">N</kbd>
    </span>
</div>
                                </div>
                                                            <div role="none">
                                    <div data-slot="menubar-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
                                    <div data-slot="menubar-item" role="menuitem" data-variant="danger" x-bind="item"
        data-id="file.close" x-on:click="menuPick($el.dataset.id, $el.dataset.confirm)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-nq-danger-text [&_svg]:text-current">Close</div>
                                </div></div>
</template></div></div>
                <div class="ms-auto flex items-center gap-3 px-2 text-muted-foreground"></div>
    </div>
    <div x-ref="area" role="region" aria-label="Desktop" class="relative min-h-0 flex-1">
                <div x-show="preview" x-cloak data-slot="desktop-snap-preview" aria-hidden="true" x-bind:style="previewStyle()"
            class="pointer-events-none absolute rounded-xl border-2 border-primary/60 bg-primary/10"></div>
        <template x-for="win in windows" x-bind:key="win.id">
            <section data-slot="desktop-window" x-show="appOf(win.appId)" x-bind:aria-label="appOf(win.appId)?.title"
                x-bind:data-focused="top()?.id === win.id ? '' : null" x-bind:data-maximised="shown(win).maximised ? '' : null"
                x-bind:hidden="shown(win).minimised" x-bind:style="frameStyle(win)" x-on:pointerdown.capture="focusWin(win.id)"
                x-bind:class="[isFull(shown(win)) ? 'rounded-none border-border' : 'rounded-xl shadow-lg', top()?.id === win.id ? 'border-border shadow-2xl' : 'border-border/60']"
                class="absolute flex flex-col overflow-hidden border bg-card text-foreground">
                <div data-slot="desktop-window-title" x-on:pointerdown="beginMove($event, win)" x-on:dblclick="compact() || maximiseWin(win.id)"
                    x-bind:class="compact() ? '' : 'cursor-default [touch-action:none]'"
                    class="flex h-9 shrink-0 select-none items-center gap-2 border-b border-border bg-secondary/60 ps-2 pe-1">
                    <span class="size-5 shrink-0" x-html="appOf(win.appId)?.iconHtml"></span>
                    <span x-text="appOf(win.appId)?.title" x-bind:class="top()?.id === win.id ? 'text-foreground' : 'text-muted-foreground'" class="min-w-0 flex-1 truncate text-label"></span>
                    <button type="button" data-action="minimise" class="grid size-6 place-items-center rounded-full text-muted-foreground outline-none hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus" aria-label="Minimise" title="Minimise" x-on:click="minimiseWin(win.id)">
                        <svg aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
</svg>                    </button>
                    <button type="button" data-action="maximise" x-show="! compact()" class="grid size-6 place-items-center rounded-full text-muted-foreground outline-none hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus" x-bind:aria-label="win.maximised ? 'Restore' : 'Maximise'" x-bind:title="win.maximised ? 'Restore' : 'Maximise'" x-on:click="maximiseWin(win.id)">
                        <svg x-show="win.maximised" aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m14 10 7-7"/>
  <path d="M20 10h-6V4"/>
  <path d="m3 21 7-7"/>
  <path d="M4 14h6v6"/>
</svg>                        <svg x-show="! win.maximised" aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M15 3h6v6"/>
  <path d="m21 3-7 7"/>
  <path d="m3 21 7-7"/>
  <path d="M9 21H3v-6"/>
</svg>                    </button>
                    <button type="button" data-action="close" class="grid size-6 place-items-center rounded-full text-muted-foreground outline-none hover:bg-nq-hover hover:text-foreground focus-visible:outline-2 focus-visible:outline-nq-focus hover:bg-nq-danger hover:text-background" aria-label="Close" title="Close" x-on:click="closeWin(win.id)">
                        <svg aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg>                    </button>
                </div>
                <div data-slot="desktop-window-body" class="min-h-0 flex-1 overflow-auto" x-init="mount($el, win.appId)"></div>
                                    <div aria-hidden="true" data-handle="n" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'n')"
                        class="absolute z-10 [touch-action:none] inset-x-2 top-0 h-1.5 cursor-ns-resize"></div>
                                    <div aria-hidden="true" data-handle="s" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 's')"
                        class="absolute z-10 [touch-action:none] inset-x-2 bottom-0 h-1.5 cursor-ns-resize"></div>
                                    <div aria-hidden="true" data-handle="e" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'e')"
                        class="absolute z-10 [touch-action:none] inset-y-2 end-0 w-1.5 cursor-ew-resize"></div>
                                    <div aria-hidden="true" data-handle="w" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'w')"
                        class="absolute z-10 [touch-action:none] inset-y-2 start-0 w-1.5 cursor-ew-resize"></div>
                                    <div aria-hidden="true" data-handle="ne" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'ne')"
                        class="absolute z-10 [touch-action:none] top-0 end-0 size-3 cursor-nesw-resize rtl:cursor-nwse-resize"></div>
                                    <div aria-hidden="true" data-handle="nw" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'nw')"
                        class="absolute z-10 [touch-action:none] top-0 start-0 size-3 cursor-nwse-resize rtl:cursor-nesw-resize"></div>
                                    <div aria-hidden="true" data-handle="se" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'se')"
                        class="absolute z-10 [touch-action:none] bottom-0 end-0 size-3 cursor-nwse-resize rtl:cursor-nesw-resize"></div>
                                    <div aria-hidden="true" data-handle="sw" x-show="! isFull(shown(win)) && ! compact()" x-on:pointerdown="beginResize($event, win, 'sw')"
                        class="absolute z-10 [touch-action:none] bottom-0 start-0 size-3 cursor-nesw-resize rtl:cursor-nwse-resize"></div>
                            </section>
        </template>
        <div class="pointer-events-none absolute inset-x-0 bottom-2 z-[850] flex justify-center px-2">
            <nav data-slot="desktop-dock" aria-label="Dock"
                class="pointer-events-auto flex max-w-full items-end gap-1.5 overflow-x-auto rounded-2xl border border-border/70 bg-card/80 p-1.5 shadow-lg backdrop-blur-md">
                <button type="button" data-action="launchpad" aria-label="Launchpad" title="Launchpad" x-bind:aria-pressed="launchpad ? 'true' : 'false'" x-on:click="setLaunchpad(! launchpad)"
                    class="grid size-11 place-items-center rounded-xl bg-secondary text-foreground outline-none transition-transform duration-150 ease-nq hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-nq-focus">
                    <svg aria-hidden="true" class="size-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <rect width="7" height="7" x="3" y="3" rx="1"/>
  <rect width="7" height="7" x="14" y="3" rx="1"/>
  <rect width="7" height="7" x="14" y="14" rx="1"/>
  <rect width="7" height="7" x="3" y="14" rx="1"/>
</svg>                </button>
                <span aria-hidden="true" class="mx-0.5 h-8 w-px self-center bg-border"></span>
                <template x-for="app in dockApps()" x-bind:key="app.id">
                    <button type="button" data-dock-app x-bind:data-app="app.id" x-bind:aria-label="app.title" x-bind:title="app.title"
                        x-bind:data-running="running(app) ? '' : null" x-bind:data-focused="isFocused(app) ? '' : null" x-on:click="activate(app)"
                        class="group relative flex size-11 flex-col items-center rounded-xl outline-none transition-transform duration-150 ease-nq hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-nq-focus">
                        <span class="size-11" x-html="app.iconHtml"></span>
                        <span x-show="running(app)" aria-hidden="true" class="absolute -bottom-1 size-1 rounded-full bg-foreground"></span>
                        <span x-show="running(app)" class="sr-only">Running</span>
                    </button>
                </template>
            </nav>
        </div>
        <div x-show="launchpad" x-cloak data-slot="desktop-launchpad" role="dialog" aria-modal="true" aria-label="Launchpad"
            x-on:keydown.escape.stop="setLaunchpad(false)" x-on:pointerdown="($event.target === $el || $event.target.dataset.launchpadGrid !== undefined) && setLaunchpad(false)"
            class="absolute inset-0 z-[900] flex flex-col items-center gap-8 overflow-y-auto bg-background/70 px-6 pt-10 pb-28 backdrop-blur-xl">
            <div class="relative w-full max-w-xs">
                <svg aria-hidden="true" class="pointer-events-none absolute inset-y-0 start-3 my-auto size-4 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m21 21-4.34-4.34"/>
  <circle cx="11" cy="11" r="8"/>
</svg>                <input data-slot="input" type="search"
                    x-model="query" x-on:keydown.enter="pickFirst()" aria-label="Search apps" placeholder="Search apps" class="w-full min-w-0 rounded-control border border-input bg-card px-3 text-body text-foreground min-h-[var(--nq-touch-min,0px)] transition-colors duration-150 ease-nq outline-none placeholder:text-muted-foreground focus-visible:border-nq-focus focus-visible:outline-1 focus-visible:outline-nq-focus data-invalid:border-nq-danger aria-invalid:border-nq-danger disabled:cursor-not-allowed disabled:opacity-50 pointer-coarse:text-[16px] h-control ps-9">
            </div>
            <ul x-show="shownApps().length" data-launchpad-grid="" class="grid w-full max-w-3xl grid-cols-[repeat(auto-fill,minmax(5.5rem,1fr))] gap-x-4 gap-y-6">
                <template x-for="app in shownApps()" x-bind:key="app.id">
                    <li class="flex justify-center">
                        <button type="button" data-launchpad-app x-bind:data-app="app.id" x-on:click="activate(app)"
                            class="flex w-22 flex-col items-center gap-2 rounded-xl p-1 text-label text-foreground outline-none hover:bg-nq-hover focus-visible:outline-2 focus-visible:outline-nq-focus">
                            <span class="size-16" x-html="app.iconHtml"></span>
                            <span class="max-w-full truncate" x-text="app.title"></span>
                        </button>
                    </li>
                </template>
            </ul>
            <p x-show="! shownApps().length" class="text-body text-muted-foreground">No apps match</p>
        </div>
    </div>
</div>
</div>
<div data-slot="confirm-provider" x-data="nqConfirmProvider()" x-id="['nq-confirm']" x-on:nq:confirm.window="ask($event.detail)" class="contents">
    <template x-teleport="body">
        <div data-slot="alert-dialog-portal">
            <div data-slot="alert-dialog-backdrop" x-nq-presence="open" class="fixed inset-0 z-50 bg-nq-fg/15 dark:bg-nq-bg/60 transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0"></div>
            <div data-slot="confirm-dialog" x-bind="popup" x-nq-presence="open" x-trap.noscroll="open"
                class="fixed inset-0 z-50 m-auto grid h-fit w-[calc(100%-2rem)] max-w-md gap-4 rounded-floating border border-border bg-popover p-6 text-popover-foreground outline-none max-h-[calc(100dvh-2rem)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
                <div data-slot="alert-dialog-header" class="flex flex-col gap-1.5 text-start"><h2 data-slot="alert-dialog-title" :id="$id('nq-confirm', 'title')" class="text-h3 text-foreground" x-text="options.title"></h2>
                    <p data-slot="alert-dialog-description" x-show="options.description" :id="$id('nq-confirm', 'description')" class="text-body-sm text-muted-foreground" x-text="options.description"></p></div>
                <div data-slot="alert-dialog-footer" class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"><button data-slot="alert-dialog-cancel"
     type="button"                         autofocus="autofocus" x-on:click="settle(false)" x-text="options.cancelLabel ?? $nq.t(&#039;Cancel&#039;, &#039;إلغاء&#039;)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Cancel</button>
                    <button data-slot="button"
     type="button"                         x-show="options.danger !== false" x-on:click="settle(true)" x-text="options.confirmLabel ?? $nq.t(&#039;Confirm&#039;, &#039;تأكيد&#039;)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-destructive text-destructive-foreground hover:bg-[color-mix(in_oklab,var(--nq-danger-solid)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Confirm</button>
                    <button data-slot="button"
     type="button"                         x-show="options.danger === false" x-on:click="settle(true)" x-text="options.confirmLabel ?? $nq.t(&#039;Confirm&#039;, &#039;تأكيد&#039;)" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border border-transparent font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground hover:bg-[color-mix(in_oklab,var(--nq-action)_88%,var(--nq-fg))] h-control px-[var(--nq-control-pad)]">
        Confirm</button></div>
            </div>
        </div>
    </template>
</div>
```
