# SideSheet

![Status](https://img.shields.io/badge/status-stable-brightgreen)

Painel que empurra o conteúdo adjacente para a esquerda, dividindo o espaço horizontal com ele — sem escurecer a tela e sem sobrepor o conteúdo, no modo desktop (>= 1024px). Abaixo de 1024px o painel passa a funcionar como um overlay em tela cheia, com fundo escurecido, igual a um Drawer.

`<s-side-sheet>` não tem nenhum input: é uma instância **única** por aplicação, declarada uma vez no shell do app (ex: `app.component.html`, logo abaixo da topbar) e controlada remotamente por `SideSheetService`, a partir de qualquer componente de rota.

## Quando usar

- Exibir detalhes de um item sem interromper o fluxo — a lista/tabela principal continua rolável, filtrável e paginável com o painel aberto
- Painéis que devem permanecer abertos enquanto o usuário navega entre registros de uma lista
- Cenários onde o conteúdo por trás do painel precisa continuar interativo mesmo no desktop

## Quando não usar

- Ações destrutivas ou de confirmação — prefira `ConfirmDialog`
- Conteúdo crítico que exige atenção exclusiva, bloqueando toda a tela — prefira `Dialog`
- Fechamento configurável por Escape, clique fora ou confirmação de fechamento — prefira `Drawer`
- Painel embutido no mesmo fluxo de layout, sem necessidade de overlay em mobile — prefira `SlideInBar`

## Instalação

```typescript
import { SideSheetComponent } from '@seniorsistemas/angular-components/side-sheet';

@Component({
  standalone: true,
  imports: [SideSheetComponent],
})
export class MeuComponent {}
```

## Uso básico

Declare `<s-side-sheet>` uma única vez, no ponto mais alto da árvore do app (ex: `app.component.html`, logo abaixo da topbar), projetando dentro dele o conteúdo principal que deve ser empurrado quando o painel abrir (normalmente o `router-outlet`):

```html
<!-- app.component.html (shell do app) -->
<div class="flex h-dvh flex-col">
  <s-topbar>...</s-topbar>

  <s-side-sheet>
    <router-outlet />
  </s-side-sheet>
</div>
```

> Ver [Pré-requisito de layout](#pré-requisito-de-layout) — sem o `class="flex h-dvh flex-col"` no container raiz, o painel não ocupa a altura da tela.

A partir de qualquer componente de rota, abra e alimente o painel via `SideSheetService.open()`:

```typescript
// pedidos.component.ts (componente de uma rota qualquer, distante do shell)
private readonly sideSheetService = inject(SideSheetService);
private readonly panelBody = viewChild<TemplateRef<unknown>>('panelBody');

protected selecionarPedido(pedido: Pedido): void {
  this.pedidoSelecionado = pedido;
  this.sideSheetService.open({ header: pedido.numero, template: this.panelBody()! });
}
```

```html
<!-- pedidos.component.html -->
<ng-template #panelBody>
  <p>Detalhes de {{ pedidoSelecionado.numero }}</p>
</ng-template>
```

Repetir `open()` enquanto o painel já está visível (ex: selecionar outro pedido) **não fecha nem recria o painel** — apenas atualiza o conteúdo. O painel fecha automaticamente a cada navegação de rota (limpando também cabeçalho, template, contexto e tamanho armazenados), evitando manter conteúdo de uma página antiga visível e evitando referenciar um `TemplateRef` de um componente que a navegação já destruiu.

Uma segunda instância de `<s-side-sheet>` montada simultaneamente lança um erro em modo de desenvolvimento — só pode existir uma por aplicação. Chamar `SideSheetService.open()` sem nenhuma instância montada emite um aviso no console (em modo de desenvolvimento) e não exibe nada.

## API

`SideSheetComponent` não possui inputs nem outputs — toda a configuração é feita via `SideSheetService`.

### SideSheetService

Serviço `providedIn: 'root'`, injetável em qualquer componente.

| Membro | Tipo | Descrição |
|--------|------|-----------|
| `visible` | `WritableSignal<boolean>` | Visibilidade do painel |
| `header` | `Signal<string>` | Cabeçalho definido pela última chamada de `open()` |
| `template` | `Signal<TemplateRef<unknown> \| null>` | Template do corpo do painel, projetado internamente pelo `s-side-sheet` |
| `context` | `Signal<unknown>` | Contexto opcional do template |
| `size` | `Signal<SideSheetSize>` | Tamanho do painel definido pela última chamada de `open()` |
| `open(config: SideSheetConfig)` | `void` | Abre (ou atualiza, se já aberto) o painel |
| `close()` | `void` | Fecha o painel, limpando também o cabeçalho, template, contexto e tamanho armazenados. Chamado automaticamente a cada navegação de rota |

### Tipos

```typescript
export type SideSheetSize = 'default' | 'half';

export type SideSheetConfig<T = unknown> = {
  header: string;
  template: TemplateRef<T>;
  context?: T;
  size?: SideSheetSize; // padrão: 'default'
};
```

### Slot de conteúdo

`<s-side-sheet>` tem um único slot (`ng-content` padrão, sem selector): o conteúdo principal da página, que é empurrado pelo painel quando ele abre (ex: o `router-outlet`). O corpo do próprio painel não é projetado por slot — vem do `template`/`context` passados para `SideSheetService.open()` e é renderizado internamente via `NgTemplateOutlet`.

## Comportamento por resolução

| Largura da viewport | Comportamento | `size: 'default'` | `size: 'half'` |
|---|---|---|---|
| < 1024px | Overlay em tela cheia, com fundo escurecido | 100% | 100% |
| 1024–1365px | Empurra o conteúdo (push) | 400px | 400px (converte para `default`) |
| 1366–1919px | Empurra o conteúdo (push) | 400px | `max(400px, 50vw)` |
| 1920–2559px | Empurra o conteúdo (push) | 480px | `max(400px, 50vw)` |
| 2560–3439px | Empurra o conteúdo (push) | 560px | `max(400px, 50vw)` |
| >= 3440px | Empurra o conteúdo (push) | 640px | `max(400px, 50vw)` |

## Pré-requisito de layout

O `<s-side-sheet>` preenche automaticamente a altura do seu contêiner pai via `flex: 1; min-height: 0;` embutido no próprio componente — mas isso só funciona se esse contêiner pai for, ele mesmo, um flex container com altura própria até a viewport. O componente usa `position: absolute` (não `fixed`) no modo overlay justamente para nunca cobrir a topbar, mas isso significa que ele também não sabe a altura dela: quem define o limite é o consumidor.

Receita mínima para o shell do app (ex: `app.component.html`, logo abaixo da topbar):

```html
<div class="flex h-dvh flex-col">
  <s-topbar>...</s-topbar>

  <s-side-sheet>
    <router-outlet />
  </s-side-sheet>
</div>
```

- `h-dvh` (não `h-screen`/`100vh`) no container raiz — usa a *dynamic viewport height*, que se ajusta corretamente quando a barra de endereço do navegador mobile aparece/some, evitando espaço em branco ou scroll indesejado no modo overlay (< 1024px).
- `flex flex-col` no mesmo container — é o que permite que o `flex: 1` (já embutido no `<s-side-sheet>`) de fato preencha a altura restante abaixo da topbar.

## Exemplos

### Trocar de item sem fechar

```typescript
// pedidos.component.ts
private readonly sideSheetService = inject(SideSheetService);
private readonly panelBody = viewChild<TemplateRef<unknown>>('panelBody');

protected selecionarPedido(pedido: Pedido): void {
  this.sideSheetService.open({ header: pedido.numero, template: this.panelBody()! });
}
```

```html
<!-- pedidos.component.html -->
<ng-template #panelBody>
  <p>Detalhes de {{ pedidoSelecionado.numero }}</p>
</ng-template>
```

Selecionar outro pedido chama `open()` novamente — o painel atualiza o conteúdo sem fechar.

### Tamanho Half

```typescript
protected editarCompleto(pedido: Pedido): void {
  this.sideSheetService.open({ header: 'Edição completa', template: this.formTemplate()!, size: 'half' });
}
```

## Acessibilidade

- No modo overlay (< 1024px): `role="dialog"`, `aria-modal="true"` e `cdkTrapFocus` aprisionam o foco dentro do painel enquanto aberto
- No modo push (>= 1024px): `role="complementary"`, sem aprisionamento de foco — o conteúdo ao lado continua totalmente navegável por teclado
- O título recebe `aria-labelledby` com um id único por instância
- Ao fechar, o foco é devolvido ao elemento que estava focado antes da abertura
- O scroll da página é bloqueado automaticamente apenas no modo overlay, e restaurado ao fechar. O bloqueio é compartilhado com outros overlays da biblioteca (ex: `Drawer`) — fechar um não reativa o scroll enquanto outro ainda estiver aberto
- Fecha somente pelo botão X do cabeçalho — não há fechamento por Escape ou clique fora, em nenhuma resolução

## Componentes relacionados

- [`Drawer`](../drawer/README.md) — painel lateral que sobrepõe o conteúdo com backdrop, fechamento configurável por Esc/clique fora
- [`Dialog`](../dialog/README.md) — modal centralizado para confirmações e conteúdo crítico
- [`SlideInBar`](../slide-in-bar/README.md) — painel embutido no fluxo do layout, sem necessidade de overlay em mobile
