# Sidepanel

- Entry point: `@koobiq/components/sidepanel`
- NgModule: `KbqSidepanelModule`
- Page: https://koobiq.io/en/components/sidepanel/overview

## Overview

A sidepanel is a window that slides in from the edge of the screen and appears on top of the main page content.

### When to use

A sidepanel is similar to a [modal](https://koobiq.io/en/components/modal.md). It works well when you need to display a large amount of data.

### Component structure

- Header
- Close button (×)
- Body. Can contain text, controls, and input fields
- Footer with action buttons

### How it works

By default the sidepanel appears on the right, but depending on context it can also appear on the left.

#### Modal mode (with page blocking)

When opening, the sidepanel slides in from the edge of the screen and dims the rest of the page (indicating that the dimmed area is not interactive). When closed (or when the overlay is clicked), it slides back out.

The sidepanel closes on a left mouse button click only: the right and middle buttons open the browser's context menu or trigger its own actions and leave the panel alone. The [modal dialog](https://koobiq.io/en/components/modal.md) behaves the same way.

**Example: Sidepanel modal mode** (`sidepanel-overview`)

```ts sidepanel-overview-example.ts
import { ChangeDetectionStrategy, Component, TemplateRef, inject, viewChild } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSelectModule } from '@koobiq/components/select';
import { KbqSidepanelModule, KbqSidepanelPosition, KbqSidepanelService } from '@koobiq/components/sidepanel';

@Component({
    selector: 'sidepanel-overview-example',
    imports: [KbqSelectModule, KbqButtonModule, KbqSidepanelModule],
    templateUrl: 'sidepanel-overview-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelOverviewExample {
    private sidepanelService = inject(KbqSidepanelService);

    position = KbqSidepanelPosition.Right;

    readonly template = viewChild.required(TemplateRef);

    arrayLength = 40;
    array = new Array(this.arrayLength);

    openSidepanel() {
        this.sidepanelService.open(this.template(), {
            position: this.position,
            hasBackdrop: true
        });
    }
}
```

```html sidepanel-overview-example.html
<div class="kbq-form-horizontal">
    <div class="kbq-form__row">
        <label class="kbq-form__label">Position</label>
        <kbq-form-field style="min-width: 120px" class="kbq-form__control flex-10">
            <kbq-select [(value)]="position">
                <kbq-option [value]="'right'">Right</kbq-option>
                <kbq-option [value]="'left'">Left</kbq-option>
                <kbq-option [value]="'top'">Top</kbq-option>
                <kbq-option [value]="'bottom'">Bottom</kbq-option>
            </kbq-select>
        </kbq-form-field>
    </div>
</div>

<br />

<button kbq-button [color]="'contrast'" (click)="openSidepanel()">
    <span>Open sidepanel</span>
</button>

<ng-template>
    <kbq-sidepanel-header [closeable]="true">Sidepanel Template Content</kbq-sidepanel-header>
    <kbq-sidepanel-body style="padding-top: 8px; padding-bottom: 8px">
        <div style="padding: 8px" class="kbq-subheading">Sidepanel Template Body</div>

        @for (item of array; track $index) {
            <div style="padding: 8px">
                {{ $index + 1 }}
            </div>
        }
    </kbq-sidepanel-body>

    <kbq-sidepanel-footer>
        <kbq-sidepanel-actions align="left">
            <button cdkFocusInitial kbq-button [color]="'contrast'">
                <span>Button</span>
            </button>
        </kbq-sidepanel-actions>

        <kbq-sidepanel-actions align="right">
            <span>Action</span>
        </kbq-sidepanel-actions>
    </kbq-sidepanel-footer>
</ng-template>
```

##### Disable close on backdrop click

The `disableClose` option in `KbqSidepanelConfig` prevents the sidepanel from being closed by clicking the backdrop or pressing Escape. This is useful when the user must explicitly confirm or dismiss the sidepanel through an action inside it, for example, to avoid losing unsaved changes.

**Example: Sidepanel with disabled close on backdrop click** (`sidepanel-disable-close`)

```ts sidepanel-disable-close-example.ts
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSidepanelModule, KbqSidepanelService } from '@koobiq/components/sidepanel';

@Component({
    selector: 'example-sidepanel-disable-close-content',
    imports: [KbqSidepanelModule, KbqButtonModule],
    template: `
        <kbq-sidepanel-header [closeable]="true">Sidepanel</kbq-sidepanel-header>
        <kbq-sidepanel-body>
            Clicking the backdrop or pressing Escape won't close this sidepanel. Use the button below or the close icon
            in the header instead.
        </kbq-sidepanel-body>
        <kbq-sidepanel-footer>
            <kbq-sidepanel-actions>
                <button kbq-button color="contrast" kbq-sidepanel-close>
                    <span>Close</span>
                </button>
            </kbq-sidepanel-actions>
        </kbq-sidepanel-footer>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExampleSidepanelDisableCloseContent {}

@Component({
    selector: 'sidepanel-disable-close-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="open()">
            <span>Open</span>
        </button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelDisableCloseExample {
    private readonly sidepanelService = inject(KbqSidepanelService);

    open() {
        this.sidepanelService.open(ExampleSidepanelDisableCloseContent, {
            hasBackdrop: true,
            disableClose: true
        });
    }
}
```

#### Non-modal mode (without page blocking)

When opening, the sidepanel slides in from the edge of the screen without dimming the rest of the page, so all elements behind it remain interactive.

**Example: Sidepanel normal mode** (`sidepanel-normal-mode`)

```ts sidepanel-normal-mode-example.ts
import { ChangeDetectionStrategy, Component, TemplateRef, inject, viewChild } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSelectModule } from '@koobiq/components/select';
import { KbqSidepanelModule, KbqSidepanelPosition, KbqSidepanelService } from '@koobiq/components/sidepanel';
import { take } from 'rxjs/operators';

@Component({
    selector: 'sidepanel-normal-mode-example',
    imports: [KbqSelectModule, KbqButtonModule, KbqSidepanelModule],
    templateUrl: 'sidepanel-normal-mode-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelNormalModeExample {
    private sidepanelService = inject(KbqSidepanelService);

    position = KbqSidepanelPosition.Right;

    isOpened = false;

    readonly template = viewChild.required(TemplateRef);

    arrayLength = 40;
    array = new Array(this.arrayLength);

    toggleSidepanel() {
        if (!this.isOpened) {
            const sidepanel = this.sidepanelService.open(this.template(), {
                position: this.position,
                hasBackdrop: false
            });

            sidepanel
                .afterClosed()
                .pipe(take(1))
                .subscribe(() => {
                    this.isOpened = false;
                });
            this.isOpened = true;
        } else {
            this.sidepanelService.closeAll();
        }
    }
}
```

```html sidepanel-normal-mode-example.html
<div class="kbq-form-horizontal">
    <div class="kbq-form__row">
        <label class="kbq-form__label">Position</label>
        <kbq-form-field style="min-width: 120px" class="kbq-form__control flex-10">
            <kbq-select [(value)]="position">
                <kbq-option [value]="'right'">Right</kbq-option>
                <kbq-option [value]="'left'">Left</kbq-option>
                <kbq-option [value]="'top'">Top</kbq-option>
                <kbq-option [value]="'bottom'">Bottom</kbq-option>
            </kbq-select>
        </kbq-form-field>
    </div>
</div>

<br />

<button kbq-button [color]="'contrast'" (click)="toggleSidepanel()">
    <span>{{ isOpened ? 'Close sidepanel' : 'Open sidepanel' }}</span>
</button>

<ng-template>
    <kbq-sidepanel-header [closeable]="true">Sidepanel Template Content</kbq-sidepanel-header>
    <kbq-sidepanel-body style="padding-top: 8px; padding-bottom: 8px">
        <div style="padding: 8px" class="kbq-subheading">Sidepanel Template Body</div>

        @for (item of array; track $index) {
            <div style="padding: 8px">
                {{ $index + 1 }}
            </div>
        }
    </kbq-sidepanel-body>

    <kbq-sidepanel-footer>
        <kbq-sidepanel-actions align="left">
            <button cdkFocusInitial kbq-button [color]="'contrast'">
                <span>Button</span>
            </button>
        </kbq-sidepanel-actions>

        <kbq-sidepanel-actions align="right">
            <span>Action</span>
        </kbq-sidepanel-actions>
    </kbq-sidepanel-footer>
</ng-template>
```

#### Stacked panels

You can open another sidepanel from within a sidepanel. Non-modal sidepanels stack on top of each other with an offset. Regardless of how many sidepanels are open, only a single offset is shown to avoid visual noise.

Use this mode when you want to create navigation flows within a sidepanel.

Clicking the vertical area exposed by the panel underneath closes the top panel.

**Example: Sidepanel overlayed** (`sidepanel-overlayed`)

```ts sidepanel-overlayed-example.ts
import { ChangeDetectionStrategy, Component, inject, TemplateRef, viewChild } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSelectModule } from '@koobiq/components/select';
import { KbqSidepanelModule, KbqSidepanelPosition, KbqSidepanelService } from '@koobiq/components/sidepanel';

@Component({
    selector: 'sidepanel-overlayed-example',
    imports: [KbqSelectModule, KbqButtonModule, KbqSidepanelModule],
    templateUrl: './sidepanel-overlayed-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelOverlayedExample {
    protected position = KbqSidepanelPosition.Right;

    protected readonly overlaysCount = 2;

    readonly template = viewChild.required(TemplateRef);

    protected readonly arrayLength = 40;
    protected readonly array = new Array(this.arrayLength);

    private sidepanelService = inject(KbqSidepanelService);

    openSidepanel(): void {
        for (let i = 0; i < this.overlaysCount; i++) {
            this.sidepanelService.open(this.template(), {
                position: this.position,
                hasBackdrop: false
            });
        }
    }

    openAnotherSidepanel(): void {
        this.sidepanelService.open(this.template(), {
            position: this.position,
            hasBackdrop: false
        });
    }
}
```

```html ./sidepanel-overlayed-example.html
<div class="kbq-form-horizontal">
    <div class="kbq-form__row">
        <label class="kbq-form__label">Position</label>
        <kbq-form-field style="min-width: 120px" class="kbq-form__control flex-10">
            <kbq-select [(value)]="position">
                <kbq-option [value]="'right'">Right</kbq-option>
                <kbq-option [value]="'left'">Left</kbq-option>
                <kbq-option [value]="'top'">Top</kbq-option>
                <kbq-option [value]="'bottom'">Bottom</kbq-option>
            </kbq-select>
        </kbq-form-field>
    </div>
</div>

<br />

<button kbq-button [color]="'contrast'" (click)="openSidepanel()">
    <span>Open sidepanel</span>
</button>

<ng-template>
    <kbq-sidepanel-header [closeable]="true">Sidepanel Template Content</kbq-sidepanel-header>
    <kbq-sidepanel-body style="padding-top: 8px; padding-bottom: 8px">
        <div style="padding: 8px" class="kbq-subheading">Sidepanel Template Body</div>

        @for (item of array; track $index) {
            <div style="padding: 8px">
                {{ $index + 1 }}
            </div>
        }
    </kbq-sidepanel-body>

    <kbq-sidepanel-footer>
        <kbq-sidepanel-actions align="left">
            <button cdkFocusInitial kbq-button [color]="'contrast'" (click)="openAnotherSidepanel()">
                <span>Open another sidepanel</span>
            </button>
        </kbq-sidepanel-actions>

        <kbq-sidepanel-actions align="right">
            <button kbq-button kbq-sidepanel-close>
                <span>Close</span>
            </button>
        </kbq-sidepanel-actions>
    </kbq-sidepanel-footer>
</ng-template>
```

##### Mode comparison

| Modal                                        | Non-modal                                                                            |
| -------------------------------------------- | ------------------------------------------------------------------------------------ |
| Not recommended for read-only viewing        | Recommended for read-only viewing                                                    |
| Used for editing like a regular modal dialog | Used only for editing parts of a large object, without explicitly saving those parts |
| Action buttons are always required           | Action buttons are optional                                                          |

#### Focus and keyboard interaction

While a modal sidepanel is open, focus cycles only through elements inside the panel, including the close button and footer buttons, and does not leave the panel. A non-modal sidepanel leaves the page behind it interactive, so it does not trap focus; set `trapFocus: true` in `KbqSidepanelConfig` if a non-modal panel needs one.

##### Focus when the sidepanel is open

When a sidepanel opens, focus moves to an element inside it. Where focus lands depends on the nature and size of the content:

- Focus is set on the first available element inside the sidepanel body, unless there are specific requirements
- If the sidepanel contains a form, focus should be placed on the first input field
- If the sidepanel contains an important irreversible action, it is better to focus on a different, less destructive element
- If the sidepanel contains only action buttons, it is helpful to focus on the button the user is most likely to click ("OK" or "Cancel", depending on the situation)
- If the sidepanel has a lot of content with a scrollbar, setting focus on the first available element may automatically scroll the content and hide its beginning. In this case, add `tabindex=-1` to a static element at the start of the content (a heading or first paragraph) and set focus on it.

##### Focus after closing the sidepanel

When the sidepanel closes, focus returns by default to the element that triggered it (though it is unlikely the user will want to reopen the same sidepanel immediately). If the trigger element no longer exists, focus returns to another element appropriate to the interaction flow.

In some situations, focus may intentionally return to a different element than the one that originally opened the sidepanel. For example, if the task performed in the sidepanel is directly connected to the next step in the workflow. Suppose a table toolbar has an "Add rows" button. Clicking it opens a sidepanel asking for the number and format of new rows. After the sidepanel closes, focus is placed on the first cell of the first new row.

##### Keyboard controls

| Key      | Action                                                                                                                               | Built in |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| Esc      | Close the sidepanel: 1. If no data was changed in the sidepanel 2. If the focused element does not have its own handler for this key | Yes      |
| Ctrl + ↵ | Perform the primary action of the sidepanel                                                                                          | No       |
| ↵        | Submit the form when pressing Enter in any text input                                                                                | No       |

The component handles Esc itself. The other two rows are a guideline for the content you put inside the sidepanel: bind them on your own form or footer buttons.

#### Layout constraints

An open sidepanel is `position: fixed` and always carries a transform — that is what slides it in — so it is the containing block for every `position: fixed` descendant. Anything inside the panel that expects to be anchored to the viewport is anchored to the panel instead. Use `position: absolute` or `position: sticky` for decorations inside a sidepanel, and route real overlays through the CDK overlay container.

### State Saving

A sidepanel can remember whether it was open and come back after a page reload. Pass a `stateSavingKey` in `KbqSidepanelConfig` to opt in — without one nothing is persisted.

A sidepanel cannot restore itself: it exists only while it is open. `KbqSidepanelService` keeps the flag, and the application opens the panel again:

```ts
private readonly sidepanel = inject(KbqSidepanelService);

constructor() {
    afterNextRender(() => {
        if (this.sidepanel.wasOpen('filters')) this.openFilters();
    });
}

protected openFilters(): void {
    this.sidepanel.open(this.filters(), { stateSavingKey: 'filters', hasBackdrop: false });
}
```

**Example: Sidepanel state saving** (`sidepanel-state-saving`)

```ts sidepanel-state-saving-example.ts
import {
    ChangeDetectionStrategy,
    Component,
    TemplateRef,
    afterNextRender,
    inject,
    signal,
    viewChild
} from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSidepanelModule, KbqSidepanelRef, KbqSidepanelService } from '@koobiq/components/sidepanel';

const stateSavingKey = 'sidepanel-state-saving-example';

@Component({
    selector: 'sidepanel-state-saving-example',
    imports: [KbqButtonModule, KbqSidepanelModule],
    template: `
        <div class="example-sidepanel-state-saving__actions">
            <button kbq-button type="button" [color]="'contrast'" (click)="toggle()">
                {{ sidepanelRef() ? 'Close sidepanel' : 'Open sidepanel' }}
            </button>

            <button kbq-button type="button" (click)="sidepanelService.clearSavedState(stateSavingKey)">
                Reset saved state
            </button>
        </div>

        <ng-template>
            <kbq-sidepanel-header [closeable]="true">Sidepanel state saving</kbq-sidepanel-header>
            <kbq-sidepanel-body>
                <div class="example-sidepanel-state-saving__body">
                    Leave this panel open and reload the page — it opens again. The panel is non-modal, so nothing on
                    the page is blocked while it comes back.
                </div>
            </kbq-sidepanel-body>
        </ng-template>
    `,
    styles: `
        .example-sidepanel-state-saving__actions {
            display: flex;
            gap: var(--kbq-size-m);
        }

        .example-sidepanel-state-saving__body {
            padding: var(--kbq-size-l);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelStateSavingExample {
    protected readonly sidepanelService = inject(KbqSidepanelService);
    protected readonly stateSavingKey = stateSavingKey;
    protected readonly sidepanelRef = signal<KbqSidepanelRef | null>(null);

    private readonly template = viewChild.required(TemplateRef);

    constructor() {
        // Once the view exists, so the template is there to open.
        afterNextRender(() => {
            if (this.sidepanelService.wasOpen(stateSavingKey)) this.open();
        });
    }

    protected toggle(): void {
        const opened = this.sidepanelRef();

        // Not closeAll(): closing a group is not recorded, so it would leave the panel remembered as open.
        if (opened) {
            opened.close();
        } else {
            this.open();
        }
    }

    private open(): void {
        const ref = this.sidepanelService.open(this.template(), { hasBackdrop: false, stateSavingKey });

        ref.afterClosed().subscribe(() => this.sidepanelRef.set(null));

        this.sidepanelRef.set(ref);
    }
}
```

What is recorded is a sidepanel closed on its own — the close button, the backdrop, Escape, or `KbqSidepanelRef.close()`. `closeAll()` and the service being destroyed leave the flag alone, so a panel that was open comes back. Give each panel its own key, and use `clearSavedState(key)` to forget one.

Consider what reopening does to the page: a modal sidepanel comes back with its backdrop and its focus trap, over a page the reader has not looked at yet.

Keys, storage and expiry work the same for every component that persists — see [Saving component state](https://koobiq.io/en/components/core.md#saving-component-state).

### Design and animation

#### Size

The panel takes up the full height of the browser window. There is no horizontal scroll — content must fit within the panel width.

| Preset | Width         |
| ------ | ------------- |
| Small  | 400           |
| Medium | 640 (default) |
| Large  | 960           |

**Example: Sidepanel sizes** (`sidepanel-sizes`)

```ts sidepanel-sizes-example.ts
import { ChangeDetectionStrategy, Component, TemplateRef, inject, viewChild } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import {
    KbqSidepanelModule,
    KbqSidepanelPosition,
    KbqSidepanelService,
    KbqSidepanelSize
} from '@koobiq/components/sidepanel';

@Component({
    selector: 'sidepanel-sizes-example',
    imports: [
        KbqButtonModule,
        KbqSidepanelModule
    ],
    templateUrl: 'sidepanel-sizes-example.html',
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelSizesExample {
    private sidepanelService = inject(KbqSidepanelService);

    readonly template = viewChild.required(TemplateRef);

    arrayLength = 40;
    array = new Array(this.arrayLength);

    showSmall() {
        this.sidepanelService.open(this.template(), {
            position: KbqSidepanelPosition.Right,
            size: KbqSidepanelSize.Small
        });
    }

    showMedium() {
        this.sidepanelService.open(this.template(), {
            position: KbqSidepanelPosition.Right,
            size: KbqSidepanelSize.Medium
        });
    }

    showLarge() {
        this.sidepanelService.open(this.template(), {
            position: KbqSidepanelPosition.Right,
            size: KbqSidepanelSize.Large
        });
    }
}
```

```html sidepanel-sizes-example.html
<div style="width: 200px" class="layout-column">
    <button kbq-button class="layout-margin-bottom-l" (click)="showSmall()">Small</button>

    <button kbq-button class="layout-margin-bottom-l" (click)="showMedium()">Medium</button>

    <button kbq-button (click)="showLarge()">Large</button>
</div>

<ng-template>
    <kbq-sidepanel-header [closeable]="true">Sidepanel Template Content</kbq-sidepanel-header>
    <kbq-sidepanel-body
        style="--kbq-sidepanel-size-content-padding-top: 8px; --kbq-sidepanel-size-content-padding-bottom: 8px"
    >
        <div style="padding: 8px" class="kbq-subheading">Sidepanel Template Body</div>

        @for (item of array; track $index) {
            <div style="padding: 8px">
                {{ $index + 1 }}
            </div>
        }
    </kbq-sidepanel-body>

    <kbq-sidepanel-footer>
        <kbq-sidepanel-actions align="left">
            <button cdkFocusInitial kbq-button [color]="'contrast'">
                <span>Button</span>
            </button>
        </kbq-sidepanel-actions>

        <kbq-sidepanel-actions align="right">
            <span>Action</span>
        </kbq-sidepanel-actions>
    </kbq-sidepanel-footer>
</ng-template>
```

The width is chosen by the designer and analyst based on the task. In addition to a fixed pixel width, the width can be set as a percentage of the total screen width, with defined minimum and maximum pixel values. Percentages allow the sidepanel to scale proportionally, while the limits prevent it from becoming too wide or too narrow. In specific cases a plain fixed pixel width can be used.

When choosing the sidepanel size, consider the following:

- Content size. A panel that is too wide or too narrow looks poor.
- Page grid. It looks good when the sidepanel appears proportional on the page and aligns with other blocks. Ideal sidepanel proportions: 1/4, 1/3, 1/2, 2/3, or 3/4 of the main window.
- Usage scenarios. If a non-modal panel is used to view an item from a list, it should ideally not cover the entire list — this preserves the ability to quickly switch between items. If a non-modal panel is used to organize parallel workspaces, verify that the areas do not interfere with each other or with the primary workflow.
- Consistency. The product will look cleaner and more cohesive if it does not use many sidepanels with different widths. Ideally, use one size, two at most.

## API

### Components

#### KbqSidepanelContainerComponent (component)

```ts
@Component({ selector: 'kbq-sidepanel-container' })
class KbqSidepanelContainerComponent extends BasePortalOutlet {
    sidepanelConfig: KbqSidepanelConfig<any>;
    withIndent: boolean;
    id: string;
    readonly portalOutlet: Signal<CdkPortalOutlet>;
    readonly attachDomPortal: ((portal: DomPortal<HTMLElement>) => any) | null;
    attachComponentPortal<T>(
        portal: ComponentPortal<T>
    ): ComponentRef<T>;
    attachTemplatePortal<C>(
        portal: TemplatePortal<C>
    ): EmbeddedViewRef<C>;
    enter(): void;
    exit(): void;
    hasAttached(): boolean;
    attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
    attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
    attach(portal: any): any;
    detach(): void;
    dispose(): void;
}
```

- `withIndent`: `boolean` — Whether the panel exposes the clickable indent strip of the sidepanel stacked underneath it.
- `id`: `string` — ID for the container DOM element.
- `portalOutlet`: `Signal<CdkPortalOutlet>` — The portal outlet inside of this container into which the content will be loaded.
- `attachComponentPortal()`: `ComponentRef<T>` — Attach a component portal as content to this sidepanel container.
- `attachTemplatePortal()`: `EmbeddedViewRef<C>` — Attach a template portal as content to this sidepanel container.
- `enter()`: `void` — Begin animation of the sidepanel entrance into view.
- `exit()`: `void` — Begin animation of the sidepanel exiting from view.
- `hasAttached()`: `boolean` — Whether this host has an attached portal.
- `detach()`: `void` — Detaches a previously attached portal.
- `dispose()`: `void` — Permanently dispose of this portal host.

#### KbqSidepanelHeader (component)

Header of a sidepanel.

```ts
@Component({ selector: 'kbq-sidepanel-header' })
class KbqSidepanelHeader {
    readonly closeable = input<boolean>(false);
    readonly truncateText = input<boolean>(true);
}
```

- `[closeable]`: `boolean` — Add button for close sidepanel. Default false
- `[truncateText]`: `boolean` — Enables text truncation. Default true
- `[localeOverrides]`: `KbqPartialLocaleData | undefined` — Localized strings for this element and its subtree, keyed by locale section. Every key is optional at
  every depth.

### Directives

#### KbqSidepanelActions (directive)

Actions block of a sidepanel footer.

```ts
@Directive({
    selector: 'kbq-sidepanel-actions, [kbq-sidepanel-actions], kbqSidepanelActions'
})
class KbqSidepanelActions {}
```

#### KbqSidepanelBody (directive)

Scrollable content container of a sidepanel.

```ts
@Directive({
    selector: 'kbq-sidepanel-body, [kbq-sidepanel-body], kbqSidepanelBody'
})
class KbqSidepanelBody {}
```

#### KbqSidepanelClose (directive)

Button that will close the current sidepanel.

```ts
@Directive({
    selector: 'button[kbq-sidepanel-close], button[kbqSidepanelClose]'
})
class KbqSidepanelClose {
    readonly sidepanelResult = input<any>(undefined, {
        alias: 'kbq-sidepanel-close'
    });
    readonly kbqSidepanelClose = input<any>();
    readonly sidepanelRef: KbqSidepanelRef<any, any>;
}
```

- `[kbq-sidepanel-close]`: `any` — Value handed to `KbqSidepanelRef.close()`, under the hyphenated spelling of the selector.
- `[kbqSidepanelClose]`: `any` — The same value, under the camel-case spelling.

#### KbqSidepanelFooter (directive)

Footer of a sidepanel.

```ts
@Directive({
    selector: 'kbq-sidepanel-footer, [kbq-sidepanel-footer], kbqSidepanelFooter'
})
class KbqSidepanelFooter {}
```

### Services

#### KbqSidepanelService (service)

```ts
@Injectable({ providedIn: 'root' })
class KbqSidepanelService {
    get openedSidepanels(): KbqSidepanelRef[];
    open<T, D = any>(
        componentOrTemplateRef: ComponentType<T> | TemplateRef<T>,
        config?: KbqSidepanelConfig<D>
    ): KbqSidepanelRef<T>;
    closeAll(): void;
    wasOpen(key: string): boolean;
    clearSavedState(key: string): void;
    getSidepanelById(id: string): KbqSidepanelRef | undefined;
}
```

- `openedSidepanels`: `KbqSidepanelRef[]` — Keeps track of the currently-open sidepanels.
- `closeAll()`: `void` — Closes all of the currently-open sidepanels.

  Closing as a group is not recorded — see `wasOpen()`.
- `wasOpen()`: `boolean` — Whether the sidepanel persisted under `key` was open when it was last recorded, so the
  application can open it again:

  ```ts
  if (sidepanel.wasOpen('filters')) this.openFilters();
  ```

  A sidepanel cannot restore itself the way the components that persist do. It exists only while it
  is open, so at the point the state would be read there is nothing left to read it — and what to
  reopen is a component class or a `TemplateRef` and its `data`, none of which survives being
  written to storage. This service keeps the flag; the application keeps the rest.

  What is recorded is a sidepanel opened and closed on its own. Closing a group — `closeAll()`, or
  this service being destroyed — leaves the flag alone, so a panel that was open when the page went
  away comes back whether the page was reloaded or navigated away from.

  Reading claims the key: from here on `KbqStateSavingService` reports the entry as this service's
  rather than orphaned. It has to happen on read, because for a sidepanel that is not open this is
  the only call there is.
- `clearSavedState()`: `void` — Removes what is persisted under `key`.

  Persistence itself stays on: a sidepanel opened under that key again is recorded again.
- `getSidepanelById()`: `KbqSidepanelRef | undefined` — Finds an open sidepanel by its id.
  - `id`: `string` — ID to use when looking up the sidepanel.

### Classes

#### KbqSidepanelConfig (class)

```ts
class KbqSidepanelConfig<D = any> {
    id?: string;
    stateSavingKey?: string;
    trapFocus?: boolean;
    trapFocusAutoCapture?: boolean;
    ariaLabel?: string;
    ariaLabelledBy?: string;
    data?: D | null;
    position?: KbqSidepanelPosition;
    size?: KbqSidepanelSize;
    hasBackdrop?: boolean;
    backdropClass?: string;
    disableClose?: boolean;
    overlayPanelClass?: string | string[];
    injector?: Injector;
    scrollStrategy?: () => ScrollStrategy;
}
```

- `id?`: `string` — ID for the sidepanel. If omitted, a unique one will be generated.
- `stateSavingKey?`: `string` — Persists whether this sidepanel is open, under this key, so the application can bring it back on
  the next visit — see `KbqSidepanelService.wasOpen()`.

  Omit it and nothing is persisted. The key is the opt-in because there is nothing else stable to
  persist under: a sidepanel lives in an overlay, and `id` falls back to a counter that means
  something different on every load.
- `trapFocus?`: `boolean` — Whether the focus trap is active. Defaults to whether the sidepanel has a backdrop.
- `trapFocusAutoCapture?`: `boolean` — capture focus on initialization. This option sets cdkTrapFocusAutoCapture. Defaults to `true`.
- `ariaLabel?`: `string` — Accessible name of the sidepanel, used when no `kbq-sidepanel-header` names it and no
  `ariaLabelledBy` is given.
- `ariaLabelledBy?`: `string` — Id of the element that labels the sidepanel. When omitted, `kbq-sidepanel-header` labels the
  sidepanel with its own title.
- `data?`: `D | null` — Data being injected into the child component.
- `hasBackdrop?`: `boolean` — Whether the sidepanel has a backdrop.
- `disableClose?`: `boolean` — Whether the user can use escape or clicking outside to close the sidepanel.
- `overlayPanelClass?`: `string | string[]` — Custom class for the overlay pane.
- `injector?`: `Injector` — Injector used for the instantiation of the component to be attached.
  Allows to pass a custom injector to provide a dependency injection hierarchy for the sidepanel content.
- `scrollStrategy?`: `() => ScrollStrategy` — Scroll strategy of the sidepanel overlay, resolved once when the sidepanel opens.
  Defaults to blocking the page scroll for sidepanels with a backdrop and to repositioning for the rest.

#### KbqSidepanelRef (class)

```ts
class KbqSidepanelRef<T = any, R = any> {
    readonly id: string;
    readonly bodyOverflow: WritableSignal<KbqOverflowShadowState>;
    instance: T;
    readonly containerInstance: KbqSidepanelContainerComponent;
    readonly overlayRef: OverlayRef;
    readonly config: KbqSidepanelConfig<any>;
    constructor(
        containerInstance: KbqSidepanelContainerComponent,
        overlayRef: OverlayRef,
        config: KbqSidepanelConfig<any>
    );
    close(result?: R): void;
    beforeClosed(): Observable<void>;
    afterClosed(): Observable<R | undefined>;
    afterOpened(): Observable<void>;
}
```

- `bodyOverflow`: `WritableSignal<KbqOverflowShadowState>` — Vertical scroll overflow state of the sidepanel body.
  Updated on scroll and used for visual adjustments.
- `instance`: `T` — Instance of the component making up the content of the sidepanel.
- `beforeClosed()`: `Observable<void>` — Gets an observable that is notified when the sidepanel is started closing.
- `afterClosed()`: `Observable<R | undefined>` — Gets an observable that is notified when the sidepanel is finished closing.
- `afterOpened()`: `Observable<void>` — Gets an observable that is notified when the sidepanel has opened and appeared.

### Types

#### KbqSidepanelState (interface)

The persisted state of a sidepanel — whether it was open.

```ts
interface KbqSidepanelState {
    opened: boolean;
}
```

### Enums

#### KbqSidepanelPosition (enum)

```ts
enum KbqSidepanelPosition {
    Right = 'right',
    Left = 'left',
    Top = 'top',
    Bottom = 'bottom'
}
```

#### KbqSidepanelSize (enum)

```ts
enum KbqSidepanelSize {
    Small = 'small',
    Medium = 'medium',
    Large = 'large'
}
```

### Constants

#### KBQ_SIDEPANEL_DATA (const)

Injection token that can be used to access the data that was passed in to a sidepanel.

```ts
const KBQ_SIDEPANEL_DATA: InjectionToken<any>;
```

#### KBQ_SIDEPANEL_DEFAULT_OPTIONS (const)

Injection token that can be used to specify default sidepanel options.

```ts
const KBQ_SIDEPANEL_DEFAULT_OPTIONS: InjectionToken<KbqSidepanelConfig>;
```

#### KBQ_SIDEPANEL_WITH_INDENT (const)

```ts
const KBQ_SIDEPANEL_WITH_INDENT: InjectionToken<boolean>;
```

## Examples

### Dynamic configuration update

`disableClose` is re-read every time the sidepanel is about to close, so it can be changed after the sidepanel is already open, via `KbqSidepanelRef.config`. The rest of `KbqSidepanelConfig` is read once, when the sidepanel opens.

**Example: Sidepanel with dynamic config update example** (`sidepanel-with-dynamic-config-update`)

```ts sidepanel-with-dynamic-config-update-example.ts
import { ChangeDetectionStrategy, Component, inject, model } from '@angular/core';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import {
    KBQ_SIDEPANEL_DATA,
    KbqSidepanelModule,
    KbqSidepanelRef,
    KbqSidepanelService
} from '@koobiq/components/sidepanel';
import { KbqToggleModule } from '@koobiq/components/toggle';

type ExampleComponentData = {
    open: () => void;
};

@Component({
    selector: 'example-sidepanel-component',
    imports: [KbqSidepanelModule, KbqButtonModule, KbqToggleModule, FormsModule],
    template: `
        <kbq-sidepanel-header [closeable]="true">Header</kbq-sidepanel-header>
        <kbq-sidepanel-body>
            <kbq-toggle [(ngModel)]="disableClose">Disable close</kbq-toggle>
        </kbq-sidepanel-body>
        <kbq-sidepanel-footer>
            <kbq-sidepanel-actions>
                <button kbq-button color="contrast" (click)="open()">
                    <span>Open another sidepanel</span>
                </button>

                <button kbq-button kbq-sidepanel-close>
                    <span>Close</span>
                </button>
            </kbq-sidepanel-actions>
        </kbq-sidepanel-footer>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExampleSidepanelComponent {
    private readonly sidepanelRef = inject(KbqSidepanelRef);
    private readonly data: ExampleComponentData = inject(KBQ_SIDEPANEL_DATA);

    protected readonly disableClose = model(this.sidepanelRef.config.disableClose);
    protected readonly open = this.data.open;

    constructor() {
        toObservable(this.disableClose)
            .pipe(takeUntilDestroyed())
            .subscribe((value) => {
                this.sidepanelRef.config.disableClose = value;
            });
    }
}

@Component({
    selector: 'sidepanel-with-dynamic-config-update-example',
    imports: [FormsModule, KbqButtonModule],
    template: `
        <button kbq-button (click)="open()">
            <span>Open</span>
        </button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelWithDynamicConfigUpdateExample {
    private readonly sidepanelService = inject(KbqSidepanelService);

    open() {
        this.sidepanelService.open<ExampleSidepanelComponent, ExampleComponentData>(ExampleSidepanelComponent, {
            data: { open: () => this.open() }
        });
    }
}
```

### Custom injector

The `injector` option in `KbqSidepanelConfig` allows passing a custom `Injector` as the parent for the sidepanel content's dependency injection hierarchy. This is useful when you need to provide injection tokens that are not available in the global injector — for example, tokens from a specific component, route, or lazily loaded module.

**Example: Sidepanel with custom injector** (`sidepanel-with-custom-injector`)

```ts sidepanel-with-custom-injector-example.ts
import { ChangeDetectionStrategy, Component, inject, InjectionToken, Injector } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqSidepanelModule, KbqSidepanelService } from '@koobiq/components/sidepanel';

export const EXAMPLE_SIDEPANEL_TOKEN = new InjectionToken<{ name: string; role: string }>('SidepanelUser');

@Component({
    selector: 'example-sidepanel-with-custom-injector-content',
    imports: [KbqSidepanelModule, KbqButtonModule],
    template: `
        <kbq-sidepanel-header [closeable]="true">User info</kbq-sidepanel-header>
        <kbq-sidepanel-body>
            <p>Name: {{ user.name }}</p>
            <p>Role: {{ user.role }}</p>
        </kbq-sidepanel-body>
        <kbq-sidepanel-footer>
            <kbq-sidepanel-actions>
                <button kbq-button kbq-sidepanel-close>Close</button>
            </kbq-sidepanel-actions>
        </kbq-sidepanel-footer>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class ExampleSidepanelWithCustomInjectorContent {
    protected readonly user = inject(EXAMPLE_SIDEPANEL_TOKEN);
}

@Component({
    selector: 'sidepanel-with-custom-injector-example',
    imports: [KbqButtonModule],
    template: `
        <button kbq-button (click)="open()">
            <span>Open</span>
        </button>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class SidepanelWithCustomInjectorExample {
    private readonly injector = inject(Injector);
    private readonly sidepanelService = inject(KbqSidepanelService);

    open() {
        this.sidepanelService.open(ExampleSidepanelWithCustomInjectorContent, {
            injector: Injector.create({
                parent: this.injector,
                providers: [
                    {
                        provide: EXAMPLE_SIDEPANEL_TOKEN,
                        useValue: { name: 'John Doe', role: 'Administrator' }
                    }
                ]
            })
        });
    }
}
```
