# Modal Component

## Описание

Модальное окно — оверлей с подложкой (40% чёрного + blur), закрывающийся по клику вне окна, по `Escape` или по кнопке-крестику в шапке. Поддерживает стек: несколько одновременно открытых модалок укладываются по `z-index` в порядке открытия, `Escape`/клик-вне обрабатывает только самая верхняя (`isTopmost`). Содержимое передаётся через обязательный snippet `main`, футер (кнопки действий) — через необязательный `footer`.

Компонент лежит прямо в `src/lib/Modal.svelte` (без отдельной папки, в отличие от большинства других компонентов библиотеки), поэтому у него нет парного файла `*Props.svelte` — конструкторской панели для него не предусмотрено.

## Пропсы

| Название       | Тип        | По умолчанию                 | Описание                                                                                     |
| -------------- | ---------- | ----------------------------- | ---------------------------------------------------------------------------------------------- |
| `isOpen`       | `boolean`  | `false`                       | Открыто ли модальное окно; поддерживает двустороннее связывание (`$bindable`)                  |
| `title`        | `string`   | `undefined`                   | Заголовок в шапке окна                                                                        |
| `wrapperClass` | `string`   | `""`                          | CSS-классы для окна (например для другой ширины/фона)                                         |
| `mainClass`    | `string`   | `""`                          | CSS-классы для контейнера содержимого (область под `main`)                                    |
| `width`        | `string`   | `""`                          | Явная ширина окна (CSS-значение, например `"600px"`); по умолчанию берётся из `wrapperClass`/`w-300` |
| `main`         | `Snippet`  | `-` (обязателен)              | Содержимое окна                                                                               |
| `footer`       | `Snippet`  | `undefined`                   | Содержимое подвала (обычно кнопки действий, выравниваются в ряд справа налево)                |
| `onCancel`     | `() => void` | `() => (isOpen = false)`    | Вызывается при закрытии окна (крестик, клик вне окна, `Escape`)                                |

## События

Компонент не генерирует DOM-события — используйте `onCancel` для реакции на закрытие.

## Примеры

### Базовое окно

```svelte
<script>
  import * as UI from "poe-svelte-ui-lib"

  let isOpen = $state(false)
</script>

<UI.Button content={{ name: "Открыть" }} onClick={() => (isOpen = true)} />

<UI.Modal bind:isOpen title="Заголовок окна">
  {#snippet main()}
    <p>Содержимое модального окна.</p>
  {/snippet}
</UI.Modal>
```

### С футером (кнопки действий)

```svelte
<script>
  import * as UI from "poe-svelte-ui-lib"

  let isOpen = $state(false)
</script>

<UI.Modal bind:isOpen title="Подтверждение" width="400px">
  {#snippet main()}
    <p>Вы уверены?</p>
  {/snippet}
  {#snippet footer()}
    <UI.Button content={{ name: "Подтвердить" }} onClick={() => (isOpen = false)} />
    <UI.Button content={{ name: "Отмена" }} wrapperClass="bg-gray" onClick={() => (isOpen = false)} />
  {/snippet}
</UI.Modal>
```

## Стек модалок (`ModalStack`)

Экспортируется отдельно из пакета (`src/lib/ModalStackStore.ts`) — обычный Svelte-стор со списком идентификаторов открытых модалок в порядке открытия:

```ts
export const ModalStack = {
  subscribe: ...,     // Writable<string[]>["subscribe"]
  open: (id: string) => void,  // добавляет id в конец стека
  close: (id: string) => void, // убирает id из стека
}
```

Каждый экземпляр `Modal.svelte` сам регистрируется в `ModalStack` при открытии/закрытии (по случайному `crypto.randomUUID()`, не связанному с пропсами) — вручную вызывать `ModalStack.open`/`close` не требуется, это внутренний механизм для вычисления `z-index` и того, какая модалка "самая верхняя" (обрабатывает `Escape`/клик вне себя). Использовать `ModalStack` напрямую имеет смысл только если нужно узнать, сколько модалок сейчас открыто, или в каком порядке — например `$ModalStack.length > 0` для блокировки скролла `body`.

## Внутренняя архитектура

### Реактивность

- `modalId` — случайный идентификатор экземпляра, генерируется один раз при создании компонента
- `zIndex` (`$derived.by`) — вычисляется по позиции `modalId` в `$ModalStack` (`100 + indexInStack`)
- `isTopmost` (`$derived`) — правда, если этот экземпляр последний в стеке
- `$effect` синхронизирует `isOpen` с `ModalStack.open`/`close`, включая очистку при размонтировании

### Директивы

- `transition:fade` для подложки, `transition:scale` для окна
- `data-modal`/`data-modal-backdrop` — атрибуты для распознавания "клика внутри" и "клика по подложке"
- `data-ui-portal` — элементы, вынесенные в портал (например выпадающий список `Select`), не считаются кликом "снаружи" модалки

### Слоты

- `main` (обязателен) и `footer` (опционален) — оба snippet, не `children`/default slot

## Заметки

### Ограничения

- `main` обязателен — компонент не поддерживает вызов без содержимого через обычные дочерние элементы (`children`), только через `{#snippet main()}...{/snippet}`
- Нет собственной конструкторской панели (`*Props.svelte`) — используется напрямую в коде приложения, не как примитив визуального конструктора
