# useWindow

[Back to Composables README](https://github.com/NantHealth/featherk/blob/integration/packages/composables/README.md)

> Wraps Kendo UI for Vue's `Window` component - not the browser `window` object. If you were expecting something like VueUse's `useWindowSize`/`useWindowScroll`, this is unrelated to those.

Composable for a zero/low-config Kendo `Window`: viewport-clamped dragging and resizing, stage transitions, and the pointerdown-capture resize/text-selection workaround are all handled internally. Every setting has a sensible default, so `useWindow()` with no arguments is a complete, working Window.

## Quick Start

1. Import `useWindow` and Kendo's `Window` component.
2. Call `useWindow()` - accept every default setting.
3. Wire a trigger (e.g. a Button) to `open()`.
4. Bind `<Window>` via `v-if`, `ref`, `v-bind`, and `v-on` - never wire individual Window props by hand.

```vue
<template>
  <!-- Step 3: trigger opens the Window using its defaults -->
  <Button @click="open">Open Window</Button>

  <!-- Step 4: v-if/ref/v-bind/v-on is the entire binding surface -->
  <Window v-if="visible" ref="windowRef" v-bind="props" v-on="events">
    <p>This is the content of the window.</p>
  </Window>
</template>

<script setup lang="ts">
// Step 1: import the composable and the Kendo component it augments
import { useWindow } from "@featherk/composables/window";
import { Window } from "@progress/kendo-vue-dialogs";
import { Button } from "@progress/kendo-vue-buttons";

// Step 2: no options - every setting uses useWindow's built-in defaults
const { windowRef, visible, props, events, open } = useWindow();
</script>
```

## Overriding Defaults

Pass `settings` with only the fields you want to change; `useWindow` merges it over `WINDOW_SETTINGS_DEFAULTS` internally, so you never spread the defaults yourself.

```ts
const { windowRef, visible, props, events, open } = useWindow({
  settings: {
    title: "Preferences",
    initialLeft: 120,
    initialTop: 40,
    initialWidth: 600,
    initialHeight: 400,
    resizable: false,
  },
});
```

## Persisting Settings Externally

Pass an existing `Ref` (for example, one already wired to your own persistence layer) as `settings` instead of a plain object. `useWindow` fills in any missing fields from `WINDOW_SETTINGS_DEFAULTS` once, in place, then reads and mutates that same ref directly (position/size updates from dragging and resizing land on it) - persistence stays entirely your concern, `useWindow` has no knowledge of how or whether you persist it.

```ts
const persistedSettings = ref<Partial<WindowSettings>>({ ...myLoadedSettings });
const win = useWindow({ settings: persistedSettings });
```

A partially-populated ref is fine - any field `myLoadedSettings` doesn't have falls back to the built-in default, exactly like the plain-object form above.

`open()` respects this: by default it reopens at whatever `left`/`top`/`width`/`height` are already on `settings` (a prior drag/resize, or a value you restored from persistence), rather than recomputing the initial inset position. Pass `open({ resetPosition: true })` to force the Window back to its computed initial position/size instead - see `UseWindowReturn.open` in the API table below.

Set `settings.initialLeft` and/or `settings.initialTop` to override the default 16px right/top inset on the first open and whenever `resetPosition` is true. Each coordinate can be overridden independently; an omitted coordinate keeps its computed inset default.

## Resizing Bounds

Resizing is clamped to the viewport (the scrolled document viewport when `settings.fixed` is `false`):

- The dragged edge stops at the viewport edge; the opposite edge stays anchored, so dragging the top or left edge past the viewport never grows the Window down or right.
- An edge that is already outside the viewport (for example, a bottom edge dragged below the fold) is never snapped back - it just cannot be pushed further out.
- The Window cannot shrink below `settings.minWidth`/`settings.minHeight`. Both are optional; unset or `null` falls back to Kendo's defaults (120px/100px).
- Keyboard resizing (Ctrl+Arrow) follows the same viewport and minimum-size limits.

`settings.left`/`top`/`width`/`height` and the `onResize` payload always hold these clamped values, not Kendo's raw resize geometry.

```ts
const win = useWindow({
  settings: { minWidth: 320, minHeight: 200 },
});
```

## Extending Window Behavior

Use the `UseWindowOptions` callbacks when application code needs to react to Window behavior. These callbacks are extension hooks: they run after `useWindow` completes its internal handling, and they do not replace Kendo's event handlers.

This applies to every callback option:

- `onMove` runs after viewport clamping and position updates.
- `onResize` runs after viewport clamping, size/position updates, and resize-state tracking.
- `onStageChange` runs after a valid controlled stage transition.
- `onOpen` runs after `open()` has shown and sized the Window.
- `onClose` runs after `close()` has reset the stage and hidden the Window.

For example, use `onMove` and `onResize` when application code needs to observe movement or resizing:

```ts
const win = useWindow({
  onMove: (payload) => {
    console.log("Moved", payload.left, payload.top);
  },
  onResize: (payload) => {
    console.log("Resized", payload.width, payload.height);
  },
});
```

## Titlebar Double-Click

Titlebar double-click stage changes follow normal Windows behavior by default: double-clicking
the titlebar toggles between `DEFAULT` and `FULLSCREEN`. Set `titlebarDoubleClick: false` to
disable both transitions. This option is independent of `settings.maximizeButton`; disabling the
maximize button does not automatically disable titlebar double-click behavior.

Keep binding `v-on="events"` on `<Window>` so viewport clamping (drag and resize), stage handling, and Kendo workarounds remain active:

```vue
<Window
  v-if="win.visible.value"
  ref="windowRef"
  v-bind="win.props.value"
  v-on="win.events"
/>
```

Do not replace entries on `events` unless you intentionally opt out of the corresponding `useWindow` protections. For most consumers, compose application behavior through the callback options and leave `v-on="events"` intact.

## API

### `UseWindowOptions`

| Option          | Type                                                      | Description                                                                                                        |
| --------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `settings`      | `Partial<WindowSettings> \| Ref<Partial<WindowSettings>>` | Overrides merged over `WINDOW_SETTINGS_DEFAULTS` internally. Pass a `Ref` for externally-owned/persisted settings. |
| `initialStage`  | `WindowStage`                                             | Initial `stage` value. Defaults to `"DEFAULT"`.                                                                    |
| `titlebarDoubleClick` | `MaybeRefOrGetter<boolean>`                         | Enables titlebar double-click stage changes. Defaults to `true`; pass `false` or a reactive ref/getter returning `false` to disable both maximize and restore transitions. |
| `onMove`        | `(payload) => void`                                       | Called after internal move handling.                                                                               |
| `onResize`      | `(payload) => void`                                       | Called after internal resize handling, with the viewport-clamped `left`/`top`/`width`/`height`.                     |
| `onStageChange` | `(stage) => void`                                         | Called after a valid stage transition.                                                                             |
| `onOpen`        | `() => void`                                              | Called after `open()` finishes sizing the Window.                                                                  |
| `onClose`       | `() => void`                                              | Called after `close()` hides the Window.                                                                           |

### `UseWindowReturn`

| Member                      | Type                                                           | Description                                                                                                                                                         |
| --------------------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `windowRef`                 | `Ref<ComponentPublicInstance \| null>`                         | Bind via `ref="windowRef"`.                                                                                                                                         |
| `settings`                  | `Ref<WindowSettings>`                                          | Reactive settings - bind checkboxes/inputs directly for a settings panel.                                                                                           |
| `visible`                   | `Ref<boolean>`                                                 | Bind via `v-if="visible"`.                                                                                                                                          |
| `stage`                     | `Ref<WindowStage>`                                             | Current `"DEFAULT" \| "MINIMIZED" \| "FULLSCREEN"` stage.                                                                                                           |
| `isDragging` / `isResizing` | `Ref<boolean>`                                                 | Live drag/resize state, already folded into `windowClass`.                                                                                                          |
| `props`                     | `ComputedRef<WindowBindableProps>`                             | Spread via `v-bind="props"` onto `<Window>`.                                                                                                                        |
| `events`                    | `WindowEvents`                                                 | Spread via `v-on="events"` onto `<Window>`.                                                                                                                         |
| `open`                      | `(openOptions?: { resetPosition?: boolean }) => Promise<void>` | Shows the Window. By default, reopens at the current `settings` position/size; pass `{ resetPosition: true }` to recompute the initial inset position/size instead. |
| `close`                     | `() => void`                                                   | Resets `stage` to `"DEFAULT"` and hides the Window.                                                                                                                 |

## Inert Props

`props` only contains Kendo `Window` props that actually work: `title`, `draggable`, `resizable`, `left`, `top`, `initialLeft`, `initialTop`, `initialWidth`, `initialHeight`, `width`, `height`, `minWidth`, `minHeight`, `stage`, and `windowClass`. `settings.closable`, `settings.minimizeButton`, and `settings.maximizeButton` are deliberately **not** included in `props`.

If you hand-wire raw Kendo props like `closeButton`/`minimizeButton`/`maximizeButton` directly on `<Window>` alongside `v-bind="props"`, they are **inert** - they will not hide the corresponding titlebar button. `useWindow` only ever suppresses those buttons via CSS (`fk-window-hide-minimize`/`fk-window-hide-maximize`/`fk-window-hide-close` classes folded into `windowClass`), never through Kendo's own button props. Toggle `settings.minimizeButton`/`settings.maximizeButton`/`settings.closable` instead.

## Kendo Defects Worked Around

- **Titlebar button props are non-functional.** Kendo's `Window` types `minimizeButton`/`maximizeButton`/`closeButton`/`restoreButton` as `[String, Function]`, but the child `WindowTitlebar` that actually renders them types the same props `[String, Function, Object, Boolean]` and renders each button unconditionally regardless of value. There is no supported way to hide these buttons through Kendo's own props - see "Inert Props" above for the CSS-class workaround `useWindow` uses instead.
- **Resize-handle drag vs. text-selection race.** Kendo Window's resize handles use Kendo's own draggable implementation, which does not prevent the native browser default until after pointer movement begins. If text inside the Window is already selected, native selection-dragging can compete with a resize started from an inward-overlapping handle. `useWindow` prevents this internally with a capture-phase `pointerdown` listener scoped to `.k-resize-handle` elements - see [docs/design/composable-native-event-listener-tactic.md](https://github.com/NantHealth/featherk/blob/integration/docs/design/composable-native-event-listener-tactic.md) for the general pattern. This is fully internal; no `@pointerdown.capture` binding is needed (or exposed) in consumer templates.
- **Resize geometry is unbounded and self-compounding.** See "Kendo Window Resize Math Defect" below.

### Kendo Window Resize Math Defect

Observed in `@progress/kendo-vue-dialogs` 6.4.1 (`Window.mjs`, `handleResize`). Because of this, `useWindow` computes resize geometry itself rather than trusting the values Kendo emits on `resize`.

**Symptoms (without the workaround)**

- Dragging the right or bottom edge past the browser window keeps growing the Window indefinitely.
- After that, the left edge can be dragged offscreen too, leaving the whole Window offscreen and unrecoverable.
- Dragging the top edge up past the viewport makes the Window grow **down**; dragging the left edge past the viewport makes it grow **right**. Corner handles show both behaviors.

**Root cause**

- Kendo sets the dragged edge to the pointer's absolute `pageX`/`pageY` with no viewport bounds. Pointer capture keeps reporting coordinates beyond the browser window, so right/bottom edges follow indefinitely.
- For `n`/`w` handles Kendo sets `top = pageY` (negative past the viewport) and adds `computedTop - pageY` to the height. `computedTop` is `Math.max(top, 0)`, so the Window renders at `0` while the height grows again on every pointer move - growth leaks out the opposite edge. Same for `left`/width.
- The emitted `left`/`top`/`width`/`height` come from Kendo's internal state, not the controlled props: values for the axis not being resized can be stale (including unclamped values from a prior drag).
- Kendo mixes coordinate spaces: `pageX`/`pageY` are document coordinates, while a `position: fixed` Window's `top`/`left` are viewport coordinates, so resizing is off by the scroll offset on a scrolled page.
- Kendo reads `props.top || currentTop`, so a controlled `top`/`left` of `0` is treated as unset and falls back to internal state.

**Workaround in `useWindow`**

- The existing capture-phase `pointerdown` listener on `.k-resize-handle` also records the handle direction (from its `k-resize-<dir>` class), the starting rect, and the starting pointer position, then tracks the pointer with a capture-phase `window` `pointermove` listener.
- On each Kendo `resize` event, geometry is computed from the pointer delta and clamped per edge (viewport bounds, anchored opposite edge, `minWidth`/`minHeight`); Kendo's emitted geometry is ignored. Kendo's event still drives timing (`drag`/`end`).
- Keyboard resizing (Ctrl+Arrow) has no pointer, so it applies the change in Kendo's emitted size as a delta to the clamped size.
- See "Resizing Bounds" above for the resulting behavior.

**Removal criteria**

Revisit when upgrading `@progress/kendo-vue-dialogs`. The workaround can be dropped once Kendo's `handleResize` bounds edges to the viewport, anchors the opposite edge at the viewport, and emits geometry consistent with the controlled props. The `useWindow` resize tests (`useWindow.test.ts`) describe the expected behavior and can be used to verify.

## Styling

All `windowClass` output (`fk-window`, `fk-window-can-drag`, `fk-window-is-dragging`, `fk-window-is-resizing`, `fk-window-fixed`, `fk-window-hide-minimize`, `fk-window-hide-maximize`, `fk-window-hide-close`) is styled in `@featherk/styles` - consumers do not need to write any Window CSS themselves. The titlebar button-hiding workaround, the Kendo defect fixes, and drag/resize motion are all shipped as part of the external stylesheet; header background, text color, icon color, and padding are configured through ThemeBuilder's Window component panel. Just make sure the app includes `@featherk/styles`.
