diff --git a/DRAFT/README.md b/DRAFT/README.md
deleted file mode 100644
index ad1266e..0000000
--- a/DRAFT/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Черновики SLM
-
-> Материалы в `DRAFT` являются рабочими черновиками и не задают нормативную спецификацию SLM.
-
-## Материалы
-
-- [Архитектура](./architecture/README.md) - слои, модули, публичные API, фасеты и зависимости.
-- [Правила](./rules/README.md) - канонические наборы, формат и правила формулировки.
-
-## Соглашение
-
-Черновики могут содержать определения, правила, рекомендации, примеры и открытые вопросы.
-
-Нормативные определения задаются [терминологией](./architecture/terminology.md). Только блокирующие правила получают код SLM; тематические черновики ссылаются на канонические правила и не повторяют их формулировки.
diff --git a/DRAFT/architecture/README.md b/DRAFT/architecture/README.md
deleted file mode 100644
index a5cc101..0000000
--- a/DRAFT/architecture/README.md
+++ /dev/null
@@ -1,42 +0,0 @@
-# Архитектура SLM
-
-> Статус: рабочий черновик. Документы в этой папке не являются спецификацией.
-
-SLM задаёт структурную основу приложения: слои, модули, публичные границы, зависимости, фасеты сред выполнения и владение жизненным циклом.
-
-## Область SLM
-
-Черновик описывает слои, модули, группы, сегменты, компоненты, публичный API, зависимости и владение жизненным циклом ресурсов.
-
-SLM не задаёт обязательную внутреннюю форму модуля, обязательный поток данных, правила монорепозиториев или полный файловый стайлгайд. Форма модуля следует его ответственности и реальным потребителям.
-
-## Виды утверждений
-
-- **Определение** нормативно задаёт смысл архитектурного термина, но не получает код правила.
-- **Правило** задаёт блокирующий архитектурный инвариант и объявляется в каноническом реестре.
-- **Рекомендация** помогает принять решение, но не делает архитектуру невалидной.
-- **Пример** иллюстрирует модель и не задаёт обязательную структуру.
-
-Нормативные определения находятся в [терминологии](./terminology.md). Канонический набор требований находится в [реестре правил SLM](../rules/registry.md). Формат и требования к правилам описаны отдельно в разделе [Правила SLM](../rules/).
-
-Остальные документы объясняют и иллюстрируют модель, но не владеют точными формулировками определений и правил.
-
-## Основная идея
-
-Модуль является основной архитектурной единицей SLM. Слой задаёт его роль и направление зависимостей: модули слоя `domains` владеют предметными ответственностями на тех же основаниях, что и остальные модули. Группа помогает навигации, сегмент организует внутреннее содержимое, а компонент всегда принадлежит модулю.
-
-SLM требует отдельную папку и единый логический публичный API модуля. По умолчанию API представлен корневым `index`; при необходимости модуль добавляет environment-фасеты `client`, `browser` и `server`. Внутренняя файловая форма модулей, сегментов и компонентов определяется стайлгайдом проекта.
-
-## Карта черновика
-
-- [Терминология](./terminology.md)
-- [Слои](./layers.md)
-- [Модули слоя domains](./domains.md)
-- [Зависимости](./dependencies.md)
-- [Модули](./modules.md)
-- [Группы](./groups.md)
-- [Сегменты](./segments.md)
-- [Компоненты](./components.md)
-- [Вложенные модули](./nested-modules.md)
-- [Жизненный цикл](./lifecycle.md)
-- [Проверка](./validation.md)
diff --git a/DRAFT/architecture/components.md b/DRAFT/architecture/components.md
deleted file mode 100644
index 0f265de..0000000
--- a/DRAFT/architecture/components.md
+++ /dev/null
@@ -1,65 +0,0 @@
-# Компоненты SLM
-
-> Пояснение нормативной модели компонентов SLM.
-
-Компонент является строительным элементом интерфейса, а не самостоятельной архитектурной единицей.
-
-## Связанные правила
-
-- [`SLM-COMPONENT-R009`](../rules/registry.md#slm-component-r009)
-- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
-- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
-- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
-- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
-- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
-
-## Файловая форма
-
-Файловую форму компонента определяет стайлгайд. Компонент может быть одним файлом фреймворка или каталогом со вспомогательными файлами.
-
-```text
-landing/
-└── ui/
- └── hero.tsx
-```
-
-```text
-landing/
-└── ui/
- └── hero/
- ├── hero.tsx
- ├── styles/
- │ └── hero.module.css
- └── types/
- └── hero-props.type.ts
-```
-
-Наличие каталога, типов, стилей или локального `index.ts` не превращает компонент в модуль.
-
-## Реализация
-
-Компонент может отображать входные данные, вызывать переданные обработчики, условно строить интерфейс и хранить локальное состояние представления.
-
-SLM не вводит отдельного запрета на импорты, выполняемые кодом компонента. Каждый такой импорт считается зависимостью родительского модуля и должен соблюдать направление слоёв, публичные фасеты и запрет циклов.
-
-Доступ к данным, состояние, контекст или код жизненного цикла внутри компонента сами по себе не создают новую архитектурную границу. Их источники, зависимости и область жизни определяет родительский модуль.
-
-Провайдер может технически реализовывать контекст и жизненный цикл фреймворка, но владельцем состояния и ресурсов остаётся родительский модуль.
-
-Файл в `app` может технически быть компонентом React или Vue. Архитектурно он является точкой входа фреймворка, а не компонентом SLM.
-
-## Компонент и модуль
-
-| Признак | Компонент | Модуль |
-|---|---|---|
-| Самостоятельная ответственность | Нет | Да |
-| Собственный публичный API | Нет | Да |
-| Собственная граница зависимостей | Нет | Да |
-| Вспомогательные файлы | Может иметь | Может иметь |
-| Сегменты и вложенные модули | Нет | Может иметь |
-
-Модуль может состоять всего из одного корневого компонента. Различие определяется владением, а не количеством файлов.
-
-## Когда нужен вложенный модуль
-
-Если часть интерфейса получает самостоятельную ответственность, публичный API, собственную границу зависимостей, область жизни или внутреннюю модульную декомпозицию, она является модулем. При локальном использовании такой модуль может размещаться как вложенный.
diff --git a/DRAFT/architecture/dependencies.md b/DRAFT/architecture/dependencies.md
deleted file mode 100644
index 15a1629..0000000
--- a/DRAFT/architecture/dependencies.md
+++ /dev/null
@@ -1,87 +0,0 @@
-# Зависимости SLM
-
-> Пояснение нормативной модели зависимостей SLM.
-
-Матрица слоёв задаёт допустимые зависимости между модулями.
-
-## Что считается зависимостью
-
-- Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
-- Импорт между файлами одного модуля не пересекает модульную границу.
-- Импорт любого внутреннего файла, сегмента или компонента считается зависимостью ближайшего модуля-владельца.
-- Вложенный модуль имеет собственную границу зависимостей.
-- Группы, сегменты и компоненты не имеют самостоятельных границ зависимостей.
-
-Точка входа фреймворка не является модулем. Её импорты участвуют в проверке направления слоёв.
-
-Ресурс `shared` также не является модулем. Его прямой импорт участвует в проверке направления слоёв, но не нарушает требование о публичном API модуля.
-
-## Допустимые связи
-
-- Модуль может импортировать модули своего слоя и слоёв, разрешённых нормативной матрицей.
-- Модули одного слоя могут импортировать друг друга.
-- Промежуточный слой не является обязательным посредником.
-- `infra` может импортировать `ui` для визуального представления технической возможности; `ui` не импортирует `infra`.
-
-Модуль слоя `domains` может импортировать публичный API другого разрешённого модуля. Runtime- и type-only импорты одинаково создают архитектурную зависимость, а циклические зависимости запрещены.
-
-```ts
-// domains/orders
-import type { Product } from '@/domains/catalog'
-```
-
-SLM не требует отдельного механизма инъекции между модулями слоя `domains`.
-
-Матрица слоёв определена в [Слоях](./layers.md).
-
-## Связанные правила
-
-- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
-- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
-- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
-- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
-- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
-- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
-- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
-- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)
-
-## Публичный API
-
-```ts
-// Допустимо
-import { Button } from '@/ui/button'
-
-// Недопустимо
-import { Button } from '@/ui/button/button'
-```
-
-Если модуль объявляет специализированный фасет, его путь также является публичным:
-
-```ts
-import type { Session } from '@/domains/auth'
-import { AuthProvider } from '@/domains/auth/client'
-import { getServerSession } from '@/domains/auth/server'
-```
-
-## Фасеты сред выполнения
-
-Ограничения фасета распространяются на все его runtime-импорты и реэкспорты, включая транзитивные. Type-only import остаётся архитектурной зависимостью, но не добавляет исполняемый код в среду фасета.
-
-`index` не импортирует и не реэкспортирует `client`, `browser` или `server`. `client` может использовать универсальную внутреннюю реализацию модуля, но не импортирует `browser` или `server`. `browser` и `server` могут использовать универсальную внутреннюю реализацию, но не импортируют друг друга.
-
-```ts
-// Browser-only фасет подключается только за границей без SSR.
-const BrowserEditor = dynamic(
- () => import('@/domains/editor/browser').then(({ BrowserEditor }) => BrowserEditor),
- { ssr: false },
-)
-```
-
-Обычный статический импорт `@/domains/editor/browser` запрещён даже внутри Client Component. Tree shaking, проверка `typeof window` и обещание не вызывать экспорт при SSR не заменяют динамическую границу с отключённым SSR.
-
-## Циклы
-
-```text
-ui/modal → ui/button → ui/icon
-ui/icon -/→ ui/modal
-```
diff --git a/DRAFT/architecture/domains.md b/DRAFT/architecture/domains.md
deleted file mode 100644
index a33ff8d..0000000
--- a/DRAFT/architecture/domains.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Модули слоя domains
-
-> Пояснение размещения предметных ответственностей в обычных SLM-модулях.
-
-## Связанные правила
-
-- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
-- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
-- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
-- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001)
-
-## Обычные модули SLM
-
-Модуль слоя `domains` является обычным SLM-модулем. Он отличается от модулей других слоёв только предметной ответственностью, а не отдельной архитектурной сущностью или обязательной файловой формой.
-
-```text
-domains/auth/
-├── hooks/
-├── services/
-├── stores/
-├── types/
-├── ui/
-└── index.ts
-```
-
-Показанные каталоги являются возможными сегментами, а не обязательным каркасом. Модуль может содержать предметные типы, сценарии, состояние, framework-код, компоненты и вложенные модули.
-
-## Публичный API
-
-Внешний код использует модуль через его обычный публичный API:
-
-```ts
-import { signOut, useSession } from '@/domains/auth'
-```
-
-Глубокий импорт во внутренний сегмент нарушает модульную границу:
-
-```ts
-import { useSession } from '@/domains/auth/hooks/use-session'
-```
-
-## Groups
-
-При большом количестве модулей слой `domains` может содержать обычные навигационные Groups:
-
-```text
-domains/
-├── shop/ # Group
-│ ├── catalog/ # SLM-модуль
-│ └── orders/ # SLM-модуль
-└── cabinet/ # Group
- └── profile/ # SLM-модуль
-```
-
-Group не имеет `index.ts`, реализации, состояния или публичного API. Она не создаёт dependency boundary и не реэкспортирует содержащиеся в ней модули.
-
-## Среды выполнения
-
-Если части публичного API модуля предназначены для разных сред выполнения, модуль сохраняет одну ответственность и разделяет доступ фасетами `client`, `browser` и `server`.
diff --git a/DRAFT/architecture/groups.md b/DRAFT/architecture/groups.md
deleted file mode 100644
index 7878925..0000000
--- a/DRAFT/architecture/groups.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Группы SLM
-
-> Пояснение нормативной модели групп SLM.
-
-Группа помогает ориентироваться в большом количестве модулей. Она классифицирует структуру, но ничего не реализует и не образует самостоятельную границу зависимостей.
-
-## Связанное правило
-
-- [`SLM-GROUP-R007`](../rules/registry.md#slm-group-r007)
-- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
-
-## Пример
-
-```text
-compositions/
-├── pages/ # Группа
-│ ├── landing/ # Модуль
-│ └── contacts/ # Модуль
-└── layouts/ # Группа
- └── main/ # Модуль
-```
-
-`pages` и `layouts` являются возможной группировкой проекта, а не обязательной структурой SLM.
-
-Рекомендуется создавать группу только при реальной навигационной потребности. Если папка начинает владеть файлами реализации, состоянием, жизненным циклом или публичным API, она является модулем и должна получить модульную границу.
diff --git a/DRAFT/architecture/layers.md b/DRAFT/architecture/layers.md
deleted file mode 100644
index 9923638..0000000
--- a/DRAFT/architecture/layers.md
+++ /dev/null
@@ -1,99 +0,0 @@
-# Слои SLM
-
-> Пояснение нормативной модели слоёв SLM.
-
-## Базовая структура
-
-```text
-src/
-├── app/
-├── compositions/
-├── domains/
-├── infra/
-├── ui/
-└── shared/
-```
-
-`src/` здесь является примером SLM root. Фактическую границу определяет устройство приложения.
-
-Отсутствующая в проекте роль не требует пустой папки. Слой является доступной архитектурной ролью, а не обязательным элементом каркаса.
-
-## Роли слоёв
-
-### App
-
-`app` связывает приложение с фреймворком: запускает его, объявляет маршруты, преобразует входные данные и подключает публичные API модулей разрешённых слоёв или ресурсы `shared`. Файлы `app` являются точками входа фреймворка, а не модулями SLM.
-
-Точка входа может напрямую использовать `compositions`, `domains`, `infra`, `ui` или `shared`, если зависимость разрешена матрицей слоёв. Такое использование не переносит ответственность импортируемого модуля в `app`.
-
-### Compositions
-
-`compositions` содержит продуктовый интерфейс: страницы, макеты, экраны, виджеты, точки входа и другие композиционные модули. Внутреннюю группировку слоя определяет проект.
-
-### Domains
-
-`domains` содержит обычные SLM-модули, владеющие предметными ответственностями приложения: моделями, правилами, сценариями и продуктовым состоянием.
-
-Предметная логика не принадлежит устройству одной страницы или маршрута. Конкретный visual outcome и сборка нескольких предметных ответственностей остаются в `compositions`.
-
-### Infra
-
-`infra` содержит технические сервисы приложения: аналитику, локализацию, тему, телеметрию и другие возможности среды выполнения без самостоятельной предметной модели.
-
-### UI
-
-`ui` содержит универсальные модули интерфейса, которые не зависят от конкретной страницы или продуктовой композиции.
-
-### Shared
-
-`shared` содержит независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода.
-
-В `shared` допускаются обычные модули и специальные немодульные ресурсы: небольшие чистые утилиты, общие типы, стили, конфигурация и статические файлы. Ресурс не имеет самостоятельной ответственности, публичного API или жизненного цикла и может импортироваться напрямую по пути, установленному стайлгайдом.
-
-Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль. Каталог ресурсов не реэкспортирует модули и не используется для обхода их публичных API.
-
-## Матрица зависимостей
-
-```text
-app
- |
-compositions
- |
-domains
- |
-infra
- |
-ui
- |
-shared
-```
-
-Код слоя может импортировать модули своего слоя и слоёв, разрешённых строкой матрицы. Разрешённая зависимость может пропускать промежуточные роли.
-
-| Слой | Может импортировать |
-|---|---|
-| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
-| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
-| `domains` | `domains`, `infra`, `ui`, `shared` |
-| `infra` | `infra`, `ui`, `shared` |
-| `ui` | `ui`, `shared` |
-| `shared` | `shared` |
-
-`infra` может импортировать `ui`, когда технической возможности требуется визуальное представление, например CAPTCHA, платёжный виджет, карта, uploader, уведомление или инструмент разработчика. Такое представление остаётся частью технической ответственности и не переносит в `infra` страницы, продуктовые тексты или композицию нескольких модулей.
-
-`ui` не импортирует `infra`. Универсальный UI получает локализованный текст, тему, callbacks аналитики и другие технические возможности через входной контракт. Если UI-модулю необходимо напрямую знать конкретный технический сервис приложения, интеграция размещается в `infra`, `domains` или `compositions`, а универсальная часть остаётся в `ui`.
-
-Импорты внутри слоя, публичный API и циклы описаны отдельно в [Зависимостях](./dependencies.md).
-
-## Связанные правила
-
-- [`SLM-LAYER-R001`](../rules/registry.md#slm-layer-r001)
-- [`SLM-LAYER-A002`](../rules/registry.md#slm-layer-a002)
-- [`SLM-LAYER-R003`](../rules/registry.md#slm-layer-r003)
-- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
-
-## Граница слоя
-
-Разрешённый импорт не переносит владение. Например, модуль слоя `domains` может использовать `infra` и `ui`, но технический сервис и универсальный интерфейс сохраняют собственных владельцев.
-
-Если код определяет продуктовую модель, правило или сценарий, он принадлежит `domains`, а не `shared` или `infra`. Внутренняя форма домена определяется его ответственностью и реальными потребителями.
diff --git a/DRAFT/architecture/lifecycle.md b/DRAFT/architecture/lifecycle.md
deleted file mode 100644
index e804278..0000000
--- a/DRAFT/architecture/lifecycle.md
+++ /dev/null
@@ -1,31 +0,0 @@
-# Жизненный цикл
-
-> Пояснение нормативной модели владения ресурсами SLM.
-
-Жизненный цикл является частью ответственности модуля. Файл, компонент, провайдер или точка входа фреймворка могут технически создавать и останавливать ресурс, но архитектурным владельцем остаётся модуль.
-
-## Связанные правила
-
-- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
-- [`SLM-LIFECYCLE-R013`](../rules/registry.md#slm-lifecycle-r013)
-
-## Граница ресурса
-
-Для ресурса определяются:
-
-- модуль-владелец;
-- место создания;
-- момент начала работы;
-- область жизни;
-- допустимое число экземпляров;
-- способ остановки и очистки.
-
-Ресурс начинает работу не раньше начала своей области жизни и не остаётся активным после её завершения. Подписки, слушатели, таймеры, наблюдатели, запросы и соединения рассматриваются одинаково, если требуют явного завершения или отмены.
-
-## Реализация
-
-Очистку может выполнять сам модуль, компонент, провайдер или фреймворк. Способ реализации не меняет владельца и не переносит ответственность в технический файл.
-
-Одиночный экземпляр на всё приложение допустим только тогда, когда модуль действительно владеет областью жизни приложения или процесса. Размещение экземпляра на уровне файла само по себе этого не доказывает.
-
-Точка входа `app` может запускать или подключать ресурс через публичный API импортируемого модуля, но не становится его владельцем.
diff --git a/DRAFT/architecture/modules.md b/DRAFT/architecture/modules.md
deleted file mode 100644
index 21d86ff..0000000
--- a/DRAFT/architecture/modules.md
+++ /dev/null
@@ -1,66 +0,0 @@
-# Модули SLM
-
-> Пояснение нормативной модели модулей SLM.
-
-Модуль является основной архитектурной единицей SLM. Он размещается в отдельной папке, но может состоять только из публичной точки входа и одного файла реализации.
-
-## Связанные правила
-
-- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
-- [`SLM-MODULE-A014`](../rules/registry.md#slm-module-a014)
-- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
-- [`SLM-MODULE-R011`](../rules/registry.md#slm-module-r011)
-- [`SLM-MODULE-R012`](../rules/registry.md#slm-module-r012)
-- [`SLM-ENVIRONMENT-R016`](../rules/registry.md#slm-environment-r016)
-- [`SLM-ENVIRONMENT-R017`](../rules/registry.md#slm-environment-r017)
-- [`SLM-ENVIRONMENT-R018`](../rules/registry.md#slm-environment-r018)
-- [`SLM-ENVIRONMENT-R019`](../rules/registry.md#slm-environment-r019)
-
-## Владение
-
-Каждая самостоятельная ответственность имеет одного модуля-владельца. Модуль определяет её публичный API, зависимости, состояние, область жизни и внутреннее устройство независимо от того, в каком файле выполняется конкретный код.
-
-Точки входа `app` и нормативные ресурсы `shared` являются единственными немодульными исключениями. Остальной код внутри SLM root либо принадлежит существующему модулю, либо образует новый модуль.
-
-## Публичный API
-
-Модуль предоставляет один логический публичный API. По умолчанию он представлен корневым `index`, который служит основным barrel. Если реальные потребители требуют разделить несовместимые среды выполнения, модуль добавляет один или несколько фасетов `client`, `browser` и `server`.
-
-Объявленные фасеты вместе образуют один публичный API и не считаются deep imports. Внешний код использует модуль только через них. Сам API открывает только контракт, необходимый реальным внешним потребителям; внутренние механизмы, изменяемое состояние и детали жизненного цикла остаются закрытыми.
-
-```text
-auth/
-├── index.ts # Универсальный фасет
-├── client.ts # Необязательная клиентская framework-граница
-├── browser.ts # Необязательная browser-only граница
-├── server.ts # Необязательная server-only граница
-└── ... # Внутренняя реализация
-```
-
-`index` экспортирует публичные типы и runtime-код, который одинаково допустимо выполнять при серверном рендеринге, включая RSC, и в клиентском runtime. Он не реэкспортирует специализированные фасеты.
-
-`client` экспортирует Client Components, hooks, Providers и другой код, который не может выполняться как RSC. Такой код может быть отмечен директивой вроде `use client`.
-
-`browser` экспортирует browser-only возможности и lazy-функциональность. Потребитель подключает этот фасет только динамически через поддерживаемую фреймворком границу с отключённым SSR.
-
-`server` экспортирует только server-only возможности. `index`, `client` и `browser` не импортируют и не реэкспортируют его код.
-
-Фасет не создаётся для симметрии или будущей потребности. Один публичный runtime-export размещается в минимально подходящем фасете и не дублируется между фасетами.
-
-## Внутреннее устройство
-
-Модуль может содержать корневые файлы, сегменты, компоненты и [вложенные модули](./nested-modules.md). Внутри своей границы он может использовать относительные импорты и не обязан обращаться к собственному публичному API; точную форму внутренних импортов определяет стайлгайд.
-
-SLM не требует полного каркаса или обязательного каталога сегментов. Файлы фасетов являются публичными точками входа, а не сегментами или самостоятельными модулями.
-
-## Визуальный модуль
-
-Визуальный модуль обычно имеет корневой компонент, который экспортируется через публичный API.
-
-```text
-button/
-├── button.tsx
-└── index.ts
-```
-
-Корневой компонент остаётся компонентом, а владельцем ответственности является модуль `button`.
diff --git a/DRAFT/architecture/nested-modules.md b/DRAFT/architecture/nested-modules.md
deleted file mode 100644
index 51b8707..0000000
--- a/DRAFT/architecture/nested-modules.md
+++ /dev/null
@@ -1,30 +0,0 @@
-# Вложенные модули
-
-> Пояснение нормативной модели вложенных модулей SLM.
-
-Вложенный модуль является обычным модулем, размещённым внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется всем общим правилам модулей.
-
-## Связанные правила
-
-- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
-- [`SLM-MODULE-R006`](../rules/registry.md#slm-module-r006)
-- [`SLM-DEPENDENCY-A005`](../rules/registry.md#slm-dependency-a005)
-- [`SLM-NESTED_MODULE-A010`](../rules/registry.md#slm-nested_module-a010)
-
-## Пример
-
-```text
-landing/
-├── landing.page.tsx
-├── parts/
-│ └── hero/
-│ ├── hero.tsx
-│ └── index.ts
-└── index.ts
-```
-
-`parts/` здесь является примером сегмента, а не обязательным именем.
-
-Код родительского модуля использует вложенный модуль через его собственный публичный API. Код за пределами родительского модуля получает доступ только через публичный API родителя.
-
-Если вложенный модуль становится нужен за пределами родителя, рекомендуется перенести его в минимальную общую область без изменения внутренней формы. Доступ через API родителя при этом остаётся допустимым и сам по себе не требует переноса.
diff --git a/DRAFT/architecture/segments.md b/DRAFT/architecture/segments.md
deleted file mode 100644
index 096d094..0000000
--- a/DRAFT/architecture/segments.md
+++ /dev/null
@@ -1,29 +0,0 @@
-# Сегменты SLM
-
-> Пояснение нормативной модели сегментов SLM.
-
-Сегмент организует внутреннее содержимое модуля. SLM определяет роль сегмента, но не задаёт обязательный список имён.
-
-## Связанное правило
-
-- [`SLM-SEGMENT-R008`](../rules/registry.md#slm-segment-r008)
-- [`SLM-MODULE-A004`](../rules/registry.md#slm-module-a004)
-
-## Файловая форма
-
-Названия, набор и содержимое сегментов определяет стайлгайд проекта. Сегмент может группировать файлы, компоненты или вложенные модули.
-
-Файлы и компоненты сегмента принадлежат родительскому модулю. Вложенный модуль внутри сегмента сохраняет собственные ответственность, публичный API и границу зависимостей.
-
-## Пример
-
-```text
-landing/ # Модуль
-└── ui/ # Сегмент модуля
- └── hero/ # Каталог компонента
- ├── hero.tsx
- ├── styles/ # Вспомогательный каталог компонента
- └── types/ # Вспомогательный каталог компонента
-```
-
-`styles/` и `types/` внутри каталога компонента не обязаны считаться сегментами SLM. Их форму определяет стайлгайд компонентов.
diff --git a/DRAFT/architecture/terminology.md b/DRAFT/architecture/terminology.md
deleted file mode 100644
index 075aaca..0000000
--- a/DRAFT/architecture/terminology.md
+++ /dev/null
@@ -1,153 +0,0 @@
-# Терминология SLM
-
-> Нормативные определения рабочего черновика. Этот раздел не объявляет правила.
-
-Определения задают обязательный смысл архитектурных терминов и используются при толковании всех правил. Код получают только блокирующие требования, а не сами определения.
-
-## Базовые понятия
-
-### SLM root
-
-Граница структурной архитектуры одного приложения. Внутри неё определяются слои, модули и зависимости SLM. Монорепозиторий, пакеты и отношения между несколькими SLM root находятся за пределами текущего черновика.
-
-### Ответственность
-
-Связная часть приложения с одной причиной изменяться. Ответственность является самостоятельной, когда ей нужны собственные публичный API, зависимости, состояние или область жизни.
-
-### Владелец
-
-Модуль, который определяет публичный API ответственности, её зависимости, состояние, область жизни и внутреннее устройство. Место выполнения кода не переносит владение.
-
-### Публичный API
-
-Единая логическая граница внешнего доступа к модулю. Публичный API скрывает внутреннее устройство и состоит из обязательного корневого фасета `index` и только реально необходимых environment-фасетов `client`, `browser` и `server`.
-
-### Фасет
-
-Объявленная публичная точка входа модуля, которая открывает часть его единого логического API для определённой среды или способа выполнения. Импорт объявленного фасета не является deep import. Любой другой путь внутрь модуля остаётся внутренним.
-
-Корневой фасет `index` является основным barrel модуля. Он экспортирует публичные типы и runtime-код, совместимый как с серверным рендерингом, включая React Server Components, так и с клиентским выполнением.
-
-Необязательные environment-фасеты имеют следующий нормативный смысл:
-
-| Фасет | Среда и способ выполнения |
-|---|---|
-| `client` | Клиентская framework-граница, которая может участвовать в server prerender и затем выполняться при hydration и в браузере |
-| `browser` | Browser-only код, подключаемый только динамически с отключённым SSR |
-| `server` | Server-only код, недоступный через универсальный, клиентский и браузерный фасеты |
-
-Client Component, импортированный Server Component, не становится универсальным кодом и не экспортируется через `index`. Совместимость фасета со средой определяется всеми его runtime-импортами и реэкспортами, включая транзитивные.
-
-### Зависимость
-
-Статическая связь внутри одного SLM root, которую импорт или реэкспорт создаёт между архитектурными границами. Обычный импорт, импорт типа и реэкспорт одинаково создают архитектурную зависимость.
-
-Зависимость любого внутреннего файла, сегмента или компонента относится к ближайшему модулю-владельцу. Вложенный модуль начинает собственную границу зависимостей.
-
-### Область жизни
-
-Период, в течение которого принадлежащие модулю состояние или долгоживущий ресурс должны оставаться активными.
-
-### Ресурс жизненного цикла
-
-Ресурс, работа которого продолжается после первоначального вызова и требует остановки, отмены, отписки или освобождения. Например, подписка, слушатель событий, таймер, наблюдатель, запрос или соединение.
-
-### Очистка
-
-Гарантированное прекращение работы ресурса не позже завершения его области жизни. Автоматическая очистка фреймворка считается очисткой владельца, если модуль устанавливает и контролирует соответствующую границу.
-
-## Структурные сущности
-
-### Нормативная матрица слоёв
-
-Отношение допустимой зависимости между слоями одного SLM root. Матрица определяет, код каких слоёв может импортировать исходный слой; она не обязана образовывать линейный порядок.
-
-Для SLM нормативно отношение `app → compositions → domains → infra → ui → shared`. `infra` может импортировать `ui`, а `ui` не импортирует `infra`. Промежуточный слой не является обязательным посредником.
-
-### Слой
-
-Одна из шести верхнеуровневых ролей внутри SLM root:
-
-| Слой | Роль |
-|---|---|
-| `app` | Связь приложения с фреймворком: запуск, маршруты и преобразование входных данных |
-| `compositions` | Сборка продуктового интерфейса: страницы, макеты, экраны, виджеты и другие композиции |
-| `domains` | Предметные области, их модели, правила, сценарии и продуктовое состояние |
-| `infra` | Технические сервисы и возможности приложения |
-| `ui` | Универсальные модули интерфейса без зависимости от конкретной продуктовой композиции |
-| `shared` | Независимый детерминированный фундамент без знания о продукте, изменяемого состояния и ввода-вывода |
-
-Полная матрица допустимых зависимостей:
-
-| Исходный слой | Допустимые целевые слои |
-|---|---|
-| `app` | `app`, `compositions`, `domains`, `infra`, `ui`, `shared` |
-| `compositions` | `compositions`, `domains`, `infra`, `ui`, `shared` |
-| `domains` | `domains`, `infra`, `ui`, `shared` |
-| `infra` | `infra`, `ui`, `shared` |
-| `ui` | `ui`, `shared` |
-| `shared` | `shared` |
-
-### Модуль
-
-Минимальная самостоятельная архитектурная единица SLM. Модуль владеет одной связной ответственностью, размещается в отдельной папке и предоставляет публичный API.
-
-### Предметная ответственность
-
-Связная предметная область приложения, которая может включать собственные модели, правила, сценарии и продуктовое состояние. Количество экранов, endpoint-ов, hooks или файлов само по себе не определяет её границу.
-
-### Группа
-
-Навигационная папка для модулей и других групп. Группа не является владельцем ответственности, состояния, области жизни, публичного API или границы зависимостей.
-
-### Сегмент
-
-Внутренняя часть одного модуля, которая группирует его содержимое по назначению. Сегмент не является самостоятельным владельцем, публичным API или границей зависимостей.
-
-### Компонент
-
-Сущность фреймворка, которая реализует часть интерфейса родительского модуля. Компонент не образует собственного владельца, публичного API или границы зависимостей.
-
-Импорты, состояние, доступ к данным и код жизненного цикла компонента принадлежат родительскому модулю. Их наличие само по себе не создаёт новый модуль; решающим признаком является самостоятельная ответственность.
-
-### Вложенный модуль
-
-Обычный модуль, размещённый внутри границы родительского модуля. Он имеет собственные ответственность, публичный API и границу зависимостей и подчиняется всем общим правилам модулей.
-
-Публичный API вложенного модуля доступен коду родительской границы. Для кода за пределами родительского модуля вложенный модуль остаётся внутренней реализацией родителя.
-
-### Точка входа фреймворка
-
-Специальная немодульная единица слоя `app`, которая непосредственно связывает приложение с фреймворком. Её импорты участвуют в проверке направления слоёв, но сама точка входа не является модулем.
-
-### Ресурс shared
-
-Специальная немодульная единица слоя `shared`: небольшая детерминированная утилита, общий тип, стиль, конфигурация или статический ресурс без продуктового знания, изменяемого состояния, ввода-вывода, области жизни и собственного публичного API.
-
-Ресурс `shared` может импортироваться напрямую по пути, установленному стайлгайдом, и не является модулем. Доступный по этому пути файл является всей единицей и не скрывает отдельное внутреннее устройство.
-
-Если ресурсу нужны самостоятельная ответственность, собственные архитектурные зависимости, несколько файлов реализации, изменяемое состояние, ввод-вывод или область жизни, он оформляется как модуль.
-
-## Структурная модель
-
-```text
-SLM root
-├── app
-│ └── точка входа фреймворка
-├── compositions | domains | infra | ui
-│ ├── группа
-│ │ └── модуль
-│ └── модуль
-│ ├── корневые файлы
-│ ├── сегмент
-│ │ ├── файлы
-│ │ ├── компоненты
-│ │ └── вложенные модули
-│ └── вложенный модуль
-└── shared
- ├── группа
- ├── модуль
- └── ресурс shared
-```
-
-Путь и имя папки сами по себе не определяют сущность. Её определяют ответственность, владелец и публичная граница. Физическое сопоставление путей с сущностями задаётся стайлгайдом или конфигурацией проверки проекта.
diff --git a/DRAFT/architecture/validation.md b/DRAFT/architecture/validation.md
deleted file mode 100644
index 625872e..0000000
--- a/DRAFT/architecture/validation.md
+++ /dev/null
@@ -1,51 +0,0 @@
-# Проверка SLM
-
-> Граница автоматической проверки и архитектурного ревью SLM.
-
-## Автоматическая проверка
-
-Проект, заявляющий соответствие SLM, сопоставляет физические пути с SLM root, слоями, модулями, Groups, вложенными модулями, публичными фасетами, точками входа `app` и ресурсами `shared`. Такое сопоставление задаётся стайлгайдом или конфигурацией проверки и не изменяет нормативный смысл сущностей.
-
-Каждое правило класса `A` должно быть реализовано проверкой проекта и блокировать её при нарушении. SLM не навязывает конкретный инструмент.
-
-Скрипт `draft-rules.js` проверяет только целостность документов: формат и уникальность кодов, ссылки и наличие тематических упоминаний. Он не проверяет архитектуру приложения.
-
-Актуальный список правил скрипт получает из [канонического реестра](../rules/registry.md).
-
-## Архитектурное ревью
-
-Правила класса `R` проверяются вручную. Статический анализ может обнаружить подозрительный код, но не способен окончательно определить:
-
-- ответственность и её владельца;
-- связность ответственности модуля;
-- соответствие кода роли слоя;
-- необходимость экспортов публичного API;
-- область жизни ресурса и достаточность очистки;
-- наличие самостоятельной границы у компонента, группы или сегмента.
-
-## Проверка фасетов
-
-Автоматическая проверка сопоставляет публичные пути модуля с фасетами `index`, `client`, `browser` и `server`, запрещает остальные внешние пути и проверяет их runtime-импорты и реэкспорты, включая транзитивные.
-
-Для проверки сред инструмент различает runtime imports, type-only imports и dynamic imports. Окончательное решение о соответствии экспортируемого кода назначению фасета принимается на ревью.
-
-На ревью проверяется:
-
-- экспортирует ли `index` только универсальные типы и runtime-код;
-- остаётся ли `client` совместимым с server prerender и browser hydration;
-- достигается ли `browser` только через dynamic boundary с отключённым SSR;
-- остаётся ли `server` недоступным через `index`, `client` и `browser`;
-- существует ли каждый специализированный фасет ради реального потребителя;
-- не дублируется ли один runtime-export между фасетами.
-
-Название файла, директива `use client`, tree shaking или локальная проверка `typeof window` сами по себе не доказывают совместимость кода со средой выполнения.
-
-## Проверка слоя domains
-
-На ревью определяется:
-
-- соответствует ли ответственность модуля предметной роли слоя `domains`;
-- не разделена ли одна область на соседние модули без самостоятельных владельцев;
-- не объединены ли в одном модуле несвязанные предметные области;
-- остаются ли страницы, маршруты и UI нескольких предметных ответственностей в `compositions`;
-- остаются ли самостоятельные технические сервисы без предметной модели в `infra`.
diff --git a/DRAFT/index.md b/DRAFT/index.md
deleted file mode 100644
index dd9cd26..0000000
--- a/DRAFT/index.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-layout: home
-title: SLM Design
-
-hero:
- name: SLM Design
- text: Последовательная архитектура фронтенд-приложений
- tagline: Слои, модули и явные публичные границы для приложений с несколькими средами выполнения.
- image:
- src: /logo.svg
- alt: SLM Design
- actions:
- - theme: brand
- text: Читать архитектуру
- link: /architecture/
- - theme: alt
- text: Реестр правил
- link: /rules/
-
-features:
- - title: Архитектурная база
- details: Шесть слоёв, модули, публичные API, зависимости, вложенные границы и жизненный цикл ресурсов.
- - title: Фасеты сред выполнения
- details: Универсальный index и специализированные client, browser и server фасеты для явного разделения сред выполнения.
- - title: Канонические правила
- details: Точные блокирующие требования отделены от определений, рекомендаций и примеров и собраны в едином реестре.
----
-
-## Что опубликовано
-
-Сайт содержит рабочий черновик архитектуры SLM и канонический реестр правил. Монорепозитории пока не входят в опубликованную документацию.
-
-Определения терминологии нормативны внутри черновика. Точные формулировки блокирующих требований находятся только в [реестре правил](/rules/registry).
diff --git a/DRAFT/rules/README.md b/DRAFT/rules/README.md
deleted file mode 100644
index a8787b0..0000000
--- a/DRAFT/rules/README.md
+++ /dev/null
@@ -1,117 +0,0 @@
-# Правила SLM
-
-> Статус: системный черновик. Не является нормативной спецификацией.
-
-Эта директория является единственным местом объявления правил SLM. Остальные черновики объясняют архитектуру и ссылаются на канонические коды, но не повторяют формулировки правил.
-
-## Что считается правилом
-
-Правило задаёт один блокирующий архитектурный инвариант.
-
-Определения, рекомендации, разрешения, примеры и открытые вопросы не получают код правила.
-
-Нормативные определения объявляются в [терминологии SLM](../architecture/terminology.md). Они обязательны для толкования правил, но нормативность определения сама по себе не превращает его в правило.
-
-## Код правила
-
-```text
-SLM-{group}-{class}{number}
-```
-
-| Часть | Значение |
-|---|---|
-| `SLM` | Принадлежность архитектуре SLM |
-| `group` | Раздел правил |
-| `class` | Способ проверки: `A` или `R` |
-| `number` | Глобально уникальный трёхзначный номер правила |
-
-## Способы проверки
-
-### `A`: автоматическая проверка
-
-Всё правило можно однозначно проверить программно без понимания предметного смысла кода. Нарушение такого правила должно блокировать автоматическую проверку.
-
-### `R`: проверка на ревью
-
-Для окончательного решения требуется понимание ответственности, владения или смысла зависимости. Линтер может проверять отдельные признаки, но не заменяет решение на ревью.
-
-Одно правило не разделяется на автоматическую и ручную копии только из-за разных способов проверки. Если существенная часть инварианта требует смыслового решения, всё правило получает класс `R`.
-
-## Разделы правил
-
-| Код | Раздел |
-|---|---|
-| `LAYER` | Слои |
-| `DEPENDENCY` | Зависимости |
-| `MODULE` | Модули |
-| `GROUP` | Группы |
-| `SEGMENT` | Сегменты |
-| `COMPONENT` | Компоненты |
-| `NESTED_MODULE` | Вложенные модули |
-| `LIFECYCLE` | Жизненный цикл |
-| `ENVIRONMENT` | Границы сред выполнения |
-
-Код раздела записывается полным английским именем в `UPPER_SNAKE_CASE`. Новый код добавляется в таблицу до первого использования.
-
-## Формат записи
-
-```md
-### SLM-MODULE-A004
-
-> **Публичный API модуля**
->
-> Каждый модуль предоставляет единый логический публичный API через обязательный корневой фасет `index` и, при необходимости, фасеты `client`, `browser` и `server`; код за пределами модуля импортирует его содержимое только через эти фасеты.
-```
-
-Код является заголовком третьего уровня и автоматически получает адрес для ссылки `#slm-module-a004`.
-
-Название и описание входят в одну цитату. Название выделяется жирным и служит кратким именем правила. Описание полностью формулирует требование и занимает одну физическую строку.
-
-Ссылка из тематического черновика:
-
-```md
-[`SLM-MODULE-A004`](./registry.md#slm-module-a004)
-```
-
-## Как формулировать правила
-
-1. Правило понятно без чтения тематической главы и опирается только на нормативные термины SLM.
-2. Правило защищает один архитектурный инвариант.
-3. Один инвариант получает один код независимо от числа участников и способов проверки.
-4. Название является кратким и устойчивым именем правила.
-5. Название обозначает предмет правила, а описание полностью формулирует требование.
-6. Описание объясняет допустимую границу и то, что считается нарушением.
-7. Описание раскрывает названный инвариант и не вводит второе независимое требование.
-8. Описание использует нормативные определения и не пересказывает их без необходимости.
-9. Название и описание используют человеческий язык и только необходимые архитектурные термины.
-10. Обоснование, подробности, примеры и исключения размещаются в тематическом черновике, а не в описании.
-11. Правило не создаётся отдельно с позиции владельца и потребителя, если обе формулировки защищают одну границу.
-12. Перед добавлением правила реестр проверяется на дубли и противоречия.
-13. Код присваивается после проверки правила на примерах и контрпримерах.
-
-## Нумерация
-
-1. Номер глобально уникален независимо от раздела и способа проверки.
-2. Номер не обозначает важность или порядок выполнения.
-3. Удалённый номер не переиспользуется для другого правила.
-4. При изменении способа проверки номер сохраняется, но меняется полный код.
-
-## Проверка качества
-
-Перед принятием правила нужно ответить «да»:
-
-- Понятно, о чём правило?
-- Название кратко и однозначно называет правило?
-- Понятно, что оно требует?
-- Понятно, что является нарушением?
-- Нельзя ли объединить его с существующим правилом?
-- Не содержит ли оно рекомендацию или разрешение?
-- Соответствует ли класс способу окончательной проверки?
-
-## Проверка документов
-
-Корневой скрипт `draft-rules.js` читает объявления только из этой директории, проверяет формат и уникальность кодов, валидирует ссылки из остальных черновиков и выводит правила разделами «Автоматические» и «Для ревью». Скрипт проверяет документы, а не архитектуру приложения.
-
-## Реестр
-
-- [Единый реестр правил SLM](./registry.md)
diff --git a/DRAFT/rules/registry.md b/DRAFT/rules/registry.md
deleted file mode 100644
index 098943e..0000000
--- a/DRAFT/rules/registry.md
+++ /dev/null
@@ -1,129 +0,0 @@
-# Реестр правил SLM
-
-Здесь собраны правила SLM. Это единственное место, где они формулируются; тематические черновики объясняют их и ссылаются на коды.
-
-## Размещение кода по слоям
-
-### SLM-LAYER-R001
-
-> **Назначение слоёв**
->
-> Код внутри SLM root размещается в слое, нормативная роль которого соответствует ответственности этого кода.
-
-### SLM-LAYER-A002
-
-> **Направление зависимостей**
->
-> Внутри одного SLM root код каждого слоя может зависеть только от кода целевых слоёв, разрешённых для него нормативной матрицей слоёв.
-
-### SLM-LAYER-R003
-
-> **Граница слоя `app`**
->
-> В `app` размещаются только точки входа фреймворка для запуска, маршрутов, преобразования входных данных и подключения публичных API модулей разрешённых слоёв или ресурсов `shared`; ответственности этих модулей остаются за пределами `app`.
-
-## Границы модулей
-
-### SLM-MODULE-A004
-
-> **Публичный API модуля**
->
-> Каждый модуль предоставляет единый логический публичный API через обязательный корневой фасет `index` и, при необходимости, фасеты `client`, `browser` и `server`; код за пределами модуля импортирует его содержимое только через эти фасеты.
-
-### SLM-MODULE-A014
-
-> **Папка модуля**
->
-> Каждый модуль размещается в отдельной папке; его публичный API и внутренняя реализация находятся внутри этой границы, а вложенные модули образуют собственные папки.
-
-### SLM-MODULE-R006
-
-> **Ответственность модуля**
->
-> Одна модульная граница содержит код одной связной ответственности; части, которые изменяются по несвязанным причинам, размещаются в разных модулях.
-
-### SLM-MODULE-R011
-
-> **Владелец ответственности**
->
-> Каждая самостоятельная ответственность и относящийся к ней код принадлежат ровно одному модулю; вне модульной границы допускаются только точки входа `app` и нормативные ресурсы `shared`.
-
-### SLM-MODULE-R012
-
-> **Состав публичного API**
->
-> Публичный API модуля открывает только контракт, необходимый реальным внешним потребителям; детали реализации и изменяемые внутренние механизмы остаются закрытыми.
-
-## Зависимости между модулями
-
-### SLM-DEPENDENCY-A005
-
-> **Циклические зависимости**
->
-> Зависимости между модулями внутри одного SLM root, включая вложенные модули, не образуют циклов.
-
-## Назначение групп
-
-### SLM-GROUP-R007
-
-> **Назначение группы**
->
-> Группа содержит только модули и другие группы, не владеет файлами реализации, состоянием, жизненным циклом или публичным API и не импортируется внешним кодом.
-
-## Назначение сегментов
-
-### SLM-SEGMENT-R008
-
-> **Граница сегмента**
->
-> Сегмент организует код только внутри одного модуля и не имеет собственной ответственности, публичного API или границы зависимостей.
-
-## Ответственность компонентов
-
-### SLM-COMPONENT-R009
-
-> **Ответственность компонента**
->
-> Компонент реализует часть ответственности одного родительского модуля; все его зависимости, состояние и жизненный цикл принадлежат этому модулю и не образуют самостоятельную архитектурную границу.
-
-## Границы вложенных модулей
-
-### SLM-NESTED_MODULE-A010
-
-> **Доступ к вложенному модулю**
->
-> Код за пределами родительского модуля не импортирует вложенный модуль напрямую и получает его экспорты только через публичный API родителя.
-
-## Жизненный цикл
-
-### SLM-LIFECYCLE-R013
-
-> **Жизненный цикл ресурсов**
->
-> Для каждого ресурса жизненного цикла модуль-владелец определяет создание, область жизни, число экземпляров и очистку; ресурс активен только внутри своей области жизни.
-
-## Границы сред выполнения
-
-### SLM-ENVIRONMENT-R016
-
-> **Универсальный фасет**
->
-> Корневой фасет `index` экспортирует только публичный код, совместимый как с серверным рендерингом, включая RSC, так и с клиентским выполнением, и не импортирует или реэкспортирует код фасетов `client`, `browser` или `server` прямо либо транзитивно.
-
-### SLM-ENVIRONMENT-R017
-
-> **Клиентский фасет**
->
-> Фасет `client` экспортирует только клиентский код, который не может выполняться как RSC, и не импортирует или реэкспортирует код фасетов `browser` или `server` прямо либо транзитивно.
-
-### SLM-ENVIRONMENT-R018
-
-> **Браузерный фасет**
->
-> Фасет `browser` экспортирует только browser-only код, а потребители импортируют его только динамически с отключённым SSR.
-
-### SLM-ENVIRONMENT-R019
-
-> **Серверный фасет**
->
-> Фасет `server` экспортирует только server-only код и не импортируется или реэкспортируется фасетами `index`, `client` или `browser` прямо либо транзитивно.
diff --git a/examples/demo-frontend/.env.example b/examples/demo-frontend/.env.example
deleted file mode 100644
index a7cf7df..0000000
--- a/examples/demo-frontend/.env.example
+++ /dev/null
@@ -1 +0,0 @@
-NEXT_PUBLIC_SIMPLE_API_URL=http://localhost:3001
diff --git a/examples/demo-frontend/.gitignore b/examples/demo-frontend/.gitignore
deleted file mode 100644
index 7b8da95..0000000
--- a/examples/demo-frontend/.gitignore
+++ /dev/null
@@ -1,42 +0,0 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.*
-.yarn/*
-!.yarn/patches
-!.yarn/plugins
-!.yarn/releases
-!.yarn/versions
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-.pnpm-debug.log*
-
-# env files (can opt-in for committing if needed)
-.env*
-!.env.example
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
diff --git a/examples/demo-frontend/AGENTS.md b/examples/demo-frontend/AGENTS.md
deleted file mode 100644
index 8bd0e39..0000000
--- a/examples/demo-frontend/AGENTS.md
+++ /dev/null
@@ -1,5 +0,0 @@
-
-# This is NOT the Next.js you know
-
-This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
-
diff --git a/examples/demo-frontend/CLAUDE.md b/examples/demo-frontend/CLAUDE.md
deleted file mode 100644
index 43c994c..0000000
--- a/examples/demo-frontend/CLAUDE.md
+++ /dev/null
@@ -1 +0,0 @@
-@AGENTS.md
diff --git a/examples/demo-frontend/README.md b/examples/demo-frontend/README.md
deleted file mode 100644
index 2e1b53f..0000000
--- a/examples/demo-frontend/README.md
+++ /dev/null
@@ -1,102 +0,0 @@
-# Demo frontend
-
-Полноценное Next.js 16 приложение поверх меньшего из двух fixture API: `../demo-backend/openapi/simple.json`.
-
-Пример показывает SLM Level 1 на каталоге: public data, JWT login/refresh/logout, persisted cart, checkout, protected orders, RBAC, admin CRUD, optimistic locking и детерминированные network outcomes.
-
-## Запуск
-
-Требуются Node.js 20+, npm 10+ и современный browser с Web Locks API. Auth и cart transitions работают fail-closed без cross-tab lock; `localhost` считается secure context для локальной разработки.
-
-Сначала запустите Simple API:
-
-```bash
-cd ../demo-backend
-npm install
-npm run dev:simple
-```
-
-Затем запустите frontend:
-
-```bash
-npm install
-npm run dev
-```
-
-Откройте `http://localhost:3000`. Backend по умолчанию доступен на `http://localhost:3001`.
-
-Чтобы изменить URL backend, создайте `.env.local` рядом с `package.json`:
-
-```bash
-NEXT_PUBLIC_SIMPLE_API_URL=http://localhost:3001
-```
-
-## Demo accounts
-
-| Email | Password | Role | Scenarios |
-|---|---|---|---|
-| `admin@demo.local` | `demo1234` | admin | Product CRUD, all orders |
-| `customer@demo.local` | `demo1234` | customer | Cart, checkout, own orders |
-
-## Routes
-
-| Route | Composition responsibility |
-|---|---|
-| `/` | Product search, category filter, sorting and pagination |
-| `/products/[productId]` | Product detail and add-to-cart |
-| `/cart` | Multi-domain checkout coordination |
-| `/orders` | Protected order history and cancellation |
-| `/sign-in` | JWT session lifecycle and account selection |
-| `/admin/products` | RBAC-gated CRUD and optimistic locking |
-
-Плавающий `Demo controls` widget переключает `X-Demo-Scenario`: slow, timeout, 500, 429, empty, expired auth, forbidden и conflict. Отдельное действие `Seed 250` загружает настоящий большой seed с интерактивными product IDs; synthetic `large-dataset` header намеренно не используется для mutation UI.
-
-## SLM boundary
-
-`src` является SLM root:
-
-```text
-src/
-├── app/ Next.js bootstrap and route entries
-├── compositions/ Layout, screens and demo widget
-├── domains/ Auth, catalog, cart, orders and demo-control
-├── infra/ Generated REST client, JWT storage and browser storage
-├── ui/ Product-agnostic button, form field and feedback panel
-└── shared/ Deterministic predicates, formatting and Result type
-```
-
-Пример использует Level 1 осознанно: у каждого домена одна browser runtime integration. Дополнительные factories, adapters и assemblies Level 2 не окупили бы стоимость. Checkout остаётся в composition, поэтому `orders` не импортирует `cart`, а module graph остаётся ацикличным.
-
-State и lifecycle распределены по владельцам:
-
-- `auth` владеет пользовательской сессией; `infra/simple-auth-session` хранит technical JWT pair, стабильный `sessionId` и CAS revision.
-- `cart` владеет строками, totals и persisted snapshot validation; monotonic revision и conditional clear сериализованы между вкладками.
-- SWR cache принадлежит REST infra module, создаётся отдельным provider и remount-ится при смене logical auth session.
-- React providers создаются один раз в application scope и очищают свои subscriptions при unmount.
-- Route entries только адаптируют Next.js params и подключают public APIs compositions.
-- Reset, seed, role и request-scenario transitions синхронизируют cache, auth, cart и route-local pagination между вкладками.
-
-## REST client
-
-Split SDK генерируется из committed OpenAPI:
-
-```bash
-npm run codegen:simple-rest-api
-```
-
-Generated-код живёт только в `src/infra/simple-rest-api/generated` и не редактируется вручную. GET hooks вызывают точечные operations через `simpleHttpClient`; submit-сценарии используют полный `simpleRestApi`. Внешний код импортирует REST capability только через `@/infra/simple-rest-api`.
-
-OpenAPI fixture описывает числовые `page` и `limit` как `object`. Исправление generated type изолировано в `types/to-generated-query.ts`; runtime query остаётся числовым.
-
-JWT refresh выполняется в transport `onError`, ограничен одним retry и дедуплицирует конкурентные refresh requests одной revision. CAS не позволяет позднему refresh воскресить logout или перезаписать новый login. Хранение refresh token в `localStorage` допустимо только для этой архитектурной fixture; production-приложение должно выбрать threat model и более безопасную session strategy.
-
-Checkout привязан к captured auth session и persisted cart revision. Backend атомарно проверяет product version, unit price, USD currency, aggregate stock и уникальность product lines. Timeout-сценарий откладывает mutation handler и отменяет его при disconnect, поэтому frontend timeout не скрывает завершившийся POST.
-
-## Проверка
-
-```bash
-npm run check
-npm run build
-```
-
-`npm run check` запускает architecture constraints, ESLint, TypeScript и Vitest. Architecture script строит import graph через TypeScript AST и явный module manifest, затем проверяет направление слоёв, public module imports, side-effect/dynamic imports, запрет утечки generated SDK и циклы.
diff --git a/examples/demo-frontend/eslint.config.mjs b/examples/demo-frontend/eslint.config.mjs
deleted file mode 100644
index fccd7e8..0000000
--- a/examples/demo-frontend/eslint.config.mjs
+++ /dev/null
@@ -1,37 +0,0 @@
-import js from '@eslint/js'
-import nextPlugin from '@next/eslint-plugin-next'
-import { defineConfig, globalIgnores } from 'eslint/config'
-import reactHooks from 'eslint-plugin-react-hooks'
-import globals from 'globals'
-import tseslint from 'typescript-eslint'
-
-export default defineConfig([
- globalIgnores([
- '.next/**',
- 'out/**',
- 'build/**',
- 'coverage/**',
- 'next-env.d.ts',
- 'src/infra/simple-rest-api/generated/**'
- ]),
- js.configs.recommended,
- ...tseslint.configs.recommended,
- {
- files: ['**/*.{js,mjs,mts,ts,tsx}'],
- plugins: {
- '@next/next': nextPlugin,
- 'react-hooks': reactHooks
- },
- languageOptions: {
- globals: {
- ...globals.browser,
- ...globals.node
- }
- },
- rules: {
- ...nextPlugin.configs.recommended.rules,
- ...nextPlugin.configs['core-web-vitals'].rules,
- ...reactHooks.configs.flat.recommended.rules
- }
- }
-])
diff --git a/examples/demo-frontend/next.config.ts b/examples/demo-frontend/next.config.ts
deleted file mode 100644
index 66ce684..0000000
--- a/examples/demo-frontend/next.config.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import { fileURLToPath } from 'node:url'
-import type { NextConfig } from 'next'
-
-const nextConfig: NextConfig = {
- turbopack: {
- root: fileURLToPath(new URL('.', import.meta.url))
- },
- images: {
- remotePatterns: [
- {
- protocol: 'https',
- hostname: 'picsum.photos',
- pathname: '/**'
- },
- {
- protocol: 'https',
- hostname: 'i.pravatar.cc',
- pathname: '/**'
- }
- ]
- }
-}
-
-export default nextConfig
diff --git a/examples/demo-frontend/package-lock.json b/examples/demo-frontend/package-lock.json
deleted file mode 100644
index dd9d3ef..0000000
--- a/examples/demo-frontend/package-lock.json
+++ /dev/null
@@ -1,4280 +0,0 @@
-{
- "name": "demo-frontend",
- "version": "0.1.0",
- "lockfileVersion": 3,
- "requires": true,
- "packages": {
- "": {
- "name": "demo-frontend",
- "version": "0.1.0",
- "dependencies": {
- "clsx": "2.1.1",
- "next": "16.2.12",
- "react": "19.2.8",
- "react-dom": "19.2.8",
- "swr": "2.4.2"
- },
- "devDependencies": {
- "@eslint/js": "10.0.1",
- "@next/eslint-plugin-next": "16.2.12",
- "@types/node": "^20",
- "@types/react": "^19",
- "@types/react-dom": "^19",
- "eslint": "10.8.0",
- "eslint-plugin-react-hooks": "7.1.1",
- "globals": "17.8.0",
- "typescript": "^5",
- "typescript-eslint": "8.65.0",
- "vitest": "4.1.10"
- }
- },
- "node_modules/@babel/code-frame": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
- "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-validator-identifier": "^7.29.7",
- "js-tokens": "^4.0.0",
- "picocolors": "^1.1.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/compat-data": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
- "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/core": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
- "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-compilation-targets": "^7.29.7",
- "@babel/helper-module-transforms": "^7.29.7",
- "@babel/helpers": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/remapping": "^2.3.5",
- "convert-source-map": "^2.0.0",
- "debug": "^4.1.0",
- "gensync": "^1.0.0-beta.2",
- "json5": "^2.2.3",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/babel"
- }
- },
- "node_modules/@babel/generator": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
- "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
- "jsesc": "^3.0.2"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-compilation-targets": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
- "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/compat-data": "^7.29.7",
- "@babel/helper-validator-option": "^7.29.7",
- "browserslist": "^4.24.0",
- "lru-cache": "^5.1.1",
- "semver": "^6.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-globals": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
- "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-imports": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
- "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/traverse": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-module-transforms": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
- "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-module-imports": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7",
- "@babel/traverse": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- },
- "peerDependencies": {
- "@babel/core": "^7.0.0"
- }
- },
- "node_modules/@babel/helper-string-parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
- "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-identifier": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
- "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helper-validator-option": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
- "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/helpers": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
- "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/parser": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
- "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.29.7"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@babel/template": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
- "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/types": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/traverse": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
- "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.29.7",
- "@babel/generator": "^7.29.7",
- "@babel/helper-globals": "^7.29.7",
- "@babel/parser": "^7.29.7",
- "@babel/template": "^7.29.7",
- "@babel/types": "^7.29.7",
- "debug": "^4.3.1"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@babel/types": {
- "version": "7.29.7",
- "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
- "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/helper-string-parser": "^7.29.7",
- "@babel/helper-validator-identifier": "^7.29.7"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/@emnapi/core": {
- "version": "1.10.0",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
- "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "@emnapi/wasi-threads": "1.2.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/runtime": {
- "version": "1.11.3",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
- "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@emnapi/wasi-threads": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
- "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "peer": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
- "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.12.2",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
- "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/config-array": {
- "version": "0.23.5",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
- "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^3.0.5",
- "debug": "^4.3.1",
- "minimatch": "^10.2.4"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- }
- },
- "node_modules/@eslint/config-helpers": {
- "version": "0.7.0",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz",
- "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^1.2.1"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- }
- },
- "node_modules/@eslint/core": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
- "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- }
- },
- "node_modules/@eslint/js": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
- "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "eslint": "^10.0.0"
- },
- "peerDependenciesMeta": {
- "eslint": {
- "optional": true
- }
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
- "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.2.tgz",
- "integrity": "sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^1.2.1",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- }
- },
- "node_modules/@humanfs/core": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
- "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/types": "^0.15.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.8",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
- "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/core": "^0.19.2",
- "@humanfs/types": "^0.15.0",
- "@humanwhocodes/retry": "^0.4.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/types": {
- "version": "0.15.0",
- "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
- "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/retry": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
- "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@img/colour": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
- "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/@img/sharp-darwin-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
- "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-darwin-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
- "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-freebsd-wasm32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
- "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "dependencies": {
- "@img/sharp-wasm32": "0.35.3"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
- "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
- "cpu": [
- "arm64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
- "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
- "cpu": [
- "x64"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
- "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
- "cpu": [
- "arm"
- ],
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
- "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
- "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
- "cpu": [
- "ppc64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-riscv64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
- "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
- "cpu": [
- "riscv64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
- "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
- "cpu": [
- "s390x"
- ],
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
- "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
- "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "musl"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
- "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "musl"
- ],
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-linux-arm": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
- "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
- "cpu": [
- "arm"
- ],
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linux-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
- "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linux-ppc64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
- "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
- "cpu": [
- "ppc64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linux-riscv64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
- "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
- "cpu": [
- "riscv64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-riscv64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linux-s390x": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
- "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
- "cpu": [
- "s390x"
- ],
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linux-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
- "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
- "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "musl"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
- "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "musl"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
- }
- },
- "node_modules/@img/sharp-wasm32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
- "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.11.1"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-webcontainers-wasm32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
- "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
- "cpu": [
- "wasm32"
- ],
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "@img/sharp-wasm32": "0.35.3"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-arm64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
- "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
- "cpu": [
- "arm64"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-ia32": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
- "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
- "cpu": [
- "ia32"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-x64": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
- "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
- "cpu": [
- "x64"
- ],
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
- "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
- "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
- }
- },
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
- "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
- "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
- "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
- "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/resolve-uri": "^3.1.0",
- "@jridgewell/sourcemap-codec": "^1.4.14"
- }
- },
- "node_modules/@napi-rs/wasm-runtime": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.1.tgz",
- "integrity": "sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@tybys/wasm-util": "^0.10.3"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/Brooooooklyn"
- },
- "peerDependencies": {
- "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3",
- "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3"
- }
- },
- "node_modules/@next/env": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz",
- "integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==",
- "license": "MIT"
- },
- "node_modules/@next/eslint-plugin-next": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.12.tgz",
- "integrity": "sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-glob": "3.3.1"
- }
- },
- "node_modules/@next/swc-darwin-arm64": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz",
- "integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-darwin-x64": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz",
- "integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz",
- "integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz",
- "integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==",
- "cpu": [
- "arm64"
- ],
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz",
- "integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-linux-x64-musl": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz",
- "integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==",
- "cpu": [
- "x64"
- ],
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz",
- "integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz",
- "integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
- "node_modules/@nodelib/fs.scandir": {
- "version": "2.1.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
- "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "2.0.5",
- "run-parallel": "^1.1.9"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.stat": {
- "version": "2.0.5",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
- "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@nodelib/fs.walk": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
- "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.scandir": "2.1.5",
- "fastq": "^1.6.0"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/@oxc-project/types": {
- "version": "0.142.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz",
- "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/Boshen"
- }
- },
- "node_modules/@rolldown/binding-android-arm64": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz",
- "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz",
- "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz",
- "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz",
- "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz",
- "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz",
- "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz",
- "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz",
- "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz",
- "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz",
- "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz",
- "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz",
- "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "openharmony"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz",
- "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/core": "2.0.0-alpha.3",
- "@emnapi/runtime": "2.0.0-alpha.3",
- "@napi-rs/wasm-runtime": "^1.2.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=23.5.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/core": {
- "version": "2.0.0-alpha.3",
- "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz",
- "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/wasi-threads": "2.0.1",
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/runtime": {
- "version": "2.0.0-alpha.3",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz",
- "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@rolldown/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz",
- "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz",
- "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz",
- "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- }
- },
- "node_modules/@rolldown/pluginutils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
- "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@standard-schema/spec": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
- "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@swc/helpers": {
- "version": "0.5.15",
- "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
- "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
- "license": "Apache-2.0",
- "dependencies": {
- "tslib": "^2.8.0"
- }
- },
- "node_modules/@tybys/wasm-util": {
- "version": "0.10.3",
- "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz",
- "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
- "node_modules/@types/chai": {
- "version": "5.2.3",
- "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
- "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/deep-eql": "*",
- "assertion-error": "^2.0.1"
- }
- },
- "node_modules/@types/deep-eql": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
- "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/esrecurse": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
- "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/estree": {
- "version": "1.0.9",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
- "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/@types/node": {
- "version": "20.19.43",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
- "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "undici-types": "~6.21.0"
- }
- },
- "node_modules/@types/react": {
- "version": "19.2.18",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz",
- "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "csstype": "^3.2.2"
- }
- },
- "node_modules/@types/react-dom": {
- "version": "19.2.4",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz",
- "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "@types/react": "^19.2.0"
- }
- },
- "node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
- "integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/regexpp": "^4.12.2",
- "@typescript-eslint/scope-manager": "8.65.0",
- "@typescript-eslint/type-utils": "8.65.0",
- "@typescript-eslint/utils": "8.65.0",
- "@typescript-eslint/visitor-keys": "8.65.0",
- "ignore": "^7.0.5",
- "natural-compare": "^1.4.0",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "@typescript-eslint/parser": "^8.65.0",
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz",
- "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/@typescript-eslint/parser": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.65.0.tgz",
- "integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/scope-manager": "8.65.0",
- "@typescript-eslint/types": "8.65.0",
- "@typescript-eslint/typescript-estree": "8.65.0",
- "@typescript-eslint/visitor-keys": "8.65.0",
- "debug": "^4.4.3"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/project-service": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
- "integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/tsconfig-utils": "^8.65.0",
- "@typescript-eslint/types": "^8.65.0",
- "debug": "^4.4.3"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/scope-manager": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
- "integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.65.0",
- "@typescript-eslint/visitor-keys": "8.65.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/tsconfig-utils": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
- "integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/type-utils": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
- "integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.65.0",
- "@typescript-eslint/typescript-estree": "8.65.0",
- "@typescript-eslint/utils": "8.65.0",
- "debug": "^4.4.3",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/types": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.65.0.tgz",
- "integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
- "integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/project-service": "8.65.0",
- "@typescript-eslint/tsconfig-utils": "8.65.0",
- "@typescript-eslint/types": "8.65.0",
- "@typescript-eslint/visitor-keys": "8.65.0",
- "debug": "^4.4.3",
- "minimatch": "^10.2.2",
- "semver": "^7.7.3",
- "tinyglobby": "^0.2.15",
- "ts-api-utils": "^2.5.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/@typescript-eslint/utils": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.65.0.tgz",
- "integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.9.1",
- "@typescript-eslint/scope-manager": "8.65.0",
- "@typescript-eslint/types": "8.65.0",
- "@typescript-eslint/typescript-estree": "8.65.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
- "integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/types": "8.65.0",
- "eslint-visitor-keys": "^5.0.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- }
- },
- "node_modules/@vitest/expect": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
- "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@standard-schema/spec": "^1.1.0",
- "@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.10",
- "@vitest/utils": "4.1.10",
- "chai": "^6.2.2",
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/mocker": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz",
- "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/spy": "4.1.10",
- "estree-walker": "^3.0.3",
- "magic-string": "^0.30.21"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "msw": "^2.4.9",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "msw": {
- "optional": true
- },
- "vite": {
- "optional": true
- }
- }
- },
- "node_modules/@vitest/pretty-format": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz",
- "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/runner": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz",
- "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/utils": "4.1.10",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/snapshot": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz",
- "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.10",
- "@vitest/utils": "4.1.10",
- "magic-string": "^0.30.21",
- "pathe": "^2.0.3"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/spy": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz",
- "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/@vitest/utils": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz",
- "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/pretty-format": "4.1.10",
- "convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- }
- },
- "node_modules/acorn": {
- "version": "8.18.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz",
- "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.15.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
- "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/assertion-error": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
- "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/balanced-match": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
- "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.11.8",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.8.tgz",
- "integrity": "sha512-zAgkquC2WYF0PIc6XbNYkA2uuxxFavzgmX61R+dHDUa558V8Ejf8ozTZFR6QzM24RWu4kBcRkhJ5kpz77j9fnQ==",
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/brace-expansion": {
- "version": "5.0.9",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
- "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "balanced-match": "^4.0.2"
- },
- "engines": {
- "node": "20 || >=22"
- }
- },
- "node_modules/braces": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
- "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fill-range": "^7.1.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/browserslist": {
- "version": "4.28.7",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
- "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "baseline-browser-mapping": "^2.10.44",
- "caniuse-lite": "^1.0.30001806",
- "electron-to-chromium": "^1.5.393",
- "node-releases": "^2.0.51",
- "update-browserslist-db": "^1.2.3"
- },
- "bin": {
- "browserslist": "cli.js"
- },
- "engines": {
- "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
- }
- },
- "node_modules/caniuse-lite": {
- "version": "1.0.30001806",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
- "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "CC-BY-4.0"
- },
- "node_modules/chai": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
- "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/client-only": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
- "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
- "license": "MIT"
- },
- "node_modules/clsx": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
- "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/convert-source-map": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
- "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/cross-spawn": {
- "version": "7.0.6",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
- "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-key": "^3.1.0",
- "shebang-command": "^2.0.0",
- "which": "^2.0.1"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/csstype": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/dequal": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
- "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/detect-libc": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
- "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
- "devOptional": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/electron-to-chromium": {
- "version": "1.5.398",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.398.tgz",
- "integrity": "sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/es-module-lexer": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz",
- "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "10.8.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.0.tgz",
- "integrity": "sha512-nuKKvN+oIBO0koN7Tm7dlkmnkc21mtt0QJLwAKzjLq14y6lRTdVG36MZHJ8eQHwdJMwZbQNMlPOYedMq/oVJvQ==",
- "dev": true,
- "license": "MIT",
- "workspaces": [
- "packages/*"
- ],
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.8.0",
- "@eslint-community/regexpp": "^4.12.2",
- "@eslint/config-array": "^0.23.5",
- "@eslint/config-helpers": "^0.7.0",
- "@eslint/core": "^1.2.1",
- "@eslint/plugin-kit": "^0.7.2",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "ajv": "^6.14.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^9.1.2",
- "eslint-visitor-keys": "^5.0.1",
- "espree": "^11.2.0",
- "esquery": "^1.7.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "minimatch": "^10.2.5",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-plugin-react-hooks": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
- "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/core": "^7.24.4",
- "@babel/parser": "^7.24.4",
- "hermes-parser": "^0.25.1",
- "zod": "^3.25.0 || ^4.0.0",
- "zod-validation-error": "^3.5.0 || ^4.0.0"
- },
- "engines": {
- "node": ">=18"
- },
- "peerDependencies": {
- "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
- }
- },
- "node_modules/eslint-scope": {
- "version": "9.1.2",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
- "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "@types/esrecurse": "^4.3.1",
- "@types/estree": "^1.0.8",
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
- "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/espree": {
- "version": "11.2.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
- "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.16.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^5.0.1"
- },
- "engines": {
- "node": "^20.19.0 || ^22.13.0 || >=24"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
- "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estree-walker": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
- "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/estree": "^1.0.0"
- }
- },
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/expect-type": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
- "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.0.0"
- }
- },
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-glob": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz",
- "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@nodelib/fs.stat": "^2.0.2",
- "@nodelib/fs.walk": "^1.2.3",
- "glob-parent": "^5.1.2",
- "merge2": "^1.3.0",
- "micromatch": "^4.0.4"
- },
- "engines": {
- "node": ">=8.6.0"
- }
- },
- "node_modules/fast-glob/node_modules/glob-parent": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
- "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.1"
- },
- "engines": {
- "node": ">= 6"
- }
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fastq": {
- "version": "1.20.1",
- "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz",
- "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "reusify": "^1.0.4"
- }
- },
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "to-regex-range": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/flatted": {
- "version": "3.4.4",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz",
- "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/fsevents": {
- "version": "2.3.3",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
- "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
- "dev": true,
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
- }
- },
- "node_modules/gensync": {
- "version": "1.0.0-beta.2",
- "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
- "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
- "node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/globals": {
- "version": "17.8.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz",
- "integrity": "sha512-Zz/LMDZScFmkakeL2cTHzf+PbWKdpU3uclqkZT7TjDG58j5WPt0PpA+n9uPI24fZtlw07q0OtEi84K+umsRzqQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/hermes-estree": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
- "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/hermes-parser": {
- "version": "0.25.1",
- "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
- "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "hermes-estree": "0.25.1"
- }
- },
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/is-number": {
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
- "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.12.0"
- }
- },
- "node_modules/isexe": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
- "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/js-tokens": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
- "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/jsesc": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
- "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "jsesc": "bin/jsesc"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json5": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
- "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "json5": "lib/cli.js"
- },
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/lightningcss": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
- "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
- "dev": true,
- "license": "MPL-2.0",
- "dependencies": {
- "detect-libc": "^2.0.3"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- },
- "optionalDependencies": {
- "lightningcss-android-arm64": "1.33.0",
- "lightningcss-darwin-arm64": "1.33.0",
- "lightningcss-darwin-x64": "1.33.0",
- "lightningcss-freebsd-x64": "1.33.0",
- "lightningcss-linux-arm-gnueabihf": "1.33.0",
- "lightningcss-linux-arm64-gnu": "1.33.0",
- "lightningcss-linux-arm64-musl": "1.33.0",
- "lightningcss-linux-x64-gnu": "1.33.0",
- "lightningcss-linux-x64-musl": "1.33.0",
- "lightningcss-win32-arm64-msvc": "1.33.0",
- "lightningcss-win32-x64-msvc": "1.33.0"
- }
- },
- "node_modules/lightningcss-android-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
- "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "android"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-arm64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
- "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-darwin-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
- "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-freebsd-x64": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
- "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "freebsd"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm-gnueabihf": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
- "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
- "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-arm64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
- "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-gnu": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
- "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "glibc"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
- "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "libc": [
- "musl"
- ],
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-arm64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
- "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/lightningcss-win32-x64-msvc": {
- "version": "1.33.0",
- "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
- "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lru-cache": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
- "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "yallist": "^3.0.2"
- }
- },
- "node_modules/magic-string": {
- "version": "0.30.21",
- "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
- "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.5"
- }
- },
- "node_modules/merge2": {
- "version": "1.4.1",
- "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
- "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/micromatch": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
- "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "braces": "^3.0.3",
- "picomatch": "^2.3.1"
- },
- "engines": {
- "node": ">=8.6"
- }
- },
- "node_modules/minimatch": {
- "version": "10.2.6",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
- "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
- "dev": true,
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "brace-expansion": "^5.0.8"
- },
- "engines": {
- "node": "18 || 20 || >=22"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
- "node_modules/ms": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
- "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/nanoid": {
- "version": "3.3.16",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
- "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "bin": {
- "nanoid": "bin/nanoid.cjs"
- },
- "engines": {
- "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
- }
- },
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/next": {
- "version": "16.2.12",
- "resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz",
- "integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==",
- "license": "MIT",
- "dependencies": {
- "@next/env": "16.2.12",
- "@swc/helpers": "0.5.15",
- "baseline-browser-mapping": "^2.9.19",
- "caniuse-lite": "^1.0.30001579",
- "postcss": "8.4.31",
- "styled-jsx": "5.1.6"
- },
- "bin": {
- "next": "dist/bin/next"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "optionalDependencies": {
- "@next/swc-darwin-arm64": "16.2.12",
- "@next/swc-darwin-x64": "16.2.12",
- "@next/swc-linux-arm64-gnu": "16.2.12",
- "@next/swc-linux-arm64-musl": "16.2.12",
- "@next/swc-linux-x64-gnu": "16.2.12",
- "@next/swc-linux-x64-musl": "16.2.12",
- "@next/swc-win32-arm64-msvc": "16.2.12",
- "@next/swc-win32-x64-msvc": "16.2.12",
- "sharp": "^0.34.5"
- },
- "peerDependencies": {
- "@opentelemetry/api": "^1.1.0",
- "@playwright/test": "^1.51.1",
- "babel-plugin-react-compiler": "*",
- "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
- "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
- "sass": "^1.3.0"
- },
- "peerDependenciesMeta": {
- "@opentelemetry/api": {
- "optional": true
- },
- "@playwright/test": {
- "optional": true
- },
- "babel-plugin-react-compiler": {
- "optional": true
- },
- "sass": {
- "optional": true
- }
- }
- },
- "node_modules/node-releases": {
- "version": "2.0.51",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
- "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/obug": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz",
- "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==",
- "dev": true,
- "funding": [
- "https://github.com/sponsors/sxzz",
- "https://opencollective.com/debug"
- ],
- "license": "MIT",
- "engines": {
- "node": ">=12.20.0"
- }
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/path-key": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
- "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/pathe": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
- "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/picocolors": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
- "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
- "license": "ISC"
- },
- "node_modules/picomatch": {
- "version": "2.3.2",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
- "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/postcss": {
- "version": "8.5.25",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
- "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.16",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
- "node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
- "node_modules/queue-microtask": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
- "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT"
- },
- "node_modules/react": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
- "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/react-dom": {
- "version": "19.2.8",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
- "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
- "license": "MIT",
- "dependencies": {
- "scheduler": "^0.27.0"
- },
- "peerDependencies": {
- "react": "^19.2.8"
- }
- },
- "node_modules/reusify": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
- "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "iojs": ">=1.0.0",
- "node": ">=0.10.0"
- }
- },
- "node_modules/rolldown": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz",
- "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@oxc-project/types": "=0.142.0",
- "@rolldown/pluginutils": "^1.0.0"
- },
- "bin": {
- "rolldown": "bin/cli.mjs"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.2.1",
- "@rolldown/binding-darwin-arm64": "1.2.1",
- "@rolldown/binding-darwin-x64": "1.2.1",
- "@rolldown/binding-freebsd-x64": "1.2.1",
- "@rolldown/binding-linux-arm-gnueabihf": "1.2.1",
- "@rolldown/binding-linux-arm64-gnu": "1.2.1",
- "@rolldown/binding-linux-arm64-musl": "1.2.1",
- "@rolldown/binding-linux-ppc64-gnu": "1.2.1",
- "@rolldown/binding-linux-s390x-gnu": "1.2.1",
- "@rolldown/binding-linux-x64-gnu": "1.2.1",
- "@rolldown/binding-linux-x64-musl": "1.2.1",
- "@rolldown/binding-openharmony-arm64": "1.2.1",
- "@rolldown/binding-wasm32-wasi": "1.2.1",
- "@rolldown/binding-win32-arm64-msvc": "1.2.1",
- "@rolldown/binding-win32-x64-msvc": "1.2.1"
- }
- },
- "node_modules/run-parallel": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
- "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "queue-microtask": "^1.2.2"
- }
- },
- "node_modules/scheduler": {
- "version": "0.27.0",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
- "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
- "license": "MIT"
- },
- "node_modules/semver": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
- "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
- "dev": true,
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- }
- },
- "node_modules/sharp": {
- "version": "0.35.3",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
- "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
- "license": "Apache-2.0",
- "optional": true,
- "dependencies": {
- "@img/colour": "^1.1.0",
- "detect-libc": "^2.1.2",
- "semver": "^7.8.5"
- },
- "engines": {
- "node": ">=20.9.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.35.3",
- "@img/sharp-darwin-x64": "0.35.3",
- "@img/sharp-freebsd-wasm32": "0.35.3",
- "@img/sharp-libvips-darwin-arm64": "1.3.2",
- "@img/sharp-libvips-darwin-x64": "1.3.2",
- "@img/sharp-libvips-linux-arm": "1.3.2",
- "@img/sharp-libvips-linux-arm64": "1.3.2",
- "@img/sharp-libvips-linux-ppc64": "1.3.2",
- "@img/sharp-libvips-linux-riscv64": "1.3.2",
- "@img/sharp-libvips-linux-s390x": "1.3.2",
- "@img/sharp-libvips-linux-x64": "1.3.2",
- "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
- "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
- "@img/sharp-linux-arm": "0.35.3",
- "@img/sharp-linux-arm64": "0.35.3",
- "@img/sharp-linux-ppc64": "0.35.3",
- "@img/sharp-linux-riscv64": "0.35.3",
- "@img/sharp-linux-s390x": "0.35.3",
- "@img/sharp-linux-x64": "0.35.3",
- "@img/sharp-linuxmusl-arm64": "0.35.3",
- "@img/sharp-linuxmusl-x64": "0.35.3",
- "@img/sharp-webcontainers-wasm32": "0.35.3",
- "@img/sharp-win32-arm64": "0.35.3",
- "@img/sharp-win32-ia32": "0.35.3",
- "@img/sharp-win32-x64": "0.35.3"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- }
- }
- },
- "node_modules/sharp/node_modules/semver": {
- "version": "7.8.5",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
- "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
- "license": "ISC",
- "optional": true,
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "node_modules/shebang-command": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
- "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "shebang-regex": "^3.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/shebang-regex": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
- "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/siginfo": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
- "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/source-map-js": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/stackback": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
- "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/std-env": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz",
- "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/styled-jsx": {
- "version": "5.1.6",
- "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
- "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==",
- "license": "MIT",
- "dependencies": {
- "client-only": "0.0.1"
- },
- "engines": {
- "node": ">= 12.0.0"
- },
- "peerDependencies": {
- "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0"
- },
- "peerDependenciesMeta": {
- "@babel/core": {
- "optional": true
- },
- "babel-plugin-macros": {
- "optional": true
- }
- }
- },
- "node_modules/swr": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/swr/-/swr-2.4.2.tgz",
- "integrity": "sha512-ej644Y2bvkIajfR32KGeSSdBXQW+ScjGjkybZgSE7kFpk9eGnV44XY9FJylXi+W75pavSX1PVNB57W5EbhGIYw==",
- "license": "MIT",
- "dependencies": {
- "dequal": "^2.0.3",
- "use-sync-external-store": "^1.6.0"
- },
- "peerDependencies": {
- "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/tinybench": {
- "version": "2.9.0",
- "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
- "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/tinyexec": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz",
- "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/tinyglobby": {
- "version": "0.2.17",
- "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
- "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fdir": "^6.5.0",
- "picomatch": "^4.0.4"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "funding": {
- "url": "https://github.com/sponsors/SuperchupuDev"
- }
- },
- "node_modules/tinyglobby/node_modules/fdir": {
- "version": "6.5.0",
- "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
- "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
- "node_modules/tinyglobby/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/tinyrainbow": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz",
- "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14.0.0"
- }
- },
- "node_modules/to-regex-range": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
- "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-number": "^7.0.0"
- },
- "engines": {
- "node": ">=8.0"
- }
- },
- "node_modules/ts-api-utils": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
- "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.12"
- },
- "peerDependencies": {
- "typescript": ">=4.8.4"
- }
- },
- "node_modules/tslib": {
- "version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
- "license": "0BSD"
- },
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/typescript": {
- "version": "5.9.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
- "license": "Apache-2.0",
- "bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
- },
- "engines": {
- "node": ">=14.17"
- }
- },
- "node_modules/typescript-eslint": {
- "version": "8.65.0",
- "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.65.0.tgz",
- "integrity": "sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@typescript-eslint/eslint-plugin": "8.65.0",
- "@typescript-eslint/parser": "8.65.0",
- "@typescript-eslint/typescript-estree": "8.65.0",
- "@typescript-eslint/utils": "8.65.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/typescript-eslint"
- },
- "peerDependencies": {
- "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
- "typescript": ">=4.8.4 <6.1.0"
- }
- },
- "node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/update-browserslist-db": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
- "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/browserslist"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/browserslist"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "escalade": "^3.2.0",
- "picocolors": "^1.1.1"
- },
- "bin": {
- "update-browserslist-db": "cli.js"
- },
- "peerDependencies": {
- "browserslist": ">= 4.21.0"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
- "node_modules/use-sync-external-store": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
- "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
- "license": "MIT",
- "peerDependencies": {
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
- }
- },
- "node_modules/vite": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz",
- "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "lightningcss": "^1.33.0",
- "picomatch": "^4.0.5",
- "postcss": "^8.5.23",
- "rolldown": "~1.2.0",
- "tinyglobby": "^0.2.17"
- },
- "bin": {
- "vite": "bin/vite.js"
- },
- "engines": {
- "node": "^20.19.0 || >=22.12.0"
- },
- "funding": {
- "url": "https://github.com/vitejs/vite?sponsor=1"
- },
- "optionalDependencies": {
- "fsevents": "~2.3.3"
- },
- "peerDependencies": {
- "@types/node": "^20.19.0 || >=22.12.0",
- "@vitejs/devtools": "^0.4.0",
- "esbuild": "^0.27.0 || ^0.28.0",
- "jiti": ">=1.21.0",
- "less": "^4.0.0",
- "sass": "^1.70.0",
- "sass-embedded": "^1.70.0",
- "stylus": ">=0.54.8",
- "sugarss": "^5.0.0",
- "terser": "^5.16.0",
- "tsx": "^4.8.1",
- "yaml": "^2.4.2"
- },
- "peerDependenciesMeta": {
- "@types/node": {
- "optional": true
- },
- "@vitejs/devtools": {
- "optional": true
- },
- "esbuild": {
- "optional": true
- },
- "jiti": {
- "optional": true
- },
- "less": {
- "optional": true
- },
- "sass": {
- "optional": true
- },
- "sass-embedded": {
- "optional": true
- },
- "stylus": {
- "optional": true
- },
- "sugarss": {
- "optional": true
- },
- "terser": {
- "optional": true
- },
- "tsx": {
- "optional": true
- },
- "yaml": {
- "optional": true
- }
- }
- },
- "node_modules/vite/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/vitest": {
- "version": "4.1.10",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz",
- "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@vitest/expect": "4.1.10",
- "@vitest/mocker": "4.1.10",
- "@vitest/pretty-format": "4.1.10",
- "@vitest/runner": "4.1.10",
- "@vitest/snapshot": "4.1.10",
- "@vitest/spy": "4.1.10",
- "@vitest/utils": "4.1.10",
- "es-module-lexer": "^2.0.0",
- "expect-type": "^1.3.0",
- "magic-string": "^0.30.21",
- "obug": "^2.1.1",
- "pathe": "^2.0.3",
- "picomatch": "^4.0.3",
- "std-env": "^4.0.0-rc.1",
- "tinybench": "^2.9.0",
- "tinyexec": "^1.0.2",
- "tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.1.0",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
- "why-is-node-running": "^2.3.0"
- },
- "bin": {
- "vitest": "vitest.mjs"
- },
- "engines": {
- "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/vitest"
- },
- "peerDependencies": {
- "@edge-runtime/vm": "*",
- "@opentelemetry/api": "^1.9.0",
- "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.10",
- "@vitest/browser-preview": "4.1.10",
- "@vitest/browser-webdriverio": "4.1.10",
- "@vitest/coverage-istanbul": "4.1.10",
- "@vitest/coverage-v8": "4.1.10",
- "@vitest/ui": "4.1.10",
- "happy-dom": "*",
- "jsdom": "*",
- "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
- },
- "peerDependenciesMeta": {
- "@edge-runtime/vm": {
- "optional": true
- },
- "@opentelemetry/api": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@vitest/browser-playwright": {
- "optional": true
- },
- "@vitest/browser-preview": {
- "optional": true
- },
- "@vitest/browser-webdriverio": {
- "optional": true
- },
- "@vitest/coverage-istanbul": {
- "optional": true
- },
- "@vitest/coverage-v8": {
- "optional": true
- },
- "@vitest/ui": {
- "optional": true
- },
- "happy-dom": {
- "optional": true
- },
- "jsdom": {
- "optional": true
- },
- "vite": {
- "optional": false
- }
- }
- },
- "node_modules/vitest/node_modules/picomatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
- "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
- "node_modules/which": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
- "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "isexe": "^2.0.0"
- },
- "bin": {
- "node-which": "bin/node-which"
- },
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/why-is-node-running": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
- "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "siginfo": "^2.0.0",
- "stackback": "0.0.2"
- },
- "bin": {
- "why-is-node-running": "cli.js"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/yallist": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
- "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
- "dev": true,
- "license": "ISC"
- },
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/zod": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
- "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
- "dev": true,
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/colinhacks"
- }
- },
- "node_modules/zod-validation-error": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
- "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18.0.0"
- },
- "peerDependencies": {
- "zod": "^3.25.0 || ^4.0.0"
- }
- }
- }
-}
diff --git a/examples/demo-frontend/package.json b/examples/demo-frontend/package.json
deleted file mode 100644
index 0e7546b..0000000
--- a/examples/demo-frontend/package.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "name": "demo-frontend",
- "version": "0.1.0",
- "private": true,
- "scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start",
- "lint": "eslint .",
- "typecheck": "tsc --noEmit",
- "test": "vitest run",
- "check:architecture": "node scripts/check-architecture.mjs",
- "check": "npm run check:architecture && npm run lint && npm run typecheck && npm run test",
- "codegen:simple-rest-api": "npx @gromlab/api-codegen@5.1.1 -i ../demo-backend/openapi/simple.json -o src/infra/simple-rest-api/generated"
- },
- "dependencies": {
- "clsx": "2.1.1",
- "next": "16.2.12",
- "react": "19.2.8",
- "react-dom": "19.2.8",
- "swr": "2.4.2"
- },
- "devDependencies": {
- "@eslint/js": "10.0.1",
- "@next/eslint-plugin-next": "16.2.12",
- "@types/node": "^20",
- "@types/react": "^19",
- "@types/react-dom": "^19",
- "eslint": "10.8.0",
- "eslint-plugin-react-hooks": "7.1.1",
- "globals": "17.8.0",
- "typescript": "^5",
- "typescript-eslint": "8.65.0",
- "vitest": "4.1.10"
- },
- "overrides": {
- "postcss": "8.5.25",
- "sharp": "0.35.3"
- }
-}
diff --git a/examples/demo-frontend/public/product-placeholder.svg b/examples/demo-frontend/public/product-placeholder.svg
deleted file mode 100644
index 6a703d6..0000000
--- a/examples/demo-frontend/public/product-placeholder.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
diff --git a/examples/demo-frontend/scripts/check-architecture.mjs b/examples/demo-frontend/scripts/check-architecture.mjs
deleted file mode 100644
index 684e550..0000000
--- a/examples/demo-frontend/scripts/check-architecture.mjs
+++ /dev/null
@@ -1,360 +0,0 @@
-import { existsSync, readdirSync, readFileSync } from 'node:fs'
-import { dirname, extname, join, relative, resolve, sep } from 'node:path'
-import { fileURLToPath } from 'node:url'
-import ts from 'typescript'
-
-const SCRIPT_DIRECTORY = dirname(fileURLToPath(import.meta.url))
-const PROJECT_ROOT = resolve(SCRIPT_DIRECTORY, '..')
-const SOURCE_ROOT = join(PROJECT_ROOT, 'src')
-const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx'])
-const MODULE_ROOTS = [
- 'compositions/layouts/store-shell',
- 'compositions/screens/cart',
- 'compositions/screens/catalog',
- 'compositions/screens/orders',
- 'compositions/screens/product-admin',
- 'compositions/screens/product-detail',
- 'compositions/screens/sign-in',
- 'compositions/widgets/demo-toolbar',
- 'domains/auth',
- 'domains/cart',
- 'domains/catalog',
- 'domains/demo-control',
- 'domains/orders',
- 'infra/browser-storage',
- 'infra/simple-auth-session',
- 'infra/simple-rest-api',
- 'shared/lib/value-predicates',
- 'ui/button',
- 'ui/feedback-panel',
- 'ui/form-field'
-].sort((left, right) => right.length - left.length)
-const MODULAR_LAYERS = new Set(['compositions', 'domains', 'infra', 'ui'])
-const ALLOWED_TARGETS = {
- app: new Set(['app', 'compositions', 'domains', 'infra', 'ui', 'shared']),
- compositions: new Set(['compositions', 'domains', 'infra', 'ui', 'shared']),
- domains: new Set(['domains', 'infra', 'ui', 'shared']),
- infra: new Set(['infra', 'shared']),
- ui: new Set(['ui', 'shared']),
- shared: new Set(['shared'])
-}
-
-/**
- * Рекурсивно собирает TypeScript-файлы SLM root.
- */
-const collectSourceFiles = (directory) => {
- return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
- const entryPath = join(directory, entry.name)
-
- if (entry.isDirectory()) {
- return collectSourceFiles(entryPath)
- }
-
- return SOURCE_EXTENSIONS.has(extname(entry.name)) ? [entryPath] : []
- })
-}
-
-/**
- * Нормализует filesystem path к формату architecture manifest.
- */
-const normalizePath = (filePath) => {
- return filePath.split(sep).join('/')
-}
-
-/**
- * Возвращает путь файла относительно SLM root.
- */
-const getSourcePath = (filePath) => {
- return normalizePath(relative(SOURCE_ROOT, filePath))
-}
-
-/**
- * Определяет слой относительного source path.
- */
-const getLayer = (sourcePath) => {
- return sourcePath.split('/')[0]
-}
-
-/**
- * Находит явно объявленного module-owner по longest-prefix rule.
- */
-const getModuleId = (sourcePath) => {
- return MODULE_ROOTS.find(
- (moduleRoot) => sourcePath === moduleRoot || sourcePath.startsWith(`${moduleRoot}/`)
- ) ?? null
-}
-
-/**
- * Проверяет deep import относительно явно объявленного public entry.
- */
-const isDeepModuleImport = (targetPath, sourceModuleId) => {
- const targetModuleId = getModuleId(targetPath)
-
- return targetModuleId !== null &&
- targetModuleId !== sourceModuleId &&
- targetPath !== targetModuleId
-}
-
-/**
- * Проверяет направление слоя независимо от синтаксиса import path.
- */
-const isLayerImportAllowed = (sourcePath, targetPath) => {
- const sourceLayer = getLayer(sourcePath)
- const targetLayer = getLayer(targetPath)
-
- return ALLOWED_TARGETS[sourceLayer]?.has(targetLayer) === true
-}
-
-/**
- * Извлекает static, side-effect, reexport и dynamic module specifiers через TypeScript AST.
- */
-const getModuleSpecifiers = (filePath, sourceCode) => {
- const scriptKind = extname(filePath) === '.tsx' ? ts.ScriptKind.TSX : ts.ScriptKind.TS
- const sourceFile = ts.createSourceFile(
- filePath,
- sourceCode,
- ts.ScriptTarget.Latest,
- true,
- scriptKind
- )
- const specifiers = []
-
- /**
- * Обходит один AST node и собирает строковые module references.
- */
- const visit = (node) => {
- if (
- (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) &&
- node.moduleSpecifier &&
- ts.isStringLiteral(node.moduleSpecifier)
- ) {
- specifiers.push(node.moduleSpecifier.text)
- }
-
- if (
- ts.isImportEqualsDeclaration(node) &&
- ts.isExternalModuleReference(node.moduleReference) &&
- node.moduleReference.expression &&
- ts.isStringLiteral(node.moduleReference.expression)
- ) {
- specifiers.push(node.moduleReference.expression.text)
- }
-
- if (
- ts.isCallExpression(node) &&
- node.arguments.length === 1 &&
- ts.isStringLiteral(node.arguments[0]) &&
- (node.expression.kind === ts.SyntaxKind.ImportKeyword ||
- (ts.isIdentifier(node.expression) && node.expression.text === 'require'))
- ) {
- specifiers.push(node.arguments[0].text)
- }
-
- if (
- ts.isImportTypeNode(node) &&
- ts.isLiteralTypeNode(node.argument) &&
- ts.isStringLiteral(node.argument.literal)
- ) {
- specifiers.push(node.argument.literal.text)
- }
-
- ts.forEachChild(node, visit)
- }
-
- visit(sourceFile)
-
- return specifiers
-}
-
-/**
- * Разрешает относительный TypeScript import.
- */
-const resolveRelativeImport = (sourceFile, specifier) => {
- const basePath = resolve(dirname(sourceFile), specifier)
- const candidates = [
- basePath,
- `${basePath}.ts`,
- `${basePath}.tsx`,
- join(basePath, 'index.ts'),
- join(basePath, 'index.tsx')
- ]
-
- return candidates.find((candidate) => existsSync(candidate)) ?? null
-}
-
-/**
- * Находит первый cycle в module graph.
- */
-const findCycle = (graph) => {
- const visited = new Set()
- const active = new Set()
- const path = []
-
- /**
- * Обходит один module node depth-first.
- */
- const visit = (moduleId) => {
- if (active.has(moduleId)) {
- const cycleStart = path.indexOf(moduleId)
-
- return [...path.slice(cycleStart), moduleId]
- }
-
- if (visited.has(moduleId)) {
- return null
- }
-
- visited.add(moduleId)
- active.add(moduleId)
- path.push(moduleId)
-
- for (const dependency of graph.get(moduleId) ?? []) {
- const cycle = visit(dependency)
-
- if (cycle !== null) {
- return cycle
- }
- }
-
- path.pop()
- active.delete(moduleId)
-
- return null
- }
-
- for (const moduleId of graph.keys()) {
- const cycle = visit(moduleId)
-
- if (cycle !== null) {
- return cycle
- }
- }
-
- return null
-}
-
-const violations = []
-const graph = new Map()
-const sourceFiles = collectSourceFiles(SOURCE_ROOT)
-const parserProbe = getModuleSpecifiers(
- 'architecture-probe.ts',
- `
- import '@/infra/simple-rest-api'
- export { useAuth } from '@/domains/auth'
- type Auth = import('@/domains/auth').AuthContextValue
- const lazy = import('@/compositions/screens/catalog')
- `
-)
-const expectedProbeSpecifiers = [
- '@/infra/simple-rest-api',
- '@/domains/auth',
- '@/domains/auth',
- '@/compositions/screens/catalog'
-]
-
-expectedProbeSpecifiers.forEach((specifier) => {
- if (!parserProbe.includes(specifier)) {
- violations.push(`architecture parser fixture missed ${specifier}`)
- }
-})
-
-if (!isDeepModuleImport('shared/lib/value-predicates/value-predicates', 'domains/cart')) {
- violations.push('architecture boundary fixture did not detect shared module deep import')
-}
-
-if (isLayerImportAllowed('shared/bridge.ts', 'domains/auth/index.ts')) {
- violations.push('architecture boundary fixture allowed shared to import domains relatively')
-}
-
-MODULE_ROOTS.forEach((moduleRoot) => {
- const moduleDirectory = join(SOURCE_ROOT, moduleRoot)
- const publicEntry = join(moduleDirectory, 'index.ts')
-
- if (!existsSync(moduleDirectory) || !existsSync(publicEntry)) {
- violations.push(`${moduleRoot}: declared module must be a folder with index.ts public API`)
- }
-})
-
-sourceFiles.forEach((sourceFile) => {
- const sourcePath = getSourcePath(sourceFile)
- const sourceLayer = getLayer(sourcePath)
- const sourceModuleId = getModuleId(sourcePath)
- const relativeSource = relative(PROJECT_ROOT, sourceFile)
- const sourceCode = readFileSync(sourceFile, 'utf8')
-
- if (MODULAR_LAYERS.has(sourceLayer) && sourceModuleId === null) {
- violations.push(`${relativeSource}: implementation file has no declared module-owner`)
- }
-
- getModuleSpecifiers(sourceFile, sourceCode).forEach((specifier) => {
- if (specifier.startsWith('@/')) {
- const targetPath = specifier.slice(2)
- const targetLayer = getLayer(targetPath)
- const targetModuleId = getModuleId(targetPath)
-
- if (!isLayerImportAllowed(sourcePath, targetPath)) {
- violations.push(`${relativeSource}: ${sourceLayer} cannot import ${targetLayer} via ${specifier}`)
- }
-
- if (isDeepModuleImport(targetPath, sourceModuleId)) {
- violations.push(`${relativeSource}: deep import into ${targetModuleId} via ${specifier}`)
- }
-
- if (sourceModuleId !== null && targetModuleId !== null && sourceModuleId !== targetModuleId) {
- const dependencies = graph.get(sourceModuleId) ?? new Set()
- dependencies.add(targetModuleId)
- graph.set(sourceModuleId, dependencies)
- }
-
- return
- }
-
- if (specifier.startsWith('.')) {
- const targetFile = resolveRelativeImport(sourceFile, specifier)
-
- if (targetFile === null) {
- return
- }
-
- const targetModuleId = getModuleId(getSourcePath(targetFile))
- const targetPath = getSourcePath(targetFile)
- const targetLayer = getLayer(targetPath)
-
- if (!isLayerImportAllowed(sourcePath, targetPath)) {
- violations.push(
- `${relativeSource}: ${sourceLayer} cannot import ${targetLayer} via ${specifier}`
- )
- }
-
- if (
- sourceModuleId !== null &&
- targetModuleId !== null &&
- sourceModuleId !== targetModuleId
- ) {
- violations.push(`${relativeSource}: relative import crosses into ${targetModuleId}`)
- }
- }
- })
-
- const isRestModuleFile = sourceModuleId === 'infra/simple-rest-api'
-
- if (sourceCode.includes('/generated') && !isRestModuleFile) {
- violations.push(`${relativeSource}: generated SDK import escaped infra/simple-rest-api`)
- }
-})
-
-const cycle = findCycle(graph)
-
-if (cycle !== null) {
- violations.push(`module dependency cycle: ${cycle.join(' -> ')}`)
-}
-
-if (violations.length > 0) {
- console.error('SLM architecture check failed:')
- violations.forEach((violation) => console.error(`- ${violation}`))
- process.exitCode = 1
-} else {
- console.log(
- `SLM architecture check passed for ${sourceFiles.length} files and ${MODULE_ROOTS.length} declared modules.`
- )
-}
diff --git a/examples/demo-frontend/src/app/(store)/admin/products/page.tsx b/examples/demo-frontend/src/app/(store)/admin/products/page.tsx
deleted file mode 100644
index 3e7c937..0000000
--- a/examples/demo-frontend/src/app/(store)/admin/products/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Metadata } from 'next'
-
-import { ProductAdminScreen } from '@/compositions/screens/product-admin'
-
-/**
- * Metadata catalog administration route.
- */
-export const metadata: Metadata = {
- title: 'Product admin'
-}
-
-/**
- * Подключает admin composition к framework route.
- */
-export default function ProductAdminPage() {
- return
-}
diff --git a/examples/demo-frontend/src/app/(store)/cart/page.tsx b/examples/demo-frontend/src/app/(store)/cart/page.tsx
deleted file mode 100644
index e8d4739..0000000
--- a/examples/demo-frontend/src/app/(store)/cart/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Metadata } from 'next'
-
-import { CartScreen } from '@/compositions/screens/cart'
-
-/**
- * Metadata cart route.
- */
-export const metadata: Metadata = {
- title: 'Cart'
-}
-
-/**
- * Подключает multi-domain checkout composition к cart route.
- */
-export default function CartPage() {
- return
-}
diff --git a/examples/demo-frontend/src/app/(store)/layout.tsx b/examples/demo-frontend/src/app/(store)/layout.tsx
deleted file mode 100644
index 4818274..0000000
--- a/examples/demo-frontend/src/app/(store)/layout.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { StoreShellLayout } from '@/compositions/layouts/store-shell'
-
-import type { AppLayoutProps } from '../types/app-layout-props.type'
-
-/**
- * Подключает общий storefront layout ко всем leaf routes группы.
- */
-export default function StoreLayout(props: AppLayoutProps) {
- const { children } = props
-
- return {children}
-}
diff --git a/examples/demo-frontend/src/app/(store)/orders/page.tsx b/examples/demo-frontend/src/app/(store)/orders/page.tsx
deleted file mode 100644
index 8797d4e..0000000
--- a/examples/demo-frontend/src/app/(store)/orders/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Metadata } from 'next'
-
-import { OrdersScreen } from '@/compositions/screens/orders'
-
-/**
- * Metadata protected orders route.
- */
-export const metadata: Metadata = {
- title: 'Orders'
-}
-
-/**
- * Подключает protected orders composition к framework route.
- */
-export default function OrdersPage() {
- return
-}
diff --git a/examples/demo-frontend/src/app/(store)/page.tsx b/examples/demo-frontend/src/app/(store)/page.tsx
deleted file mode 100644
index d5b6de6..0000000
--- a/examples/demo-frontend/src/app/(store)/page.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { CatalogScreen } from '@/compositions/screens/catalog'
-
-/**
- * Подключает catalog composition к корневому route.
- */
-export default function CatalogPage() {
- return
-}
diff --git a/examples/demo-frontend/src/app/(store)/products/[productId]/page.tsx b/examples/demo-frontend/src/app/(store)/products/[productId]/page.tsx
deleted file mode 100644
index e7deb90..0000000
--- a/examples/demo-frontend/src/app/(store)/products/[productId]/page.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Metadata } from 'next'
-
-import { ProductDetailScreen } from '@/compositions/screens/product-detail'
-
-import type { ProductPageProps } from '../../../types/product-page-props.type'
-
-/**
- * Metadata dynamic product route без запроса к runtime fixture во время build.
- */
-export const metadata: Metadata = {
- title: 'Product detail'
-}
-
-/**
- * Адаптирует async dynamic segment к product detail composition.
- */
-export default async function ProductPage(props: ProductPageProps) {
- const { productId } = await props.params
-
- return
-}
diff --git a/examples/demo-frontend/src/app/(store)/sign-in/page.tsx b/examples/demo-frontend/src/app/(store)/sign-in/page.tsx
deleted file mode 100644
index 897dbc4..0000000
--- a/examples/demo-frontend/src/app/(store)/sign-in/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Metadata } from 'next'
-
-import { SignInScreen } from '@/compositions/screens/sign-in'
-
-/**
- * Metadata sign-in route.
- */
-export const metadata: Metadata = {
- title: 'Sign in'
-}
-
-/**
- * Подключает auth composition к framework route.
- */
-export default function SignInPage() {
- return
-}
diff --git a/examples/demo-frontend/src/app/favicon.ico b/examples/demo-frontend/src/app/favicon.ico
deleted file mode 100644
index 718d6fe..0000000
Binary files a/examples/demo-frontend/src/app/favicon.ico and /dev/null differ
diff --git a/examples/demo-frontend/src/app/globals.css b/examples/demo-frontend/src/app/globals.css
deleted file mode 100644
index b4902ce..0000000
--- a/examples/demo-frontend/src/app/globals.css
+++ /dev/null
@@ -1,85 +0,0 @@
-:root {
- --color-paper: #f3efe4;
- --color-surface: #faf7ef;
- --color-surface-strong: #e6e0d4;
- --color-ink: #191918;
- --color-ink-muted: #65615a;
- --color-ink-faint: #67635c;
- --color-line: #d7d0c3;
- --color-line-strong: #aaa297;
- --color-blue: #2847f4;
- --color-acid: #c9ff50;
- --color-acid-dark: #6f9c00;
- --color-orange: #ff8a3d;
- --color-error: #b83228;
- --color-focus: #8dacff;
- --font-body: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- --font-display: 'Arial Black', 'Helvetica Neue', Arial, sans-serif;
- --font-mono: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
- --shadow-card: 0 10px 40px rgb(37 31 22 / 7%);
- --shadow-card-hover: 0 20px 55px rgb(37 31 22 / 14%);
-}
-
-html {
- color: var(--color-ink);
- background: var(--color-paper);
- scroll-behavior: smooth;
- height: 100%;
-}
-
-html,
-body {
- max-width: 100vw;
- overflow-x: hidden;
-}
-
-body {
- min-height: 100%;
- display: flex;
- flex-direction: column;
- color: var(--color-ink);
- background:
- radial-gradient(circle at 15% 0%, rgb(255 255 255 / 58%), transparent 25%),
- var(--color-paper);
- font-family: var(--font-body);
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
-}
-
-* {
- box-sizing: border-box;
- padding: 0;
- margin: 0;
-}
-
-a {
- color: inherit;
- text-decoration: none;
-}
-
-button,
-input,
-select,
-textarea {
- font: inherit;
-}
-
-::selection {
- color: var(--color-ink);
- background: var(--color-acid);
-}
-
-@media (prefers-reduced-motion: reduce) {
- html {
- scroll-behavior: auto;
- }
-
- *,
- *::before,
- *::after {
- scroll-behavior: auto !important;
- transition-duration: 0.01ms !important;
- animation-duration: 0.01ms !important;
- animation-iteration-count: 1 !important;
- }
-}
diff --git a/examples/demo-frontend/src/app/layout.tsx b/examples/demo-frontend/src/app/layout.tsx
deleted file mode 100644
index ba38117..0000000
--- a/examples/demo-frontend/src/app/layout.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { Metadata } from 'next'
-
-import { AppProviders } from './providers'
-import type { AppLayoutProps } from './types/app-layout-props.type'
-import './globals.css'
-
-/**
- * Статические metadata архитектурного demo-приложения.
- */
-export const metadata: Metadata = {
- title: {
- default: 'Layer Supply',
- template: '%s / Layer Supply'
- },
- description: 'A complete Next.js storefront demonstrating Scoped Layered Module Design.'
-}
-
-/**
- * Корневая Next.js entry point с application-scoped providers.
- */
-export default function RootLayout(props: AppLayoutProps) {
- const { children } = props
-
- return (
-
-
- {children}
-
-
- )
-}
diff --git a/examples/demo-frontend/src/app/providers.tsx b/examples/demo-frontend/src/app/providers.tsx
deleted file mode 100644
index 9b0343f..0000000
--- a/examples/demo-frontend/src/app/providers.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-'use client'
-
-import { AuthProvider, useAuth } from '@/domains/auth'
-import { CartProvider } from '@/domains/cart'
-import { SimpleRestApiProvider } from '@/infra/simple-rest-api'
-
-import type { AppLayoutProps } from './types/app-layout-props.type'
-
-/**
- * Создаёт новый REST cache при каждой смене logical auth session.
- *
- * Используется для:
- * - исключения DTO предыдущего пользователя из нового session scope
- */
-const AuthScopedRestApiProvider = (props: AppLayoutProps) => {
- const { children } = props
- const auth = useAuth()
- const cacheScopeKey = auth.status === 'checking'
- ? `checking:${auth.sessionKey ?? 'anonymous'}`
- : auth.sessionKey ?? auth.status
-
- return (
-
- {children}
-
- )
-}
-
-/**
- * Подключает application-scoped domain state и auth-scoped REST cache.
- *
- * Используется для:
- * - сохранения auth/cart state между route transitions
- * - пересоздания technical cache на границе пользовательской сессии
- */
-export const AppProviders = (props: AppLayoutProps) => {
- const { children } = props
-
- return (
-
-
- {children}
-
-
- )
-}
diff --git a/examples/demo-frontend/src/app/types/app-layout-props.type.ts b/examples/demo-frontend/src/app/types/app-layout-props.type.ts
deleted file mode 100644
index d7802e3..0000000
--- a/examples/demo-frontend/src/app/types/app-layout-props.type.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { ReactNode } from 'react'
-
-/**
- * Props framework layouts, передающих готовое route subtree.
- */
-export type AppLayoutProps = {
- /** Вложенное route subtree. */
- children: ReactNode
-}
diff --git a/examples/demo-frontend/src/app/types/product-page-props.type.ts b/examples/demo-frontend/src/app/types/product-page-props.type.ts
deleted file mode 100644
index 5e0f54b..0000000
--- a/examples/demo-frontend/src/app/types/product-page-props.type.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Props dynamic product route в Next.js 16.
- */
-export type ProductPageProps = {
- /** Асинхронные параметры dynamic segment. */
- params: Promise<{
- /** Идентификатор продукта из URL. */
- productId: string
- }>
-}
diff --git a/examples/demo-frontend/src/compositions/layouts/store-shell/index.ts b/examples/demo-frontend/src/compositions/layouts/store-shell/index.ts
deleted file mode 100644
index f211945..0000000
--- a/examples/demo-frontend/src/compositions/layouts/store-shell/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { StoreShellLayout } from './store-shell.layout'
-export type { StoreShellLayoutProps } from './types/store-shell-layout-props.type'
diff --git a/examples/demo-frontend/src/compositions/layouts/store-shell/store-shell.layout.tsx b/examples/demo-frontend/src/compositions/layouts/store-shell/store-shell.layout.tsx
deleted file mode 100644
index 829afc6..0000000
--- a/examples/demo-frontend/src/compositions/layouts/store-shell/store-shell.layout.tsx
+++ /dev/null
@@ -1,134 +0,0 @@
-'use client'
-
-import cl from 'clsx'
-import Link from 'next/link'
-import { usePathname } from 'next/navigation'
-
-import { useAuth } from '@/domains/auth'
-import { useCart } from '@/domains/cart'
-import { DemoToolbarWidget } from '@/compositions/widgets/demo-toolbar'
-import { Button } from '@/ui/button'
-
-import type { StoreShellLayoutProps } from './types/store-shell-layout-props.type'
-import styles from './styles/store-shell.module.css'
-
-/**
- * Общий каркас навигации и application-scoped controls магазина.
- *
- * Используется для:
- * - сохранения auth/cart контекста между App Router страницами
- * - доступа к deterministic demo controls
- */
-export const StoreShellLayout = (props: StoreShellLayoutProps) => {
- const { children, className, ...rootAttrs } = props
- const pathname = usePathname()
- const auth = useAuth()
- const cart = useCart()
- const isAdmin = auth.status === 'authenticated' && auth.user?.role === 'admin'
- const hasSession = auth.sessionKey !== null
- const accountLabel = auth.user?.name ?? 'Sign in'
-
- /**
- * Определяет активный верхнеуровневый navigation item.
- */
- const isActivePath = (path: string): boolean => {
- return path === '/' ? pathname === '/' : pathname.startsWith(path)
- }
-
- const isCatalogActive = isActivePath('/')
- const isCartActive = isActivePath('/cart')
- const isOrdersActive = isActivePath('/orders')
- const isAdminActive = isActivePath('/admin')
-
- /**
- * Завершает текущую сессию из общего header.
- */
- const handleSignOut = (): void => {
- void auth.signOut()
- }
-
- /**
- * Пытается восстановить unreadable cart явным empty snapshot.
- */
- const handleCartReset = (): void => {
- void cart.clearCart()
- }
-
- return (
-
-
-
- LS
-
- Layer
-
- Supply
-
-
-
-
-
-
- {auth.status}
-
- {accountLabel}
-
- {hasSession && (
-
- )}
-
-
- {cart.error && (
-
- {cart.error.message}
-
-
- )}
-
-
-
{children}
-
-
-
-
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/layouts/store-shell/styles/store-shell.module.css b/examples/demo-frontend/src/compositions/layouts/store-shell/styles/store-shell.module.css
deleted file mode 100644
index d04b9c6..0000000
--- a/examples/demo-frontend/src/compositions/layouts/store-shell/styles/store-shell.module.css
+++ /dev/null
@@ -1,186 +0,0 @@
-.root {
- min-height: 100vh;
-}
-
-.header {
- position: sticky;
- top: 0;
- z-index: 30;
- display: grid;
- grid-template-columns: auto 1fr auto;
- align-items: center;
- gap: clamp(18px, 4vw, 56px);
- min-height: 82px;
- padding: 12px clamp(18px, 5vw, 72px);
- border-bottom: 1px solid var(--color-line);
- background: color-mix(in srgb, var(--color-paper) 88%, transparent);
- backdrop-filter: blur(18px);
-}
-
-.brand {
- display: inline-flex;
- align-items: center;
- gap: 10px;
-}
-
-.brandMark {
- display: grid;
- place-items: center;
- width: 42px;
- height: 42px;
- color: var(--color-paper);
- border-radius: 50%;
- background: var(--color-blue);
- font-family: var(--font-display);
- font-size: 0.9rem;
-}
-
-.brandText {
- font-family: var(--font-display);
- font-size: 0.86rem;
- font-weight: 820;
- letter-spacing: 0.04em;
- line-height: 0.82;
- text-transform: uppercase;
-}
-
-.nav {
- display: flex;
- align-items: center;
- gap: clamp(14px, 3vw, 34px);
-}
-
-.navLink {
- position: relative;
- padding: 10px 0;
- color: var(--color-ink-muted);
- font-size: 0.76rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.navLink::after {
- position: absolute;
- right: 0;
- bottom: 3px;
- left: 0;
- height: 3px;
- border-radius: 3px;
- background: var(--color-orange);
- transform: scaleX(0);
- transition: transform 160ms ease;
- content: '';
-}
-
-.navLink:hover,
-.active {
- color: var(--color-ink);
-}
-
-.active::after {
- transform: scaleX(1);
-}
-
-.count {
- display: inline-grid;
- place-items: center;
- min-width: 20px;
- height: 20px;
- margin-left: 3px;
- padding: 0 5px;
- color: var(--color-ink);
- border-radius: 999px;
- background: var(--color-acid);
- font-family: var(--font-mono);
- font-size: 0.66rem;
-}
-
-.account {
- display: grid;
- grid-template-columns: auto auto;
- align-items: center;
- justify-items: end;
- gap: 2px 10px;
-}
-
-.accountMeta {
- grid-column: 1 / -1;
- color: var(--color-ink-faint);
- font-family: var(--font-mono);
- font-size: 0.6rem;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.accountLink {
- max-width: 160px;
- overflow: hidden;
- font-size: 0.82rem;
- font-weight: 720;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.cartError {
- grid-column: 1 / -1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- width: 100%;
- padding: 8px 12px;
- color: var(--color-error);
- border-left: 3px solid var(--color-error);
- background: color-mix(in srgb, var(--color-error) 8%, transparent);
- font-size: 0.75rem;
-}
-
-.page {
- min-height: calc(100vh - 160px);
-}
-
-.footer {
- display: flex;
- justify-content: space-between;
- gap: 24px;
- padding: 28px clamp(18px, 5vw, 72px) 90px;
- color: var(--color-ink-faint);
- border-top: 1px solid var(--color-line);
- font-family: var(--font-mono);
- font-size: 0.68rem;
- letter-spacing: 0.06em;
- text-transform: uppercase;
-}
-
-@media (max-width: 850px) {
- .header {
- grid-template-columns: 1fr auto;
- }
-
- .nav {
- grid-column: 1 / -1;
- grid-row: 2;
- overflow-x: auto;
- }
-}
-
-@media (max-width: 540px) {
- .header {
- padding-inline: 14px;
- }
-
- .brandText,
- .accountMeta {
- display: none;
- }
-
- .nav {
- gap: 18px;
- }
-
- .footer {
- flex-direction: column;
- padding-inline: 18px;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/layouts/store-shell/types/store-shell-layout-props.type.ts b/examples/demo-frontend/src/compositions/layouts/store-shell/types/store-shell-layout-props.type.ts
deleted file mode 100644
index afe0a47..0000000
--- a/examples/demo-frontend/src/compositions/layouts/store-shell/types/store-shell-layout-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры общего storefront layout.
- */
-export type StoreShellLayoutParams = object
-
-/**
- * Атрибуты корневого контейнера.
- */
-type RootAttrs = ComponentPropsWithoutRef<'div'>
-
-/**
- * Props общего storefront layout.
- */
-export type StoreShellLayoutProps = RootAttrs & StoreShellLayoutParams
diff --git a/examples/demo-frontend/src/compositions/screens/cart/cart.screen.tsx b/examples/demo-frontend/src/compositions/screens/cart/cart.screen.tsx
deleted file mode 100644
index 9ba08cf..0000000
--- a/examples/demo-frontend/src/compositions/screens/cart/cart.screen.tsx
+++ /dev/null
@@ -1,374 +0,0 @@
-'use client'
-
-import type { ChangeEvent, FocusEvent } from 'react'
-import { useEffect, useRef, useState } from 'react'
-import cl from 'clsx'
-import Image from 'next/image'
-import Link from 'next/link'
-import { useRouter } from 'next/navigation'
-
-import { useAuth } from '@/domains/auth'
-import { getCartProductQuantityLimit, useCart } from '@/domains/cart'
-import { PRODUCT_IMAGE_PLACEHOLDER, useCatalogCommands } from '@/domains/catalog'
-import { useOrderCommands, validateOrderDraft } from '@/domains/orders'
-import type { OrderDraftLine } from '@/domains/orders'
-import { formatMoney } from '@/shared/lib/format-money'
-import { isEmptyArray, isNonEmptyArray } from '@/shared/lib/value-predicates'
-import { Button } from '@/ui/button'
-import { FeedbackPanel } from '@/ui/feedback-panel'
-
-import type { CartScreenProps } from './types/cart-screen-props.type'
-import styles from './styles/cart.module.css'
-
-/**
- * Multi-domain cart and checkout composition.
- *
- * Используется для:
- * - координации cart, auth и orders без междоменного цикла
- * - отображения stock и checkout errors
- */
-export const CartScreen = (props: CartScreenProps) => {
- const { className, ...rootAttrs } = props
- const cart = useCart()
- const auth = useAuth()
- const catalogCommands = useCatalogCommands()
- const orderCommands = useOrderCommands()
- const router = useRouter()
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [checkoutError, setCheckoutError] = useState(null)
- const [quantityDraftsById, setQuantityDraftsById] = useState>({})
- const isMountedRef = useRef(false)
- const checkoutAttemptRef = useRef(0)
- const hasLines = isNonEmptyArray(cart.lines)
- const orderDraft: OrderDraftLine[] = cart.lines.map((line) => ({
- productId: line.product.id,
- quantity: line.quantity,
- availableStock: line.product.stock,
- currency: line.product.currency,
- expectedVersion: line.product.version,
- expectedUnitPriceCents: line.product.priceCents
- }))
- const draftError = validateOrderDraft(orderDraft)
- const canCheckout = draftError === null
- const totalLabel = cart.currency === null
- ? 'Mixed currencies'
- : formatMoney(cart.subtotalCents, cart.currency)
- const checkoutLabel = auth.status === 'authenticated'
- ? 'Place order'
- : auth.status === 'unavailable'
- ? 'Retry session first'
- : 'Sign in to checkout'
-
- useEffect(() => {
- isMountedRef.current = true
-
- return () => {
- isMountedRef.current = false
- checkoutAttemptRef.current += 1
- }
- }, [])
-
- /**
- * Сохраняет локальный quantity draft до commit на blur.
- */
- const handleQuantityChange = (
- event: ChangeEvent,
- productId: string
- ): void => {
- const value = event.currentTarget.value
-
- setQuantityDraftsById((currentDrafts) => ({
- ...currentDrafts,
- [productId]: value
- }))
- }
-
- /**
- * Применяет завершённое редактирование quantity без удаления на пустом draft.
- */
- const handleQuantityCommit = (
- event: FocusEvent,
- productId: string
- ): void => {
- const value = event.currentTarget.value.trim()
-
- if (value === '') {
- setQuantityDraftsById((currentDrafts) => {
- const nextDrafts = { ...currentDrafts }
-
- delete nextDrafts[productId]
-
- return nextDrafts
- })
- return
- }
-
- const quantity = Number(value)
-
- if (Number.isFinite(quantity)) {
- void cart.setQuantity(productId, quantity)
- }
-
- setQuantityDraftsById((currentDrafts) => {
- const nextDrafts = { ...currentDrafts }
-
- delete nextDrafts[productId]
-
- return nextDrafts
- })
- }
-
- /**
- * Проверяет lifecycle текущей checkout-попытки после async boundary.
- */
- const isCheckoutAttemptActive = (attemptId: number): boolean => {
- return isMountedRef.current && checkoutAttemptRef.current === attemptId
- }
-
- /**
- * Создаёт order из cart snapshot или направляет гостя к auth route.
- */
- const handleCheckout = async (): Promise => {
- if (auth.status === 'unavailable') {
- const result = await auth.refreshCurrentUser()
-
- if (!result.isSuccess) {
- setCheckoutError(result.error.message)
- }
- return
- }
-
- if (auth.status !== 'authenticated') {
- router.push('/sign-in')
- return
- }
-
- const sessionKey = auth.sessionKey
-
- if (sessionKey === null || isSubmitting) {
- return
- }
-
- const cartRevision = cart.revision
- const attemptId = checkoutAttemptRef.current + 1
-
- checkoutAttemptRef.current = attemptId
-
- setIsSubmitting(true)
- setCheckoutError(null)
-
- const productsResult = await catalogCommands.loadProducts(
- orderDraft.map((line) => line.productId)
- )
-
- if (!isCheckoutAttemptActive(attemptId)) {
- return
- }
-
- if (!productsResult.isSuccess) {
- setIsSubmitting(false)
- setCheckoutError(productsResult.error.message)
- return
- }
-
- if (!auth.isCurrentSession(sessionKey)) {
- setIsSubmitting(false)
- setCheckoutError('The active session changed. Start checkout again.')
- return
- }
-
- const reconciliationStatus = await cart.reconcileProducts(
- productsResult.data,
- cartRevision
- )
-
- if (!isCheckoutAttemptActive(attemptId)) {
- return
- }
-
- if (reconciliationStatus === 'stale') {
- setIsSubmitting(false)
- setCheckoutError('The cart changed during checkout. Review it and try again.')
- return
- }
-
- if (reconciliationStatus === 'unavailable') {
- setIsSubmitting(false)
- setCheckoutError('Cart storage is unavailable. Checkout was not started.')
- return
- }
-
- if (reconciliationStatus === 'updated') {
- setIsSubmitting(false)
- setCheckoutError('Inventory changed. Review the refreshed totals before checkout.')
- return
- }
-
- const cartRevisionStatus = await cart.isCurrentRevision(cartRevision)
-
- if (!isCheckoutAttemptActive(attemptId)) {
- return
- }
-
- if (cartRevisionStatus === 'unavailable') {
- setIsSubmitting(false)
- setCheckoutError('Cart storage is unavailable. Checkout was not started.')
- return
- }
-
- if (cartRevisionStatus === 'stale' || !auth.isCurrentSession(sessionKey)) {
- setIsSubmitting(false)
- setCheckoutError('The checkout scope changed. Review the cart and try again.')
- return
- }
-
- const result = await orderCommands.createOrder(orderDraft, sessionKey)
-
- if (result.isSuccess) {
- const clearStatus = await cart.clearCart(cartRevision)
-
- if (!isCheckoutAttemptActive(attemptId)) {
- return
- }
-
- setIsSubmitting(false)
-
- if (clearStatus === 'unavailable') {
- setCheckoutError('The order was created, but cart storage could not be updated.')
- return
- }
-
- if (clearStatus === 'stale') {
- setCheckoutError('The order was created, but the cart changed in another tab.')
- return
- }
-
- if (auth.isCurrentSession(sessionKey)) {
- router.push('/orders')
- }
- return
- }
-
- if (!isCheckoutAttemptActive(attemptId)) {
- return
- }
-
- setIsSubmitting(false)
- setCheckoutError(result.error.message)
- }
-
- return (
-
-
-
- {isEmptyArray(cart.lines) && cart.isHydrated && (
-
-
- Open catalog
-
-
- )}
-
- {hasLines && (
-
-
-
-
-
- )}
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/cart/index.ts b/examples/demo-frontend/src/compositions/screens/cart/index.ts
deleted file mode 100644
index 67e9737..0000000
--- a/examples/demo-frontend/src/compositions/screens/cart/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { CartScreen } from './cart.screen'
-export type { CartScreenProps } from './types/cart-screen-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/cart/styles/cart.module.css b/examples/demo-frontend/src/compositions/screens/cart/styles/cart.module.css
deleted file mode 100644
index c23c7a2..0000000
--- a/examples/demo-frontend/src/compositions/screens/cart/styles/cart.module.css
+++ /dev/null
@@ -1,200 +0,0 @@
-.root {
- display: grid;
- gap: 34px;
- padding: clamp(42px, 7vw, 100px) clamp(18px, 6vw, 90px) 100px;
-}
-
-.header {
- display: flex;
- align-items: end;
- justify-content: space-between;
- gap: 20px;
- padding-bottom: 28px;
- border-bottom: 1px solid var(--color-line);
-}
-
-.kicker,
-.sequence,
-.lineInfo > span,
-.quantity span {
- color: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.66rem;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-.header h1 {
- margin-top: 13px;
- font-family: var(--font-display);
- font-size: clamp(3rem, 8vw, 7.5rem);
- letter-spacing: -0.055em;
- line-height: 0.82;
-}
-
-.count {
- padding: 10px 14px;
- border: 1px solid var(--color-line-strong);
- border-radius: 999px;
- font-family: var(--font-mono);
- font-size: 0.72rem;
- text-transform: uppercase;
-}
-
-.layout {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(300px, 0.38fr);
- gap: clamp(30px, 6vw, 90px);
- align-items: start;
-}
-
-.lines {
- display: grid;
-}
-
-.line {
- display: grid;
- grid-template-columns: 132px minmax(0, 1fr) 100px auto;
- align-items: center;
- gap: 22px;
- padding: 18px 0;
- border-bottom: 1px solid var(--color-line);
-}
-
-.imageWrap {
- position: relative;
- overflow: hidden;
- aspect-ratio: 1;
- border-radius: 15px;
- background: var(--color-surface-strong);
-}
-
-.image {
- object-fit: cover;
-}
-
-.lineInfo {
- display: grid;
- gap: 7px;
-}
-
-.lineInfo h2 {
- font-family: var(--font-display);
- font-size: clamp(1.4rem, 3vw, 2.25rem);
- line-height: 0.95;
-}
-
-.lineInfo strong {
- font-family: var(--font-mono);
- font-size: 0.76rem;
-}
-
-.quantity {
- display: grid;
- gap: 7px;
-}
-
-.quantity input {
- width: 80px;
- min-height: 42px;
- padding: 0 10px;
- border: 1px solid var(--color-line-strong);
- border-radius: 10px;
- background: var(--color-surface);
- font: inherit;
-}
-
-.summary {
- position: sticky;
- top: 110px;
- display: grid;
- gap: 24px;
- padding: clamp(24px, 4vw, 42px);
- border-radius: 24px;
- background: var(--color-acid);
-}
-
-.sequence {
- color: color-mix(in srgb, var(--color-ink) 58%, transparent);
-}
-
-.summary h2 {
- font-family: var(--font-display);
- font-size: clamp(2rem, 4vw, 3.6rem);
- line-height: 0.9;
-}
-
-.summary dl {
- display: grid;
-}
-
-.summary dl div {
- display: flex;
- justify-content: space-between;
- gap: 20px;
- padding: 13px 0;
- border-bottom: 1px solid color-mix(in srgb, var(--color-ink) 18%, transparent);
-}
-
-.summary dt {
- color: color-mix(in srgb, var(--color-ink) 60%, transparent);
- font-size: 0.8rem;
-}
-
-.summary dd {
- font-family: var(--font-mono);
- font-size: 0.76rem;
-}
-
-.warning,
-.error {
- padding: 11px 13px;
- border-radius: 10px;
- font-size: 0.78rem;
- line-height: 1.45;
-}
-
-.warning {
- background: rgb(255 255 255 / 38%);
-}
-
-.error {
- color: var(--color-error);
- background: rgb(255 255 255 / 55%);
-}
-
-.catalogLink {
- padding: 14px 20px;
- color: var(--color-paper);
- border-radius: 999px;
- background: var(--color-ink);
- font-size: 0.72rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-@media (max-width: 980px) {
- .layout {
- grid-template-columns: 1fr;
- }
-
- .summary {
- position: static;
- }
-}
-
-@media (max-width: 700px) {
- .line {
- grid-template-columns: 92px 1fr;
- }
-
- .quantity,
- .line > button {
- grid-column: 2;
- }
-
- .quantity input {
- width: 100%;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/cart/types/cart-screen-props.type.ts b/examples/demo-frontend/src/compositions/screens/cart/types/cart-screen-props.type.ts
deleted file mode 100644
index 0649ee0..0000000
--- a/examples/demo-frontend/src/compositions/screens/cart/types/cart-screen-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры cart and checkout screen.
- */
-export type CartScreenParams = object
-
-/**
- * Атрибуты корневого main без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props cart and checkout screen.
- */
-export type CartScreenProps = RootAttrs & CartScreenParams
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/catalog.screen.tsx b/examples/demo-frontend/src/compositions/screens/catalog/catalog.screen.tsx
deleted file mode 100644
index 2895ba0..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/catalog.screen.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-'use client'
-
-import type { ChangeEvent } from 'react'
-import { useDeferredValue, useState } from 'react'
-import cl from 'clsx'
-
-import { useCategories, useProductCatalog } from '@/domains/catalog'
-import type { CatalogFilters } from '@/domains/catalog'
-import { useOnDemoFixtureChange } from '@/domains/demo-control'
-import { isEmptyArray, isNonEmptyArray, isOneOf } from '@/shared/lib/value-predicates'
-import { Button } from '@/ui/button'
-import { FeedbackPanel } from '@/ui/feedback-panel'
-
-import { ProductCard } from './ui/product-card'
-import type { CatalogScreenProps } from './types/catalog-screen-props.type'
-import styles from './styles/catalog.module.css'
-
-const CATALOG_SORTS = ['newest', 'price-asc', 'price-desc', 'name'] as const
-const SKELETON_IDS = ['one', 'two', 'three', 'four', 'five', 'six'] as const
-
-/**
- * Публичная витрина с фильтрами и offset pagination.
- *
- * Используется для:
- * - поиска и сравнения fixture-продуктов
- * - демонстрации loading, empty и transport error outcomes
- */
-export const CatalogScreen = (props: CatalogScreenProps) => {
- const { className, ...rootAttrs } = props
- const [filters, setFilters] = useState({
- page: 1,
- limit: 12,
- search: '',
- categoryId: '',
- sort: 'newest'
- })
- const deferredSearch = useDeferredValue(filters.search)
- const catalog = useProductCatalog({ ...filters, search: deferredSearch })
- const categoriesState = useCategories()
- const hasProducts = isNonEmptyArray(catalog.products)
- const shouldShowEmpty =
- !catalog.isLoading && catalog.error === null && isEmptyArray(catalog.products)
- const resultSummary = catalog.pagination
- ? `${catalog.pagination.total} objects / page ${catalog.pagination.page}`
- : 'Connecting to inventory'
- const canGoBack = (catalog.pagination?.page ?? 1) > 1
- const canGoForward =
- catalog.pagination !== null && catalog.pagination.page < catalog.pagination.totalPages
-
- useOnDemoFixtureChange((kind) => {
- if (kind === 'data') {
- setFilters((current) => ({ ...current, page: 1 }))
- }
- })
-
- /**
- * Обновляет search-фильтр, сохраняя ввод отзывчивым через deferred value.
- */
- const handleSearchChange = (event: ChangeEvent): void => {
- setFilters((current) => ({ ...current, search: event.target.value, page: 1 }))
- }
-
- /**
- * Применяет category-фильтр и возвращает выдачу на первую страницу.
- */
- const handleCategoryChange = (event: ChangeEvent): void => {
- setFilters((current) => ({ ...current, categoryId: event.target.value, page: 1 }))
- }
-
- /**
- * Применяет только поддерживаемую доменом сортировку.
- */
- const handleSortChange = (event: ChangeEvent): void => {
- const value = event.target.value
-
- if (isOneOf(value, CATALOG_SORTS)) {
- setFilters((current) => ({ ...current, sort: value, page: 1 }))
- }
- }
-
- /**
- * Загружает предыдущую страницу выдачи.
- */
- const handlePreviousPage = (): void => {
- setFilters((current) => ({ ...current, page: Math.max(1, current.page - 1) }))
- }
-
- /**
- * Загружает следующую страницу выдачи.
- */
- const handleNextPage = (): void => {
- setFilters((current) => ({ ...current, page: current.page + 1 }))
- }
-
- /**
- * Повторяет неуспешный catalog query.
- */
- const handleReload = (): void => {
- void catalog.reload()
- }
-
- return (
-
-
- Deterministic objects for frontend systems
-
- Useful things,
-
- visible boundaries.
-
-
- A storefront where every loading state, cache edge, auth transition and conflict can be
- reproduced on demand.
-
- 01 / CATALOG
-
-
-
-
-
-
-
-
-
-
-
-
- {resultSummary}
- {catalog.isRefreshing && Refreshing}
-
-
- {catalog.isLoading && (
-
- {SKELETON_IDS.map((id) => (
-
- ))}
-
- )}
-
- {catalog.error && (
-
-
-
- )}
-
- {shouldShowEmpty && (
-
- )}
-
- {hasProducts && (
-
- {catalog.products.map((product, index) => (
-
- ))}
-
- )}
-
- {catalog.pagination && (catalog.pagination.totalPages > 1 || filters.page > 1) && (
-
-
-
- {catalog.pagination.page} / {catalog.pagination.totalPages}
-
-
-
- )}
-
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/index.ts b/examples/demo-frontend/src/compositions/screens/catalog/index.ts
deleted file mode 100644
index e49ebba..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { CatalogScreen } from './catalog.screen'
-export type { CatalogScreenProps } from './types/catalog-screen-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/styles/catalog.module.css b/examples/demo-frontend/src/compositions/screens/catalog/styles/catalog.module.css
deleted file mode 100644
index b3bba7c..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/styles/catalog.module.css
+++ /dev/null
@@ -1,212 +0,0 @@
-.root {
- padding-bottom: 80px;
-}
-
-.hero {
- position: relative;
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(250px, 0.34fr);
- gap: 20px 60px;
- min-height: 520px;
- padding: clamp(72px, 11vw, 150px) clamp(20px, 7vw, 110px) 80px;
- overflow: hidden;
- border-bottom: 1px solid var(--color-line);
-}
-
-.hero::before {
- position: absolute;
- top: 18%;
- right: -8vw;
- width: clamp(230px, 34vw, 520px);
- aspect-ratio: 1;
- border: 1px solid var(--color-line-strong);
- border-radius: 50%;
- box-shadow:
- inset 0 0 0 38px var(--color-paper),
- inset 0 0 0 39px var(--color-line);
- content: '';
-}
-
-.hero::after {
- position: absolute;
- right: 15%;
- bottom: -140px;
- width: 240px;
- height: 300px;
- background: var(--color-acid);
- transform: rotate(24deg);
- content: '';
-}
-
-.kicker,
-.sequence {
- position: relative;
- z-index: 1;
- font-family: var(--font-mono);
- font-size: 0.7rem;
- letter-spacing: 0.12em;
- text-transform: uppercase;
-}
-
-.kicker {
- grid-column: 1 / -1;
- color: var(--color-blue);
-}
-
-.title {
- position: relative;
- z-index: 1;
- max-width: 10ch;
- font-family: var(--font-display);
- font-size: clamp(3.3rem, 9vw, 8.8rem);
- letter-spacing: -0.065em;
- line-height: 0.78;
-}
-
-.lede {
- position: relative;
- z-index: 1;
- align-self: end;
- max-width: 33ch;
- color: var(--color-ink-muted);
- font-size: clamp(1rem, 1.6vw, 1.3rem);
- line-height: 1.58;
-}
-
-.sequence {
- position: absolute;
- bottom: 30px;
- left: clamp(20px, 7vw, 110px);
- color: var(--color-ink-faint);
-}
-
-.catalog {
- display: grid;
- gap: 26px;
- padding: 54px clamp(18px, 5vw, 72px) 0;
-}
-
-.filters {
- display: grid;
- grid-template-columns: minmax(240px, 1fr) repeat(2, minmax(180px, 0.35fr));
- gap: 14px;
-}
-
-.searchField,
-.selectField {
- display: grid;
- gap: 8px;
-}
-
-.filters label > span {
- color: var(--color-ink-faint);
- font-size: 0.66rem;
- font-weight: 760;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-.filters :is(input, select) {
- width: 100%;
- min-height: 50px;
- padding: 0 15px;
- color: var(--color-ink);
- border: 1px solid var(--color-line-strong);
- border-radius: 13px;
- outline: none;
- background: var(--color-surface);
- font: inherit;
-}
-
-.filters :is(input, select):focus {
- border-color: var(--color-blue);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-blue) 14%, transparent);
-}
-
-.resultBar {
- display: flex;
- justify-content: space-between;
- min-height: 24px;
- color: var(--color-ink-faint);
- font-family: var(--font-mono);
- font-size: 0.68rem;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.refreshing {
- color: var(--color-blue);
-}
-
-.productGrid {
- display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: clamp(14px, 2vw, 26px);
-}
-
-.skeleton {
- min-height: 410px;
- border: 1px solid var(--color-line);
- border-radius: 22px;
- background:
- linear-gradient(105deg, transparent 30%, rgb(255 255 255 / 48%) 45%, transparent 60%)
- 0 0 / 220% 100%,
- var(--color-surface-strong);
- animation: sweep 1.4s linear infinite;
-}
-
-.pagination {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 24px;
- padding-top: 18px;
- font-family: var(--font-mono);
- font-size: 0.72rem;
-}
-
-@keyframes sweep {
- to {
- background-position: -220% 0;
- }
-}
-
-@media (max-width: 980px) {
- .hero {
- grid-template-columns: 1fr;
- }
-
- .lede {
- max-width: 50ch;
- }
-
- .filters,
- .productGrid {
- grid-template-columns: repeat(2, minmax(0, 1fr));
- }
-
- .searchField {
- grid-column: 1 / -1;
- }
-}
-
-@media (max-width: 640px) {
- .hero {
- min-height: 440px;
- padding-top: 70px;
- }
-
- .hero::after {
- right: -40px;
- width: 130px;
- }
-
- .filters,
- .productGrid {
- grid-template-columns: 1fr;
- }
-
- .searchField {
- grid-column: auto;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/types/catalog-screen-props.type.ts b/examples/demo-frontend/src/compositions/screens/catalog/types/catalog-screen-props.type.ts
deleted file mode 100644
index 66d3a1a..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/types/catalog-screen-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры storefront catalog screen.
- */
-export type CatalogScreenParams = object
-
-/**
- * Атрибуты корневого main без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props storefront catalog screen.
- */
-export type CatalogScreenProps = RootAttrs & CatalogScreenParams
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/index.ts b/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/index.ts
deleted file mode 100644
index db1e46f..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ProductCard } from './product-card'
-export type { ProductCardProps } from './types/product-card-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/product-card.tsx b/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/product-card.tsx
deleted file mode 100644
index d0721fd..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/product-card.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-'use client'
-
-import cl from 'clsx'
-import Image from 'next/image'
-import Link from 'next/link'
-
-import { useCart } from '@/domains/cart'
-import { PRODUCT_IMAGE_PLACEHOLDER } from '@/domains/catalog'
-import { formatMoney } from '@/shared/lib/format-money'
-import { Button } from '@/ui/button'
-
-import type { ProductCardProps } from './types/product-card-props.type'
-import styles from './styles/product-card.module.css'
-
-/**
- * Карточка продукта для grid-выдачи catalog screen.
- *
- * Используется для:
- * - перехода к detail route
- * - добавления доступного продукта в application cart
- */
-export const ProductCard = (props: ProductCardProps) => {
- const { product, index, className, ...rootAttrs } = props
- const cart = useCart()
- const isSoldOut = product.stock === 0
- const stockLabel = isSoldOut ? 'Out of stock' : `${product.stock} ready`
-
- /**
- * Добавляет продукт в cart domain без transport-зависимости компонента.
- */
- const handleAdd = (): void => {
- void cart.addProduct(product)
- }
-
- return (
-
-
- {
- event.currentTarget.srcset = ''
- event.currentTarget.src = PRODUCT_IMAGE_PLACEHOLDER
- }}
- />
- {String(index + 1).padStart(2, '0')}
-
-
-
-
- {stockLabel}
- {product.rating.toFixed(1)} / 5
-
-
-
{product.name}
-
-
{product.description}
-
- {formatMoney(product.priceCents, product.currency)}
-
-
-
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/styles/product-card.module.css b/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/styles/product-card.module.css
deleted file mode 100644
index a14023b..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/styles/product-card.module.css
+++ /dev/null
@@ -1,98 +0,0 @@
-.root {
- display: grid;
- overflow: hidden;
- min-width: 0;
- border: 1px solid var(--color-line);
- border-radius: 22px;
- background: var(--color-surface);
- box-shadow: var(--shadow-card);
- transition:
- transform 200ms ease,
- box-shadow 200ms ease;
-}
-
-.root:hover {
- box-shadow: var(--shadow-card-hover);
- transform: translateY(-5px);
-}
-
-.imageLink {
- position: relative;
- display: block;
- overflow: hidden;
- aspect-ratio: 4 / 3;
- background: var(--color-surface-strong);
-}
-
-.image {
- object-fit: cover;
- filter: saturate(0.78) contrast(1.04);
- transition: transform 420ms cubic-bezier(0.2, 0.8, 0.2, 1);
-}
-
-.root:hover .image {
- transform: scale(1.035);
-}
-
-.index {
- position: absolute;
- top: 12px;
- left: 12px;
- display: grid;
- place-items: center;
- width: 38px;
- height: 38px;
- color: var(--color-paper);
- border-radius: 50%;
- background: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.7rem;
-}
-
-.body {
- display: grid;
- grid-template-rows: auto auto 1fr auto;
- gap: 13px;
- padding: 20px;
-}
-
-.meta,
-.footer {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 14px;
-}
-
-.meta {
- color: var(--color-ink-faint);
- font-family: var(--font-mono);
- font-size: 0.64rem;
- letter-spacing: 0.07em;
- text-transform: uppercase;
-}
-
-.name {
- font-family: var(--font-display);
- font-size: clamp(1.35rem, 2.5vw, 2rem);
- line-height: 0.98;
-}
-
-.description {
- display: -webkit-box;
- overflow: hidden;
- color: var(--color-ink-muted);
- font-size: 0.9rem;
- line-height: 1.55;
- -webkit-box-orient: vertical;
- -webkit-line-clamp: 2;
-}
-
-.footer {
- padding-top: 4px;
-}
-
-.price {
- font-family: var(--font-mono);
- font-size: 0.94rem;
-}
diff --git a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/types/product-card-props.type.ts b/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/types/product-card-props.type.ts
deleted file mode 100644
index b810789..0000000
--- a/examples/demo-frontend/src/compositions/screens/catalog/ui/product-card/types/product-card-props.type.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-import type { Product } from '@/domains/catalog'
-
-/**
- * Собственные параметры product card внутри catalog screen.
- */
-export type ProductCardParams = {
- /** Catalog-модель продукта. */
- product: Product
- /** Позиция карточки для editorial numbering. */
- index: number
-}
-
-/**
- * Атрибуты корневой article-карточки без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props product card внутри catalog screen.
- */
-export type ProductCardProps = RootAttrs & ProductCardParams
diff --git a/examples/demo-frontend/src/compositions/screens/orders/index.ts b/examples/demo-frontend/src/compositions/screens/orders/index.ts
deleted file mode 100644
index e60a5a9..0000000
--- a/examples/demo-frontend/src/compositions/screens/orders/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { OrdersScreen } from './orders.screen'
-export type { OrdersScreenProps } from './types/orders-screen-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/orders/orders.screen.tsx b/examples/demo-frontend/src/compositions/screens/orders/orders.screen.tsx
deleted file mode 100644
index a825044..0000000
--- a/examples/demo-frontend/src/compositions/screens/orders/orders.screen.tsx
+++ /dev/null
@@ -1,212 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import cl from 'clsx'
-import Link from 'next/link'
-
-import { useAuth } from '@/domains/auth'
-import { canCancelOrder, useOrderCommands, useOrders } from '@/domains/orders'
-import type { OrderError } from '@/domains/orders'
-import { formatDate } from '@/shared/lib/format-date'
-import { formatMoney } from '@/shared/lib/format-money'
-import { isEmptyArray, isNonEmptyArray } from '@/shared/lib/value-predicates'
-import { Button } from '@/ui/button'
-import { FeedbackPanel } from '@/ui/feedback-panel'
-
-import type { OrdersScreenProps } from './types/orders-screen-props.type'
-import styles from './styles/orders.module.css'
-
-/**
- * Protected история заказов с допустимыми state transitions.
- *
- * Используется для:
- * - проверки auth-aware deferred query
- * - демонстрации успешной и конфликтной отмены заказа
- */
-export const OrdersScreen = (props: OrdersScreenProps) => {
- const { className, ...rootAttrs } = props
- const auth = useAuth()
- const isAuthenticated = auth.status === 'authenticated'
- const ordersState = useOrders(isAuthenticated)
- const orderCommands = useOrderCommands()
- const [cancellingOrderId, setCancellingOrderId] = useState(null)
- const [actionError, setActionError] = useState(null)
- const hasOrders = isNonEmptyArray(ordersState.orders)
-
- /**
- * Повторяет неуспешный protected orders query.
- */
- const handleReload = (): void => {
- void ordersState.reload()
- }
-
- /**
- * Повторяет recoverable auth check перед protected query.
- */
- const handleSessionRetry = (): void => {
- void auth.refreshCurrentUser()
- }
-
- /**
- * Запрашивает допустимый переход заказа в cancelled.
- */
- const handleCancel = async (orderId: string): Promise => {
- const sessionKey = auth.sessionKey
-
- if (sessionKey === null) {
- return
- }
-
- setCancellingOrderId(orderId)
- setActionError(null)
-
- const result = await orderCommands.cancelOrder(orderId, sessionKey)
-
- setCancellingOrderId(null)
-
- if (!result.isSuccess) {
- setActionError(result.error)
- }
- }
-
- if (auth.status === 'checking') {
- return (
-
-
-
- )
- }
-
- if (!isAuthenticated) {
- if (auth.status === 'unavailable') {
- return (
-
-
-
-
-
- )
- }
-
- return (
-
-
-
- Choose account
-
-
-
- )
- }
-
- return (
-
-
-
- {ordersState.isLoading && (
-
- )}
-
- {ordersState.error && (
-
-
-
- )}
-
- {actionError && {actionError.message}
}
-
- {isEmptyArray(ordersState.orders) && !ordersState.isLoading && ordersState.error === null && (
-
-
- Browse catalog
-
-
- )}
-
- {hasOrders && (
-
- {ordersState.orders.map((order, index) => {
- const canCancel = canCancelOrder(order)
- const isCancelling = cancellingOrderId === order.id
-
- return (
-
- {String(index + 1).padStart(2, '0')}
-
-
- {formatDate(order.createdAt)}
-
{order.id}
-
-
- {order.status}
-
-
-
-
- {order.lines.map((line) => (
-
- {line.quantity} x
- {line.productName}
- {formatMoney(line.unitPriceCents, order.currency)}
-
- ))}
-
-
-
- {formatMoney(order.totalCents, order.currency)}
- {canCancel && (
-
- )}
-
-
- )
- })}
-
- )}
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/orders/styles/orders.module.css b/examples/demo-frontend/src/compositions/screens/orders/styles/orders.module.css
deleted file mode 100644
index b4f4ad5..0000000
--- a/examples/demo-frontend/src/compositions/screens/orders/styles/orders.module.css
+++ /dev/null
@@ -1,187 +0,0 @@
-.root {
- display: grid;
- gap: 38px;
- padding: clamp(48px, 8vw, 112px) clamp(18px, 7vw, 110px) 110px;
-}
-
-.header {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(260px, 0.42fr);
- gap: 40px;
- align-items: end;
- padding-bottom: 34px;
- border-bottom: 1px solid var(--color-line);
-}
-
-.kicker {
- color: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.68rem;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-.header h1 {
- max-width: 9ch;
- margin-top: 14px;
- font-family: var(--font-display);
- font-size: clamp(3rem, 8vw, 7.4rem);
- letter-spacing: -0.055em;
- line-height: 0.8;
-}
-
-.lede {
- color: var(--color-ink-muted);
- font-size: 1rem;
- line-height: 1.7;
-}
-
-.orders {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 20px;
-}
-
-.order {
- position: relative;
- display: grid;
- gap: 26px;
- overflow: hidden;
- padding: clamp(24px, 4vw, 38px);
- border: 1px solid var(--color-line);
- border-radius: 24px;
- background: var(--color-surface);
- box-shadow: var(--shadow-card);
-}
-
-.orderIndex {
- position: absolute;
- right: -8px;
- bottom: -22px;
- color: color-mix(in srgb, var(--color-ink) 5%, transparent);
- font-family: var(--font-display);
- font-size: 9rem;
- line-height: 1;
- pointer-events: none;
-}
-
-.orderHeader,
-.orderFooter,
-.line {
- position: relative;
- z-index: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 18px;
-}
-
-.orderHeader span,
-.line span {
- color: var(--color-ink-faint);
- font-family: var(--font-mono);
- font-size: 0.66rem;
-}
-
-.orderHeader h2 {
- margin-top: 6px;
- font-family: var(--font-display);
- font-size: 1.7rem;
-}
-
-.status {
- padding: 8px 11px;
- border-radius: 999px;
- background: var(--color-surface-strong);
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.status[data-status='pending'] {
- color: #845300;
- background: #ffe5ad;
-}
-
-.status[data-status='paid'] {
- color: #215a00;
- background: #dfffa0;
-}
-
-.status[data-status='shipped'] {
- color: #123ec0;
- background: #dae4ff;
-}
-
-.status[data-status='cancelled'] {
- color: #8b2920;
- background: #ffd9d5;
-}
-
-.lines {
- position: relative;
- z-index: 1;
- display: grid;
-}
-
-.line {
- display: grid;
- grid-template-columns: auto 1fr auto;
- padding: 11px 0;
- border-bottom: 1px solid var(--color-line);
-}
-
-.line strong {
- font-size: 0.84rem;
-}
-
-.orderFooter {
- padding-top: 4px;
-}
-
-.orderFooter > strong {
- font-family: var(--font-mono);
- font-size: 1rem;
-}
-
-.actionError {
- padding: 14px 18px;
- color: var(--color-error);
- border-left: 5px solid var(--color-error);
- background: color-mix(in srgb, var(--color-error) 8%, transparent);
-}
-
-.loading {
- min-height: 380px;
- border-radius: 24px;
- background: var(--color-surface-strong);
- animation: pulse 1s ease-in-out infinite alternate;
-}
-
-.outcome {
- min-height: calc(100vh - 82px);
- place-items: center;
-}
-
-.primaryLink {
- padding: 14px 20px;
- color: var(--color-paper);
- border-radius: 999px;
- background: var(--color-ink);
- font-size: 0.72rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-@keyframes pulse {
- to {
- opacity: 0.45;
- }
-}
-
-@media (max-width: 850px) {
- .header,
- .orders {
- grid-template-columns: 1fr;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/orders/types/orders-screen-props.type.ts b/examples/demo-frontend/src/compositions/screens/orders/types/orders-screen-props.type.ts
deleted file mode 100644
index 62aac45..0000000
--- a/examples/demo-frontend/src/compositions/screens/orders/types/orders-screen-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры protected orders screen.
- */
-export type OrdersScreenParams = object
-
-/**
- * Атрибуты корневого main без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props protected orders screen.
- */
-export type OrdersScreenProps = RootAttrs & OrdersScreenParams
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/index.ts b/examples/demo-frontend/src/compositions/screens/product-admin/index.ts
deleted file mode 100644
index 287840d..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ProductAdminScreen } from './product-admin.screen'
-export type { ProductAdminScreenProps } from './types/product-admin-screen-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/product-admin.screen.tsx b/examples/demo-frontend/src/compositions/screens/product-admin/product-admin.screen.tsx
deleted file mode 100644
index d913d8c..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/product-admin.screen.tsx
+++ /dev/null
@@ -1,351 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import cl from 'clsx'
-import Link from 'next/link'
-
-import { useAuth } from '@/domains/auth'
-import {
- useCategories,
- useCatalogCommands,
- useProductCatalog
-} from '@/domains/catalog'
-import type {
- CatalogError,
- CreateProductInput,
- Product,
- UpdateProductInput
-} from '@/domains/catalog'
-import { useOnDemoFixtureChange } from '@/domains/demo-control'
-import { formatMoney } from '@/shared/lib/format-money'
-import { isNonEmptyArray } from '@/shared/lib/value-predicates'
-import { Button } from '@/ui/button'
-import { FeedbackPanel } from '@/ui/feedback-panel'
-
-import { ProductForm } from './ui/product-form'
-import type { ProductAdminScreenProps } from './types/product-admin-screen-props.type'
-import styles from './styles/product-admin.module.css'
-
-/**
- * Admin-only catalog mutation workspace.
- *
- * Используется для:
- * - create/update/delete RBAC-сценариев
- * - проверки optimistic-lock conflict outcome
- */
-export const ProductAdminScreen = (props: ProductAdminScreenProps) => {
- const { className, ...rootAttrs } = props
- const auth = useAuth()
- const catalogCommands = useCatalogCommands()
- const [adminPage, setAdminPage] = useState(1)
- const catalog = useProductCatalog({
- page: adminPage,
- limit: 100,
- search: '',
- categoryId: '',
- sort: 'newest'
- })
- const categoriesState = useCategories()
- const [selectedProduct, setSelectedProduct] = useState(null)
- const [formGeneration, setFormGeneration] = useState(0)
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [deletingProductId, setDeletingProductId] = useState(null)
- const [mutationError, setMutationError] = useState(null)
- const [successMessage, setSuccessMessage] = useState(null)
- const isAdmin = auth.status === 'authenticated' && auth.user?.role === 'admin'
- const hasProducts = isNonEmptyArray(catalog.products)
- const canGoBack = (catalog.pagination?.page ?? 1) > 1
- const canGoForward =
- catalog.pagination !== null && catalog.pagination.page < catalog.pagination.totalPages
- const isSelectedProductAvailable =
- selectedProduct !== null &&
- (
- catalog.isLoading ||
- catalog.error !== null ||
- catalog.products.some((product) => product.id === selectedProduct.id)
- )
- const formProduct = isSelectedProductAvailable ? selectedProduct : null
- const formKey = `${formProduct?.id ?? 'new'}-${formProduct?.version ?? 0}-${categoriesState.categories.length}-${formGeneration}`
-
- useOnDemoFixtureChange((kind) => {
- if (kind === 'data') {
- setAdminPage(1)
- setSelectedProduct(null)
- setMutationError(null)
- setFormGeneration((value) => value + 1)
- }
- })
-
- /**
- * Переключает product form в edit mode с последним catalog snapshot.
- */
- const handleEdit = (product: Product): void => {
- setSelectedProduct(product)
- setMutationError(null)
- setSuccessMessage(null)
- }
-
- /**
- * Возвращает product form в create mode.
- */
- const handleCancelEdit = (): void => {
- setSelectedProduct(null)
- setMutationError(null)
- setFormGeneration((value) => value + 1)
- }
-
- /**
- * Выбирает create или update domain scenario по форме input.
- */
- const handleSubmit = async (
- input: CreateProductInput | UpdateProductInput
- ): Promise => {
- const sessionKey = auth.sessionKey
-
- if (sessionKey === null) {
- return
- }
-
- setIsSubmitting(true)
- setMutationError(null)
- setSuccessMessage(null)
-
- const result = 'id' in input
- ? await catalogCommands.updateProduct(input, sessionKey)
- : await catalogCommands.createProduct(input, sessionKey)
-
- setIsSubmitting(false)
-
- if (!result.isSuccess) {
- setMutationError(result.error)
- return
- }
-
- setSuccessMessage(`${result.data.name} saved at version ${result.data.version}.`)
- setSelectedProduct(null)
- setFormGeneration((value) => value + 1)
- }
-
- /**
- * Удаляет продукт после явного browser confirmation.
- */
- const handleRemove = async (product: Product): Promise => {
- const sessionKey = auth.sessionKey
-
- if (sessionKey === null) {
- return
- }
-
- const shouldRemove = window.confirm(`Remove ${product.name}?`)
-
- if (!shouldRemove) {
- return
- }
-
- setDeletingProductId(product.id)
- setMutationError(null)
- setSuccessMessage(null)
-
- const result = await catalogCommands.removeProduct(product.id, sessionKey)
-
- setDeletingProductId(null)
-
- if (!result.isSuccess) {
- setMutationError(result.error)
- return
- }
-
- setSuccessMessage(`${product.name} removed.`)
-
- if (selectedProduct?.id === product.id) {
- setSelectedProduct(null)
- setFormGeneration((value) => value + 1)
- }
- }
-
- /**
- * Повторяет catalog query после transport failure.
- */
- const handleReload = (): void => {
- void catalog.reload()
- }
-
- /**
- * Открывает предыдущую admin inventory page.
- */
- const handlePreviousPage = (): void => {
- setAdminPage((page) => Math.max(1, page - 1))
- }
-
- /**
- * Открывает следующую admin inventory page.
- */
- const handleNextPage = (): void => {
- setAdminPage((page) => page + 1)
- }
-
- /**
- * Повторяет recoverable auth check перед admin workspace.
- */
- const handleSessionRetry = (): void => {
- void auth.refreshCurrentUser()
- }
-
- /**
- * Загружает authority snapshot после optimistic-lock conflict.
- */
- const handleLoadLatest = async (): Promise => {
- if (formProduct === null) {
- return
- }
-
- setIsSubmitting(true)
- const result = await catalogCommands.loadProducts([formProduct.id])
- setIsSubmitting(false)
-
- if (!result.isSuccess) {
- setMutationError(result.error)
- return
- }
-
- const latestProduct = result.data[0]
-
- if (latestProduct !== undefined) {
- setSelectedProduct(latestProduct)
- setMutationError(null)
- setSuccessMessage('Loaded the latest product version. Review changes before saving.')
- }
- }
-
- if (auth.status === 'checking') {
- return (
-
-
-
- )
- }
-
- if (!isAdmin) {
- if (auth.status === 'unavailable') {
- return (
-
-
-
-
-
- )
- }
-
- return (
-
-
-
- Sign in as admin
-
-
-
- )
- }
-
- return (
-
-
-
- {catalog.error && (
-
-
-
- )}
-
-
-
-
- {mutationError?.code === 'conflict' && formProduct !== null && (
-
- )}
-
-
-
-
-
- Live snapshot
-
Inventory
-
-
{catalog.pagination?.total ?? 0} products
-
-
- {successMessage && {successMessage}
}
-
- {hasProducts && (
-
- {catalog.products.map((product) => (
-
-
- v{product.version} / {product.stock} stock
-
{product.name}
- {formatMoney(product.priceCents, product.currency)}
-
-
-
-
-
-
- ))}
-
- )}
-
- {catalog.pagination && (catalog.pagination.totalPages > 1 || adminPage > 1) && (
-
-
- {catalog.pagination.page} / {catalog.pagination.totalPages}
-
-
- )}
-
-
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/styles/product-admin.module.css b/examples/demo-frontend/src/compositions/screens/product-admin/styles/product-admin.module.css
deleted file mode 100644
index 4a0004b..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/styles/product-admin.module.css
+++ /dev/null
@@ -1,162 +0,0 @@
-.root {
- display: grid;
- gap: 42px;
- padding: clamp(46px, 7vw, 100px) clamp(18px, 6vw, 90px) 110px;
-}
-
-.header {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(280px, 0.42fr);
- gap: 42px;
- align-items: end;
- padding-bottom: 34px;
- border-bottom: 1px solid var(--color-line);
-}
-
-.kicker,
-.inventoryHeader span,
-.productRow span {
- color: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.65rem;
- letter-spacing: 0.09em;
- text-transform: uppercase;
-}
-
-.header h1 {
- max-width: 10ch;
- margin-top: 14px;
- font-family: var(--font-display);
- font-size: clamp(3rem, 7.5vw, 7.2rem);
- letter-spacing: -0.055em;
- line-height: 0.8;
-}
-
-.header > p {
- color: var(--color-ink-muted);
- line-height: 1.7;
-}
-
-.workspace {
- display: grid;
- grid-template-columns: minmax(360px, 0.78fr) minmax(0, 1.22fr);
- gap: clamp(24px, 5vw, 70px);
- align-items: start;
-}
-
-.formColumn {
- display: grid;
- gap: 12px;
-}
-
-.inventory {
- display: grid;
- gap: 18px;
-}
-
-.inventoryHeader {
- display: flex;
- align-items: end;
- justify-content: space-between;
- gap: 20px;
- padding-bottom: 18px;
- border-bottom: 1px solid var(--color-line);
-}
-
-.inventoryHeader h2 {
- margin-top: 7px;
- font-family: var(--font-display);
- font-size: clamp(2rem, 4vw, 3.6rem);
- line-height: 0.9;
-}
-
-.inventoryHeader strong {
- color: var(--color-ink-faint);
- font-family: var(--font-mono);
- font-size: 0.68rem;
- text-transform: uppercase;
-}
-
-.productList {
- display: grid;
-}
-
-.productRow {
- display: grid;
- grid-template-columns: 1fr auto;
- align-items: center;
- gap: 18px;
- padding: 17px 0;
- border-bottom: 1px solid var(--color-line);
-}
-
-.productRow h3 {
- margin: 6px 0;
- font-family: var(--font-display);
- font-size: 1.45rem;
- line-height: 1;
-}
-
-.productRow small {
- color: var(--color-ink-muted);
- font-family: var(--font-mono);
- font-size: 0.72rem;
-}
-
-.rowActions {
- display: flex;
- flex-wrap: wrap;
- justify-content: end;
- gap: 8px;
-}
-
-.success {
- padding: 12px 14px;
- color: #315c00;
- border-left: 4px solid var(--color-acid-dark);
- background: color-mix(in srgb, var(--color-acid) 28%, transparent);
- font-size: 0.82rem;
-}
-
-.pagination {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 14px;
- padding-top: 12px;
- font-family: var(--font-mono);
- font-size: 0.68rem;
-}
-
-.outcome {
- min-height: calc(100vh - 82px);
- place-items: center;
-}
-
-.primaryLink {
- padding: 14px 20px;
- color: var(--color-paper);
- border-radius: 999px;
- background: var(--color-ink);
- font-size: 0.72rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-@media (max-width: 980px) {
- .header,
- .workspace {
- grid-template-columns: 1fr;
- }
-}
-
-@media (max-width: 560px) {
- .productRow {
- grid-template-columns: 1fr;
- }
-
- .rowActions {
- justify-content: start;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/types/product-admin-screen-props.type.ts b/examples/demo-frontend/src/compositions/screens/product-admin/types/product-admin-screen-props.type.ts
deleted file mode 100644
index c8e06ae..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/types/product-admin-screen-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры product administration screen.
- */
-export type ProductAdminScreenParams = object
-
-/**
- * Атрибуты корневого main без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props product administration screen.
- */
-export type ProductAdminScreenProps = RootAttrs & ProductAdminScreenParams
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/index.ts b/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/index.ts
deleted file mode 100644
index 0ce90e6..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ProductForm } from './product-form'
-export type { ProductFormProps } from './types/product-form-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/product-form.tsx b/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/product-form.tsx
deleted file mode 100644
index 52d5418..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/product-form.tsx
+++ /dev/null
@@ -1,272 +0,0 @@
-'use client'
-
-import type { ChangeEvent, FormEvent } from 'react'
-import { useState } from 'react'
-import cl from 'clsx'
-
-import type { CatalogCurrency, CreateProductInput, UpdateProductInput } from '@/domains/catalog'
-import { isOneOf } from '@/shared/lib/value-predicates'
-import { Button } from '@/ui/button'
-import { FormField } from '@/ui/form-field'
-
-import type { ProductFormProps } from './types/product-form-props.type'
-import styles from './styles/product-form.module.css'
-
-const CURRENCIES = ['USD', 'EUR'] as const
-
-/**
- * Локальное редактируемое состояние admin-формы.
- */
-type ProductFormValues = {
- /** Название продукта. */
- name: string
- /** Полное описание. */
- description: string
- /** Цена в cents как form string. */
- priceCents: string
- /** Валюта. */
- currency: CatalogCurrency
- /** Выбранная категория. */
- categoryId: string
- /** Stock как form string. */
- stock: string
- /** URL удалённого изображения. */
- imageUrl: string
-}
-
-/**
- * Создаёт initial form state из create mode или catalog snapshot.
- */
-const createInitialValues = (props: ProductFormProps): ProductFormValues => {
- const { product, categories } = props
-
- if (product !== null) {
- return {
- name: product.name,
- description: product.description,
- priceCents: String(product.priceCents),
- currency: product.currency,
- categoryId: product.categoryId,
- stock: String(product.stock),
- imageUrl: product.imageUrl
- }
- }
-
- return {
- name: '',
- description: '',
- priceCents: '9900',
- currency: 'USD',
- categoryId: categories[0]?.id ?? '',
- stock: '10',
- imageUrl: 'https://picsum.photos/seed/new-object/640/480'
- }
-}
-
-/**
- * Форма create/update с сохранением optimistic-lock версии продукта.
- *
- * Используется для:
- * - создания fixture-продукта
- * - редактирования последнего catalog snapshot
- */
-export const ProductForm = (props: ProductFormProps) => {
- const {
- categories,
- product,
- isSubmitting,
- error,
- onSubmit,
- onCancel,
- className,
- ...rootAttrs
- } = props
- const [values, setValues] = useState(() => createInitialValues(props))
- const isEditMode = product !== null
- const title = isEditMode ? `Edit ${product.name}` : 'Create object'
- const submitLabel = isEditMode ? 'Save version' : 'Create product'
-
- /**
- * Обновляет текстовое поле формы по имени control.
- */
- const handleTextChange = (
- event: ChangeEvent
- ): void => {
- const field = event.target.name
- const value = event.target.value
-
- if (field === 'name' || field === 'description' || field === 'imageUrl') {
- setValues((current) => ({ ...current, [field]: value }))
- }
- }
-
- /**
- * Обновляет числовое поле, сохраняя browser form representation.
- */
- const handleNumberChange = (event: ChangeEvent): void => {
- const field = event.target.name
- const value = event.target.value
-
- if (field === 'priceCents' || field === 'stock') {
- setValues((current) => ({ ...current, [field]: value }))
- }
- }
-
- /**
- * Обновляет выбранную category.
- */
- const handleCategoryChange = (event: ChangeEvent): void => {
- setValues((current) => ({ ...current, categoryId: event.target.value }))
- }
-
- /**
- * Обновляет только поддерживаемую backend currency.
- */
- const handleCurrencyChange = (event: ChangeEvent): void => {
- const value = event.target.value
-
- if (isOneOf(value, CURRENCIES)) {
- setValues((current) => ({ ...current, currency: value }))
- }
- }
-
- /**
- * Передаёт нормализованный domain input screen-владельцу.
- */
- const handleSubmit = async (event: FormEvent): Promise => {
- event.preventDefault()
-
- const baseInput: CreateProductInput = {
- name: values.name.trim(),
- description: values.description.trim(),
- priceCents: Number(values.priceCents),
- currency: values.currency,
- categoryId: values.categoryId,
- stock: Number(values.stock),
- imageUrl: values.imageUrl.trim()
- }
-
- if (product !== null) {
- const updateInput: UpdateProductInput = {
- ...baseInput,
- id: product.id,
- version: product.version
- }
-
- await onSubmit(updateInput)
- return
- }
-
- await onSubmit(baseInput)
- }
-
- return (
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/styles/product-form.module.css b/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/styles/product-form.module.css
deleted file mode 100644
index 7bdc5f7..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/styles/product-form.module.css
+++ /dev/null
@@ -1,54 +0,0 @@
-.root {
- display: grid;
- gap: 18px;
- padding: clamp(22px, 4vw, 38px);
- border: 1px solid var(--color-line);
- border-radius: 24px;
- background: var(--color-surface);
- box-shadow: var(--shadow-card);
-}
-
-.heading {
- display: flex;
- align-items: start;
- justify-content: space-between;
- gap: 18px;
- padding-bottom: 18px;
- border-bottom: 1px solid var(--color-line);
-}
-
-.heading span {
- color: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.64rem;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.heading h2 {
- margin-top: 8px;
- font-family: var(--font-display);
- font-size: clamp(1.8rem, 4vw, 3rem);
- line-height: 0.92;
-}
-
-.columns {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 14px;
-}
-
-.error {
- padding: 12px 14px;
- color: var(--color-error);
- border-left: 4px solid var(--color-error);
- background: color-mix(in srgb, var(--color-error) 8%, transparent);
- font-size: 0.8rem;
- line-height: 1.45;
-}
-
-@media (max-width: 560px) {
- .columns {
- grid-template-columns: 1fr;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/types/product-form-props.type.ts b/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/types/product-form-props.type.ts
deleted file mode 100644
index 47453d3..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-admin/ui/product-form/types/product-form-props.type.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-import type {
- Category,
- CreateProductInput,
- Product,
- UpdateProductInput
-} from '@/domains/catalog'
-
-/**
- * Собственные параметры admin product form.
- */
-export type ProductFormParams = {
- /** Справочник допустимых категорий. */
- categories: Category[]
- /** Редактируемый продукт или null для create mode. */
- product: Product | null
- /** Выполняется ли catalog mutation. */
- isSubmitting: boolean
- /** Ошибка последней catalog mutation. */
- error: string | null
- /** Передаёт валидный create/update input screen-владельцу. */
- onSubmit: (input: CreateProductInput | UpdateProductInput) => Promise
- /** Возвращает форму в create mode. */
- onCancel: () => void
-}
-
-/**
- * Атрибуты корневой form без собственного submit callback.
- */
-type RootAttrs = Omit, 'children' | 'onSubmit'>
-
-/**
- * Props admin product form.
- */
-export type ProductFormProps = RootAttrs & ProductFormParams
diff --git a/examples/demo-frontend/src/compositions/screens/product-detail/index.ts b/examples/demo-frontend/src/compositions/screens/product-detail/index.ts
deleted file mode 100644
index 9dbb2f3..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-detail/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { ProductDetailScreen } from './product-detail.screen'
-export type { ProductDetailScreenProps } from './types/product-detail-screen-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/product-detail/product-detail.screen.tsx b/examples/demo-frontend/src/compositions/screens/product-detail/product-detail.screen.tsx
deleted file mode 100644
index 57481f0..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-detail/product-detail.screen.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-'use client'
-
-import cl from 'clsx'
-import Image from 'next/image'
-import Link from 'next/link'
-
-import { PRODUCT_IMAGE_PLACEHOLDER, useProduct } from '@/domains/catalog'
-import { useCart } from '@/domains/cart'
-import { formatMoney } from '@/shared/lib/format-money'
-import { Button } from '@/ui/button'
-import { FeedbackPanel } from '@/ui/feedback-panel'
-
-import type { ProductDetailScreenProps } from './types/product-detail-screen-props.type'
-import styles from './styles/product-detail.module.css'
-
-/**
- * Детальная карточка выбранного catalog-продукта.
- *
- * Используется для:
- * - проверки dynamic route и detail query
- * - добавления продукта в application cart
- */
-export const ProductDetailScreen = (props: ProductDetailScreenProps) => {
- const { productId, className, ...rootAttrs } = props
- const productState = useProduct(productId)
- const cart = useCart()
-
- /**
- * Повторяет неуспешный product detail query.
- */
- const handleReload = (): void => {
- void productState.reload()
- }
-
- if (productState.isLoading) {
- return (
-
-
-
- )
- }
-
- if (productState.error !== null || productState.product === null) {
- const message = productState.error?.message ?? 'The product returned no usable data.'
-
- return (
-
-
-
-
- Back to catalog
-
-
-
- )
- }
-
- const product = productState.product
- const isSoldOut = product.stock === 0
- const stockLabel = isSoldOut ? 'Unavailable' : `${product.stock} units in deterministic stock`
-
- /**
- * Добавляет текущий detail-продукт в cart domain.
- */
- const handleAdd = (): void => {
- void cart.addProduct(product)
- }
-
- return (
-
-
- Back / catalog
-
-
-
-
- {
- event.currentTarget.srcset = ''
- event.currentTarget.src = PRODUCT_IMAGE_PLACEHOLDER
- }}
- />
- v{product.version}
-
-
-
-
Object / {product.categoryId.replace('category-', '')}
-
{product.name}
-
{product.description}
-
-
-
-
- Rating
- - {product.rating.toFixed(1)} / 5
-
-
-
- Stock
- - {stockLabel}
-
-
-
- Lock
- - Version {product.version}
-
-
-
-
- {formatMoney(product.priceCents, product.currency)}
-
-
-
-
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/product-detail/styles/product-detail.module.css b/examples/demo-frontend/src/compositions/screens/product-detail/styles/product-detail.module.css
deleted file mode 100644
index f60867f..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-detail/styles/product-detail.module.css
+++ /dev/null
@@ -1,149 +0,0 @@
-.root {
- min-height: calc(100vh - 82px);
- padding: clamp(28px, 5vw, 72px);
-}
-
-.back,
-.textLink {
- display: inline-flex;
- color: var(--color-blue);
- font-size: 0.7rem;
- font-weight: 760;
- letter-spacing: 0.09em;
- text-transform: uppercase;
-}
-
-.product {
- display: grid;
- grid-template-columns: minmax(0, 1.15fr) minmax(340px, 0.85fr);
- gap: clamp(32px, 7vw, 100px);
- margin-top: 34px;
-}
-
-.visual {
- position: relative;
- min-height: min(66vw, 720px);
- overflow: hidden;
- border-radius: 28px;
- background: var(--color-surface-strong);
-}
-
-.image {
- object-fit: cover;
-}
-
-.version {
- position: absolute;
- right: 18px;
- bottom: 18px;
- display: grid;
- place-items: center;
- min-width: 54px;
- height: 54px;
- color: var(--color-paper);
- border-radius: 50%;
- background: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.72rem;
-}
-
-.content {
- align-self: center;
- display: grid;
- gap: 28px;
-}
-
-.eyebrow {
- color: var(--color-blue);
- font-family: var(--font-mono);
- font-size: 0.68rem;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-.title {
- max-width: 9ch;
- font-family: var(--font-display);
- font-size: clamp(3.2rem, 7vw, 7rem);
- letter-spacing: -0.055em;
- line-height: 0.82;
-}
-
-.description {
- max-width: 48ch;
- color: var(--color-ink-muted);
- font-size: 1.05rem;
- line-height: 1.7;
-}
-
-.facts {
- display: grid;
- border-block: 1px solid var(--color-line);
-}
-
-.facts div {
- display: grid;
- grid-template-columns: 90px 1fr;
- gap: 16px;
- padding: 13px 0;
- border-bottom: 1px solid var(--color-line);
-}
-
-.facts div:last-child {
- border-bottom: 0;
-}
-
-.facts dt {
- color: var(--color-ink-faint);
- font-family: var(--font-mono);
- font-size: 0.66rem;
- letter-spacing: 0.07em;
- text-transform: uppercase;
-}
-
-.facts dd {
- font-size: 0.88rem;
-}
-
-.purchase {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 20px;
-}
-
-.purchase strong {
- font-family: var(--font-mono);
- font-size: clamp(1.35rem, 3vw, 2rem);
-}
-
-.outcome {
- display: grid;
- place-items: center;
-}
-
-.loading {
- min-height: 70vh;
- border-radius: 28px;
- background:
- linear-gradient(105deg, transparent 30%, rgb(255 255 255 / 55%) 45%, transparent 60%)
- 0 0 / 220% 100%,
- var(--color-surface-strong);
- animation: sweep 1.4s linear infinite;
-}
-
-@keyframes sweep {
- to {
- background-position: -220% 0;
- }
-}
-
-@media (max-width: 820px) {
- .product {
- grid-template-columns: 1fr;
- }
-
- .visual {
- min-height: 70vw;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/product-detail/types/product-detail-screen-props.type.ts b/examples/demo-frontend/src/compositions/screens/product-detail/types/product-detail-screen-props.type.ts
deleted file mode 100644
index eb05583..0000000
--- a/examples/demo-frontend/src/compositions/screens/product-detail/types/product-detail-screen-props.type.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры product detail screen.
- */
-export type ProductDetailScreenParams = {
- /** Product id, адаптированный route entry. */
- productId: string
-}
-
-/**
- * Атрибуты корневого main без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props product detail screen.
- */
-export type ProductDetailScreenProps = RootAttrs & ProductDetailScreenParams
diff --git a/examples/demo-frontend/src/compositions/screens/sign-in/index.ts b/examples/demo-frontend/src/compositions/screens/sign-in/index.ts
deleted file mode 100644
index 9fd245f..0000000
--- a/examples/demo-frontend/src/compositions/screens/sign-in/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { SignInScreen } from './sign-in.screen'
-export type { SignInScreenProps } from './types/sign-in-screen-props.type'
diff --git a/examples/demo-frontend/src/compositions/screens/sign-in/sign-in.screen.tsx b/examples/demo-frontend/src/compositions/screens/sign-in/sign-in.screen.tsx
deleted file mode 100644
index f7e9e6d..0000000
--- a/examples/demo-frontend/src/compositions/screens/sign-in/sign-in.screen.tsx
+++ /dev/null
@@ -1,205 +0,0 @@
-'use client'
-
-import type { ChangeEvent, FormEvent } from 'react'
-import { useState } from 'react'
-import cl from 'clsx'
-import Link from 'next/link'
-import { useRouter } from 'next/navigation'
-
-import { AUTH_DEMO_ACCOUNTS, useAuth } from '@/domains/auth'
-import type { AuthError, DemoAccount, SignInCredentials } from '@/domains/auth'
-import { Button } from '@/ui/button'
-import { FeedbackPanel } from '@/ui/feedback-panel'
-import { FormField } from '@/ui/form-field'
-
-import type { SignInScreenProps } from './types/sign-in-screen-props.type'
-import styles from './styles/sign-in.module.css'
-
-/**
- * Auth route с двумя воспроизводимыми fixture-ролями.
- *
- * Используется для:
- * - запуска JWT login/refresh lifecycle
- * - переключения customer и admin RBAC outcomes
- */
-export const SignInScreen = (props: SignInScreenProps) => {
- const { className, ...rootAttrs } = props
- const auth = useAuth()
- const router = useRouter()
- const [credentials, setCredentials] = useState({
- email: AUTH_DEMO_ACCOUNTS[0].email,
- password: AUTH_DEMO_ACCOUNTS[0].password
- })
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [error, setError] = useState(null)
-
- /**
- * Подставляет выбранную deterministic account в форму.
- */
- const handleAccountSelect = (account: DemoAccount): void => {
- setCredentials({ email: account.email, password: account.password })
- setError(null)
- }
-
- /**
- * Обновляет email login-формы.
- */
- const handleEmailChange = (event: ChangeEvent): void => {
- setCredentials((current) => ({ ...current, email: event.target.value }))
- }
-
- /**
- * Обновляет password login-формы.
- */
- const handlePasswordChange = (event: ChangeEvent): void => {
- setCredentials((current) => ({ ...current, password: event.target.value }))
- }
-
- /**
- * Выполняет auth-domain login и переводит пользователя в каталог.
- */
- const handleSubmit = async (event: FormEvent): Promise => {
- event.preventDefault()
- setIsSubmitting(true)
- setError(null)
-
- const result = await auth.signIn(credentials)
-
- setIsSubmitting(false)
-
- if (result.isSuccess) {
- router.push('/')
- return
- }
-
- setError(result.error)
- }
-
- /**
- * Повторяет recoverable проверку сохранённой session.
- */
- const handleSessionRetry = (): void => {
- void auth.refreshCurrentUser()
- }
-
- /**
- * Явно завершает сохранённую session после recoverable failure.
- */
- const handleSignOut = (): void => {
- void auth.signOut()
- }
-
- if (auth.status === 'checking') {
- return (
-
-
-
- )
- }
-
- if (auth.status === 'authenticated' && auth.user !== null) {
- return (
-
-
-
-
- Browse catalog
-
-
- Open orders
-
-
-
-
- )
- }
-
- if (auth.status === 'unavailable') {
- return (
-
-
-
-
-
-
-
-
- )
- }
-
- return (
-
-
- Auth / JWT transport
- Choose a boundary to cross.
-
- Both accounts use the same password, but their domain permissions produce different
- navigation, mutations and order visibility.
-
-
-
- {AUTH_DEMO_ACCOUNTS.map((account) => (
-
- ))}
-
-
-
-
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/screens/sign-in/styles/sign-in.module.css b/examples/demo-frontend/src/compositions/screens/sign-in/styles/sign-in.module.css
deleted file mode 100644
index 332f49e..0000000
--- a/examples/demo-frontend/src/compositions/screens/sign-in/styles/sign-in.module.css
+++ /dev/null
@@ -1,162 +0,0 @@
-.root {
- display: grid;
- grid-template-columns: minmax(0, 1fr) minmax(360px, 0.72fr);
- min-height: calc(100vh - 82px);
-}
-
-.intro,
-.formPanel {
- padding: clamp(44px, 8vw, 110px);
-}
-
-.intro {
- display: grid;
- align-content: center;
- gap: 30px;
- color: var(--color-paper);
- background: var(--color-ink);
-}
-
-.kicker,
-.sequence {
- font-family: var(--font-mono);
- font-size: 0.68rem;
- letter-spacing: 0.12em;
- text-transform: uppercase;
-}
-
-.kicker {
- color: var(--color-acid);
-}
-
-.title {
- max-width: 9ch;
- font-family: var(--font-display);
- font-size: clamp(3.2rem, 8vw, 7.5rem);
- letter-spacing: -0.055em;
- line-height: 0.82;
-}
-
-.description {
- max-width: 48ch;
- color: rgb(247 242 232 / 68%);
- font-size: 1rem;
- line-height: 1.7;
-}
-
-.accounts {
- display: grid;
- grid-template-columns: repeat(2, minmax(0, 1fr));
- gap: 12px;
-}
-
-.accountCard {
- display: grid;
- gap: 8px;
- padding: 18px;
- color: inherit;
- border: 1px solid rgb(255 255 255 / 18%);
- border-radius: 16px;
- background: rgb(255 255 255 / 5%);
- text-align: left;
- cursor: pointer;
- transition:
- border-color 160ms ease,
- background 160ms ease;
-}
-
-.accountCard:hover,
-.accountCard:focus-visible {
- border-color: var(--color-acid);
- outline: none;
- background: rgb(200 255 77 / 8%);
-}
-
-.accountCard span,
-.accountCard small {
- color: rgb(247 242 232 / 54%);
- font-size: 0.66rem;
- letter-spacing: 0.07em;
- text-transform: uppercase;
-}
-
-.accountCard strong {
- overflow: hidden;
- font-family: var(--font-mono);
- font-size: 0.75rem;
- text-overflow: ellipsis;
-}
-
-.formPanel {
- position: relative;
- display: grid;
- align-content: center;
- gap: 26px;
- background:
- radial-gradient(circle at 90% 10%, color-mix(in srgb, var(--color-orange) 22%, transparent), transparent 32%),
- var(--color-paper);
-}
-
-.sequence {
- color: var(--color-blue);
-}
-
-.formPanel h2 {
- font-family: var(--font-display);
- font-size: clamp(2rem, 5vw, 4rem);
- line-height: 0.9;
-}
-
-.form {
- display: grid;
- gap: 20px;
- max-width: 440px;
-}
-
-.error {
- padding: 12px 14px;
- color: var(--color-error);
- border-left: 4px solid var(--color-error);
- background: color-mix(in srgb, var(--color-error) 8%, transparent);
- font-size: 0.84rem;
- line-height: 1.45;
-}
-
-.outcome {
- place-items: center;
- padding: clamp(30px, 8vw, 120px);
-}
-
-.outcomeActions {
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- gap: 18px;
-}
-
-.primaryLink {
- padding: 14px 20px;
- color: var(--color-paper);
- border-radius: 999px;
- background: var(--color-ink);
- font-size: 0.72rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- text-transform: uppercase;
-}
-
-.textLink {
- color: var(--color-blue);
- font-size: 0.76rem;
- font-weight: 760;
-}
-
-@media (max-width: 880px) {
- .root {
- grid-template-columns: 1fr;
- }
-
- .accounts {
- grid-template-columns: 1fr;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/screens/sign-in/types/sign-in-screen-props.type.ts b/examples/demo-frontend/src/compositions/screens/sign-in/types/sign-in-screen-props.type.ts
deleted file mode 100644
index e27e363..0000000
--- a/examples/demo-frontend/src/compositions/screens/sign-in/types/sign-in-screen-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры sign-in screen.
- */
-export type SignInScreenParams = object
-
-/**
- * Атрибуты корневого main без внешнего содержимого.
- */
-type RootAttrs = Omit, 'children'>
-
-/**
- * Props sign-in screen.
- */
-export type SignInScreenProps = RootAttrs & SignInScreenParams
diff --git a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/demo-toolbar.widget.tsx b/examples/demo-frontend/src/compositions/widgets/demo-toolbar/demo-toolbar.widget.tsx
deleted file mode 100644
index bcf3ca6..0000000
--- a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/demo-toolbar.widget.tsx
+++ /dev/null
@@ -1,140 +0,0 @@
-'use client'
-
-import type { ChangeEvent } from 'react'
-import { useEffect, useEffectEvent, useRef } from 'react'
-import cl from 'clsx'
-
-import { useAuth } from '@/domains/auth'
-import { useCart } from '@/domains/cart'
-import { useDemoControl } from '@/domains/demo-control'
-import { Button } from '@/ui/button'
-
-import type { DemoToolbarWidgetProps } from './types/demo-toolbar-widget-props.type'
-import styles from './styles/demo-toolbar.module.css'
-
-/**
- * Управляет детерминированными сбоями и seed fixture-бэкенда.
- *
- * Используется для:
- * - проверки loading, empty, error и auth outcomes
- * - сброса данных между архитектурными демонстрациями
- */
-export const DemoToolbarWidget = (props: DemoToolbarWidgetProps) => {
- const { className, ...rootAttrs } = props
- const demo = useDemoControl()
- const auth = useAuth()
- const cart = useCart()
- const selectedOption = demo.scenarios.find((option) => option.value === demo.scenario)
- const healthLabel = demo.health === 'online' ? `API ${demo.apiVersion ?? ''}`.trim() : demo.health
- const canChangeRole = auth.user !== null
- const nextRole = auth.user?.role === 'admin' ? 'customer' : 'admin'
- const roleActionLabel = `Become ${nextRole}`
- const previousFixtureChangeRef = useRef(demo.fixtureChange)
- const synchronizeFixtureOwners = useEffectEvent((change: string) => {
- if (change.startsWith('data:')) {
- void cart.clearCart()
- }
-
- if (auth.sessionKey !== null) {
- void auth.refreshCurrentUser()
- }
- })
-
- useEffect(() => {
- if (previousFixtureChangeRef.current !== demo.fixtureChange) {
- previousFixtureChangeRef.current = demo.fixtureChange
- synchronizeFixtureOwners(demo.fixtureChange)
- }
- }, [demo.fixtureChange])
-
- /**
- * Применяет выбранный request-local сценарий.
- */
- const handleScenarioChange = (event: ChangeEvent): void => {
- const selectedScenario = demo.scenarios.find((option) => option.value === event.target.value)
-
- if (selectedScenario !== undefined) {
- void demo.selectScenario(selectedScenario.value)
- }
- }
-
- /**
- * Восстанавливает исходный малый seed.
- */
- const handleReset = async (): Promise => {
- await demo.resetData()
- }
-
- /**
- * Загружает большой deterministic dataset.
- */
- const handleLargeSeed = async (): Promise => {
- await demo.seedData('large')
- }
-
- /**
- * Переключает роль текущего fixture-пользователя и обновляет профиль.
- */
- const handleRoleChange = async (): Promise => {
- if (auth.user === null) {
- return
- }
-
- await demo.changeRole(auth.user.id, nextRole)
- }
-
- return (
-
- )
-}
diff --git a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/index.ts b/examples/demo-frontend/src/compositions/widgets/demo-toolbar/index.ts
deleted file mode 100644
index e746a91..0000000
--- a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { DemoToolbarWidget } from './demo-toolbar.widget'
-export type { DemoToolbarWidgetProps } from './types/demo-toolbar-widget-props.type'
diff --git a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/styles/demo-toolbar.module.css b/examples/demo-frontend/src/compositions/widgets/demo-toolbar/styles/demo-toolbar.module.css
deleted file mode 100644
index a78b56b..0000000
--- a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/styles/demo-toolbar.module.css
+++ /dev/null
@@ -1,159 +0,0 @@
-.root {
- position: fixed;
- right: 18px;
- bottom: 18px;
- z-index: 40;
- width: min(390px, calc(100vw - 36px));
-}
-
-.panel {
- overflow: hidden;
- color: #f7f2e8;
- border: 1px solid rgb(255 255 255 / 18%);
- border-radius: 18px;
- background: rgb(20 20 19 / 94%);
- box-shadow: 0 24px 70px rgb(0 0 0 / 28%);
- backdrop-filter: blur(18px);
-}
-
-.summary {
- display: grid;
- grid-template-columns: auto 1fr auto;
- align-items: center;
- gap: 10px;
- min-height: 48px;
- padding: 0 16px;
- font-size: 0.74rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- cursor: pointer;
- list-style: none;
-}
-
-.summary::-webkit-details-marker {
- display: none;
-}
-
-.pulse {
- width: 8px;
- height: 8px;
- border-radius: 50%;
- background: #ffac33;
- box-shadow: 0 0 0 5px rgb(255 172 51 / 12%);
-}
-
-.pulse[data-health='online'] {
- background: #c8ff4d;
- box-shadow: 0 0 0 5px rgb(200 255 77 / 12%);
-}
-
-.pulse[data-health='offline'] {
- background: #ff6b5f;
- box-shadow: 0 0 0 5px rgb(255 107 95 / 14%);
-}
-
-.current {
- max-width: 130px;
- overflow: hidden;
- color: #c8ff4d;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-
-.body {
- display: grid;
- gap: 16px;
- padding: 4px 16px 16px;
- border-top: 1px solid rgb(255 255 255 / 12%);
-}
-
-.intro {
- display: grid;
- grid-template-columns: 1fr auto;
- gap: 5px 12px;
- padding-top: 14px;
-}
-
-.eyebrow,
-.control span {
- color: rgb(247 242 232 / 56%);
- font-size: 0.64rem;
- font-weight: 760;
- letter-spacing: 0.11em;
- text-transform: uppercase;
-}
-
-.description {
- grid-column: 1 / -1;
- color: rgb(247 242 232 / 78%);
- font-size: 0.82rem;
- line-height: 1.45;
-}
-
-.health {
- color: #c8ff4d;
- font-family: var(--font-mono);
- font-size: 0.68rem;
- text-transform: uppercase;
-}
-
-.control {
- display: grid;
- gap: 7px;
-}
-
-.control select {
- width: 100%;
- min-height: 42px;
- padding: 0 12px;
- color: #f7f2e8;
- border: 1px solid rgb(255 255 255 / 20%);
- border-radius: 10px;
- background: #292927;
- font: inherit;
-}
-
-.actions {
- display: flex;
- flex-wrap: wrap;
- gap: 8px;
-}
-
-.actions button {
- color: #f7f2e8;
- border-color: rgb(255 255 255 / 20%);
- background: transparent;
-}
-
-.message,
-.error {
- padding: 10px 12px;
- border-radius: 10px;
- font-size: 0.78rem;
- line-height: 1.4;
-}
-
-.message {
- color: #dfffa0;
- background: rgb(200 255 77 / 10%);
-}
-
-.error {
- color: #ffd1cc;
- background: rgb(255 107 95 / 13%);
-}
-
-@media (max-width: 640px) {
- .root {
- right: 10px;
- bottom: 10px;
- width: calc(100vw - 20px);
- }
-
- .body {
- max-height: calc(100dvh - 78px);
- overflow-y: auto;
- overscroll-behavior: contain;
- }
-}
diff --git a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/types/demo-toolbar-widget-props.type.ts b/examples/demo-frontend/src/compositions/widgets/demo-toolbar/types/demo-toolbar-widget-props.type.ts
deleted file mode 100644
index 1dcd664..0000000
--- a/examples/demo-frontend/src/compositions/widgets/demo-toolbar/types/demo-toolbar-widget-props.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Параметры виджета архитектурных demo-сценариев.
- */
-export type DemoToolbarWidgetParams = object
-
-/**
- * Атрибуты корневого aside.
- */
-type RootAttrs = ComponentPropsWithoutRef<'aside'>
-
-/**
- * Props demo toolbar widget.
- */
-export type DemoToolbarWidgetProps = RootAttrs & DemoToolbarWidgetParams
diff --git a/examples/demo-frontend/src/domains/auth/auth.constants.ts b/examples/demo-frontend/src/domains/auth/auth.constants.ts
deleted file mode 100644
index 0ae4296..0000000
--- a/examples/demo-frontend/src/domains/auth/auth.constants.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import type { DemoAccount } from './types/demo-account.type'
-
-/**
- * Учётные записи Simple API для воспроизводимых auth/RBAC-сценариев.
- */
-export const AUTH_DEMO_ACCOUNTS: readonly DemoAccount[] = [
- {
- email: 'admin@demo.local',
- password: 'demo1234',
- role: 'admin',
- description: 'Catalog CRUD and every order'
- },
- {
- email: 'customer@demo.local',
- password: 'demo1234',
- role: 'customer',
- description: 'Checkout and own orders'
- }
-]
diff --git a/examples/demo-frontend/src/domains/auth/auth.context.ts b/examples/demo-frontend/src/domains/auth/auth.context.ts
deleted file mode 100644
index 177e973..0000000
--- a/examples/demo-frontend/src/domains/auth/auth.context.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createContext } from 'react'
-
-import type { AuthContextValue } from './types/auth-context.type'
-
-/**
- * Внутренний React context application-scoped auth API.
- */
-export const AuthContext = createContext(null)
diff --git a/examples/demo-frontend/src/domains/auth/auth.provider.tsx b/examples/demo-frontend/src/domains/auth/auth.provider.tsx
deleted file mode 100644
index 7496c73..0000000
--- a/examples/demo-frontend/src/domains/auth/auth.provider.tsx
+++ /dev/null
@@ -1,393 +0,0 @@
-'use client'
-
-import { startTransition, useEffect, useRef, useState } from 'react'
-
-import {
- clearSimpleAuthSessionScope,
- discardSimpleAuthSession,
- readSimpleAuthSession,
- replaceSimpleAuthSession,
- subscribeSimpleAuthSession
-} from '@/infra/simple-auth-session'
-import type { SimpleAuthSession } from '@/infra/simple-auth-session'
-import { simpleRestApi } from '@/infra/simple-rest-api'
-import type { Result } from '@/shared/types/result.type'
-
-import { AuthContext } from './auth.context'
-import { mapAuthError } from './errors/auth-error.mapper'
-import { mapAuthUser } from './mappers/auth-user.mapper'
-import type { AuthError } from './types/auth-error.type'
-import type { AuthContextValue, SignInCredentials } from './types/auth-context.type'
-import type { AuthProviderProps } from './types/auth-provider-props.type'
-import type { AuthUser } from './types/auth-user.type'
-
-const SESSION_STORAGE_ERROR: AuthError = {
- code: 'service-unavailable',
- message: 'Browser storage is unavailable. The session was not changed.'
-}
-const SESSION_CHANGED_ERROR: AuthError = {
- code: 'session-expired',
- message: 'The active session changed before the request completed.'
-}
-
-/**
- * Владеет пользовательской сессией на всём времени жизни приложения.
- *
- * Используется для:
- * - восстановления профиля из CAS-protected JWT session
- * - предоставления login/logout API всем composition-модулям
- */
-export const AuthProvider = (props: AuthProviderProps) => {
- const { children } = props
- const [status, setStatus] = useState('checking')
- const [user, setUser] = useState(null)
- const [sessionKey, setSessionKey] = useState(null)
- const [sessionError, setSessionError] = useState(null)
- const sessionKeyRef = useRef(null)
- const profileRequestIdRef = useRef(0)
-
- useEffect(() => {
- let isActive = true
-
- /**
- * Загружает authority-профиль только для всё ещё актуальной session scope.
- */
- const loadSessionUser = async (session: SimpleAuthSession): Promise => {
- const requestId = ++profileRequestIdRef.current
-
- try {
- const response = await simpleRestApi.users.simpleUsersMe()
- const currentResult = readSimpleAuthSession()
-
- if (
- !isActive ||
- requestId !== profileRequestIdRef.current
- ) {
- return
- }
-
- if (currentResult.status === 'unavailable') {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return
- }
-
- if (currentResult.session?.sessionId !== session.sessionId) {
- return
- }
-
- startTransition(() => {
- sessionKeyRef.current = session.sessionId
- setSessionKey(session.sessionId)
- setUser(mapAuthUser(response.data))
- setSessionError(null)
- setStatus('authenticated')
- })
- } catch (error) {
- const currentResult = readSimpleAuthSession()
-
- if (!isActive || requestId !== profileRequestIdRef.current) {
- return
- }
-
- if (currentResult.status === 'unavailable') {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return
- }
-
- const currentSession = currentResult.session
-
- if (currentSession === null) {
- sessionKeyRef.current = null
- setSessionKey(null)
- setUser(null)
- setSessionError(null)
- setStatus('guest')
- return
- }
-
- if (currentSession.sessionId === session.sessionId) {
- startTransition(() => {
- sessionKeyRef.current = session.sessionId
- setSessionKey(session.sessionId)
- setSessionError(mapAuthError(error))
- setStatus('unavailable')
- })
- }
- }
- }
-
- const unsubscribe = subscribeSimpleAuthSession((result) => {
- if (!isActive) {
- return
- }
-
- if (result.status === 'unavailable') {
- profileRequestIdRef.current += 1
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return
- }
-
- const session = result.session
-
- if (session === null) {
- profileRequestIdRef.current += 1
- sessionKeyRef.current = null
- setSessionKey(null)
- setUser(null)
- setSessionError(null)
- setStatus('guest')
- return
- }
-
- if (session.sessionId !== sessionKeyRef.current) {
- profileRequestIdRef.current += 1
- sessionKeyRef.current = session.sessionId
- setSessionKey(session.sessionId)
- setUser(null)
- setSessionError(null)
- setStatus('checking')
- void loadSessionUser(session)
- }
- })
- const initialResult = readSimpleAuthSession()
-
- if (initialResult.status === 'unavailable') {
- queueMicrotask(() => {
- if (isActive) {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- }
- })
- } else if (initialResult.session === null) {
- queueMicrotask(() => {
- if (isActive) {
- setStatus('guest')
- }
- })
- } else {
- const session = initialResult.session
-
- sessionKeyRef.current = session.sessionId
- void loadSessionUser(session)
- }
-
- return () => {
- isActive = false
- unsubscribe()
- }
- }, [])
-
- /**
- * Выполняет login и создаёт новую logical session scope.
- */
- const signIn = async (
- credentials: SignInCredentials
- ): Promise> => {
- try {
- const response = await simpleRestApi.auth.simpleAuthLogin(credentials)
- const nextUser = mapAuthUser(response.data.user)
- const session = await replaceSimpleAuthSession(response.data.tokens)
-
- if (session === null) {
- return { isSuccess: false, error: SESSION_STORAGE_ERROR }
- }
-
- startTransition(() => {
- profileRequestIdRef.current += 1
- sessionKeyRef.current = session.sessionId
- setSessionKey(session.sessionId)
- setUser(nextUser)
- setSessionError(null)
- setStatus('authenticated')
- })
-
- return { isSuccess: true, data: nextUser }
- } catch (error) {
- return { isSuccess: false, error: mapAuthError(error) }
- }
- }
-
- /**
- * Локально завершает session до best-effort backend logout.
- */
- const signOut = async (): Promise => {
- const currentResult = readSimpleAuthSession()
-
- if (currentResult.status === 'unavailable') {
- if (currentResult.observedValue === null) {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return
- }
-
- const isDiscarded = await discardSimpleAuthSession(currentResult.observedValue)
-
- if (!isDiscarded) {
- const latestResult = readSimpleAuthSession()
-
- if (latestResult.status === 'unavailable') {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- }
- return
- }
-
- profileRequestIdRef.current += 1
- sessionKeyRef.current = null
- setSessionKey(null)
- setUser(null)
- setSessionError(null)
- setStatus('guest')
- return
- }
-
- const session = currentResult.session
-
- if (session === null) {
- profileRequestIdRef.current += 1
- sessionKeyRef.current = null
- setSessionKey(null)
- setUser(null)
- setSessionError(null)
- setStatus('guest')
- return
- }
-
- const isCleared = await clearSimpleAuthSessionScope(session.sessionId)
-
- if (!isCleared) {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return
- }
-
- profileRequestIdRef.current += 1
- sessionKeyRef.current = null
- setSessionKey(null)
- setUser(null)
- setSessionError(null)
- setStatus('guest')
-
- try {
- await simpleRestApi.auth.simpleAuthLogout({
- refreshToken: session.tokens.refreshToken
- })
- } catch {
- // Remote revocation is best-effort after the local scope is already closed.
- }
- }
-
- /**
- * Обновляет authority-профиль или публикует recoverable session failure.
- */
- const refreshCurrentUser = async (): Promise> => {
- const expectedResult = readSimpleAuthSession()
-
- if (expectedResult.status === 'unavailable') {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return { isSuccess: false, error: SESSION_STORAGE_ERROR }
- }
-
- const expectedSession = expectedResult.session
-
- if (expectedSession === null) {
- profileRequestIdRef.current += 1
- sessionKeyRef.current = null
- setSessionKey(null)
- setUser(null)
- setSessionError(null)
- setStatus('guest')
- return { isSuccess: false, error: SESSION_CHANGED_ERROR }
- }
-
- const requestId = ++profileRequestIdRef.current
-
- try {
- const response = await simpleRestApi.users.simpleUsersMe()
- const nextUser = mapAuthUser(response.data)
- const currentResult = readSimpleAuthSession()
-
- if (currentResult.status === 'unavailable') {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return { isSuccess: false, error: SESSION_STORAGE_ERROR }
- }
-
- const currentSession = currentResult.session
-
- if (
- requestId !== profileRequestIdRef.current ||
- currentSession?.sessionId !== expectedSession.sessionId
- ) {
- return { isSuccess: false, error: SESSION_CHANGED_ERROR }
- }
-
- startTransition(() => {
- sessionKeyRef.current = expectedSession.sessionId
- setSessionKey(expectedSession.sessionId)
- setUser(nextUser)
- setSessionError(null)
- setStatus('authenticated')
- })
-
- return { isSuccess: true, data: nextUser }
- } catch (error) {
- const authError = mapAuthError(error)
- const currentResult = readSimpleAuthSession()
-
- if (currentResult.status === 'unavailable') {
- setSessionError(SESSION_STORAGE_ERROR)
- setStatus('unavailable')
- return { isSuccess: false, error: SESSION_STORAGE_ERROR }
- }
-
- const currentSession = currentResult.session
-
- if (
- requestId !== profileRequestIdRef.current ||
- currentSession?.sessionId !== expectedSession.sessionId
- ) {
- return { isSuccess: false, error: SESSION_CHANGED_ERROR }
- }
-
- startTransition(() => {
- sessionKeyRef.current = expectedSession.sessionId
- setSessionKey(expectedSession.sessionId)
- setSessionError(authError)
- setStatus('unavailable')
- })
-
- return { isSuccess: false, error: authError }
- }
- }
-
- /**
- * Сверяет captured session scope с domain state и persisted authority.
- */
- const isCurrentSession = (expectedSessionKey: string): boolean => {
- const currentResult = readSimpleAuthSession()
- const persistedSession = currentResult.status === 'ready' ? currentResult.session : null
-
- return (
- sessionKeyRef.current === expectedSessionKey &&
- persistedSession?.sessionId === expectedSessionKey
- )
- }
-
- const value: AuthContextValue = {
- status,
- user,
- sessionKey,
- sessionError,
- isCurrentSession,
- signIn,
- signOut,
- refreshCurrentUser
- }
-
- return {children}
-}
diff --git a/examples/demo-frontend/src/domains/auth/errors/auth-error.mapper.ts b/examples/demo-frontend/src/domains/auth/errors/auth-error.mapper.ts
deleted file mode 100644
index 7bba417..0000000
--- a/examples/demo-frontend/src/domains/auth/errors/auth-error.mapper.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { readSimpleApiFailure } from '@/infra/simple-rest-api'
-
-import type { AuthError } from '../types/auth-error.type'
-
-/**
- * Переводит technical failure в ожидаемую ошибку auth-домена.
- */
-export const mapAuthError = (error: unknown): AuthError => {
- const failure = readSimpleApiFailure(error)
-
- if (failure.code === 'INVALID_CREDENTIALS') {
- return { code: 'invalid-credentials', message: 'Email or password is incorrect.' }
- }
-
- if (failure.status === 401) {
- return { code: 'session-expired', message: 'Your session expired. Sign in again.' }
- }
-
- if (failure.status === 403) {
- return { code: 'access-denied', message: 'This account cannot perform that action.' }
- }
-
- if (failure.status === 0) {
- return { code: 'service-unavailable', message: failure.message }
- }
-
- return { code: 'unknown', message: 'Authentication could not be completed.' }
-}
diff --git a/examples/demo-frontend/src/domains/auth/hooks/use-auth.hook.ts b/examples/demo-frontend/src/domains/auth/hooks/use-auth.hook.ts
deleted file mode 100644
index ea14c37..0000000
--- a/examples/demo-frontend/src/domains/auth/hooks/use-auth.hook.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client'
-
-import { useContext } from 'react'
-
-import { AuthContext } from '../auth.context'
-import type { AuthContextValue } from '../types/auth-context.type'
-
-/**
- * Возвращает application-scoped auth API текущего пользователя.
- */
-export const useAuth = (): AuthContextValue => {
- const context = useContext(AuthContext)
-
- if (context === null) {
- throw new Error('useAuth must be used inside AuthProvider')
- }
-
- return context
-}
diff --git a/examples/demo-frontend/src/domains/auth/index.ts b/examples/demo-frontend/src/domains/auth/index.ts
deleted file mode 100644
index 900f0ad..0000000
--- a/examples/demo-frontend/src/domains/auth/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client'
-
-export { AuthProvider } from './auth.provider'
-export { AUTH_DEMO_ACCOUNTS } from './auth.constants'
-export { useAuth } from './hooks/use-auth.hook'
-export type { AuthError, AuthErrorCode } from './types/auth-error.type'
-export type {
- AuthContextValue,
- AuthStatus,
- SignInCredentials
-} from './types/auth-context.type'
-export type { AuthProviderProps } from './types/auth-provider-props.type'
-export type { AuthUser, AuthUserRole } from './types/auth-user.type'
-export type { DemoAccount } from './types/demo-account.type'
diff --git a/examples/demo-frontend/src/domains/auth/mappers/auth-user.mapper.ts b/examples/demo-frontend/src/domains/auth/mappers/auth-user.mapper.ts
deleted file mode 100644
index 44a4969..0000000
--- a/examples/demo-frontend/src/domains/auth/mappers/auth-user.mapper.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { SimpleUserDto } from '@/infra/simple-rest-api'
-
-import type { AuthUser } from '../types/auth-user.type'
-
-/**
- * Переводит внешний user DTO в модель auth-домена.
- */
-export const mapAuthUser = (user: SimpleUserDto): AuthUser => {
- return {
- id: user.id,
- email: user.email,
- name: user.name,
- role: user.role,
- avatarUrl: typeof user.avatarUrl === 'string' ? user.avatarUrl : null
- }
-}
diff --git a/examples/demo-frontend/src/domains/auth/types/auth-context.type.ts b/examples/demo-frontend/src/domains/auth/types/auth-context.type.ts
deleted file mode 100644
index 6ac2174..0000000
--- a/examples/demo-frontend/src/domains/auth/types/auth-context.type.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { Result } from '@/shared/types/result.type'
-
-import type { AuthError } from './auth-error.type'
-import type { AuthUser } from './auth-user.type'
-
-/**
- * Состояние восстановления пользовательской сессии.
- */
-export type AuthStatus = 'checking' | 'guest' | 'authenticated' | 'unavailable'
-
-/**
- * Данные формы входа.
- */
-export type SignInCredentials = {
- /** Email demo-пользователя. */
- email: string
- /** Пароль demo-пользователя. */
- password: string
-}
-
-/**
- * Публичный API auth-домена в application scope.
- */
-export type AuthContextValue = {
- /** Текущее состояние восстановления или авторизации. */
- status: AuthStatus
- /** Авторизованный пользователь, если сессия активна. */
- user: AuthUser | null
- /** Непрозрачный ключ логической сессии для изоляции protected state. */
- sessionKey: string | null
- /** Recoverable ошибка проверки сохранённой сессии. */
- sessionError: AuthError | null
- /** Проверяет актуальность captured logical session для async workflow. */
- isCurrentSession: (sessionKey: string) => boolean
- /** Выполняет login и сохраняет технические JWT-данные. */
- signIn: (credentials: SignInCredentials) => Promise>
- /** Завершает локальную и backend-сессию. */
- signOut: () => Promise
- /** Повторно загружает профиль после внешнего изменения роли. */
- refreshCurrentUser: () => Promise>
-}
diff --git a/examples/demo-frontend/src/domains/auth/types/auth-error.type.ts b/examples/demo-frontend/src/domains/auth/types/auth-error.type.ts
deleted file mode 100644
index a508bc4..0000000
--- a/examples/demo-frontend/src/domains/auth/types/auth-error.type.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/**
- * Стабильные коды ожидаемых ошибок auth-сценариев.
- */
-export type AuthErrorCode =
- | 'invalid-credentials'
- | 'session-expired'
- | 'access-denied'
- | 'service-unavailable'
- | 'unknown'
-
-/**
- * Безопасная ошибка auth-домена.
- */
-export type AuthError = {
- /** Код для UI-ветвления. */
- code: AuthErrorCode
- /** Сообщение без transport payload. */
- message: string
-}
diff --git a/examples/demo-frontend/src/domains/auth/types/auth-provider-props.type.ts b/examples/demo-frontend/src/domains/auth/types/auth-provider-props.type.ts
deleted file mode 100644
index 8ae960d..0000000
--- a/examples/demo-frontend/src/domains/auth/types/auth-provider-props.type.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { ReactNode } from 'react'
-
-/**
- * Props application-scoped auth provider.
- */
-export type AuthProviderProps = {
- /** Поддерево, использующее auth API. */
- children: ReactNode
-}
diff --git a/examples/demo-frontend/src/domains/auth/types/auth-user.type.ts b/examples/demo-frontend/src/domains/auth/types/auth-user.type.ts
deleted file mode 100644
index 695d4f0..0000000
--- a/examples/demo-frontend/src/domains/auth/types/auth-user.type.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Роль пользователя в Simple storefront.
- */
-export type AuthUserRole = 'admin' | 'customer'
-
-/**
- * Авторизованный пользователь без transport-полей.
- */
-export type AuthUser = {
- /** Стабильный идентификатор пользователя. */
- id: string
- /** Email для входа и отображения профиля. */
- email: string
- /** Отображаемое имя. */
- name: string
- /** Текущая роль доступа. */
- role: AuthUserRole
- /** Необязательный URL аватара. */
- avatarUrl: string | null
-}
diff --git a/examples/demo-frontend/src/domains/auth/types/demo-account.type.ts b/examples/demo-frontend/src/domains/auth/types/demo-account.type.ts
deleted file mode 100644
index b7a0a4c..0000000
--- a/examples/demo-frontend/src/domains/auth/types/demo-account.type.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { AuthUserRole } from './auth-user.type'
-
-/**
- * Детерминированная учётная запись fixture-бэкенда.
- */
-export type DemoAccount = {
- /** Email для формы входа. */
- email: string
- /** Общий fixture-пароль. */
- password: string
- /** Роль, которую демонстрирует учётная запись. */
- role: AuthUserRole
- /** Краткое описание доступных сценариев. */
- description: string
-}
diff --git a/examples/demo-frontend/src/domains/cart/cart.context.ts b/examples/demo-frontend/src/domains/cart/cart.context.ts
deleted file mode 100644
index 5f9b337..0000000
--- a/examples/demo-frontend/src/domains/cart/cart.context.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { createContext } from 'react'
-
-import type { CartContextValue } from './types/cart.type'
-
-/**
- * Внутренний React context application-scoped корзины.
- */
-export const CartContext = createContext(null)
diff --git a/examples/demo-frontend/src/domains/cart/cart.logic.ts b/examples/demo-frontend/src/domains/cart/cart.logic.ts
deleted file mode 100644
index 0c35db0..0000000
--- a/examples/demo-frontend/src/domains/cart/cart.logic.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-import type { Product } from '@/domains/catalog'
-
-import type { CartLine, CartReconciliation, CartTotals } from './types/cart.type'
-
-const MAX_CART_LINE_QUANTITY = 20
-
-/**
- * Возвращает единый cart/order limit для конкретного продукта.
- */
-export const getCartProductQuantityLimit = (product: Product): number => {
- return Math.min(product.stock, MAX_CART_LINE_QUANTITY)
-}
-
-/**
- * Ограничивает cart quantity текущим stock продукта.
- */
-const clampQuantity = (product: Product, quantity: number): number => {
- if (!Number.isFinite(quantity)) {
- return 0
- }
-
- return Math.max(0, Math.min(Math.floor(quantity), getCartProductQuantityLimit(product)))
-}
-
-/**
- * Добавляет продукт или увеличивает существующую строку корзины.
- */
-export const addCartProduct = (lines: CartLine[], product: Product): CartLine[] => {
- const existingLine = lines.find((line) => line.product.id === product.id)
-
- if (existingLine === undefined) {
- const quantity = clampQuantity(product, 1)
-
- return quantity === 0 ? lines : [...lines, { product, quantity }]
- }
-
- return lines.map((line) => {
- if (line.product.id !== product.id) {
- return line
- }
-
- return {
- product,
- quantity: clampQuantity(product, line.quantity + 1)
- }
- })
-}
-
-/**
- * Устанавливает количество строки и удаляет её при нулевом результате.
- */
-export const setCartProductQuantity = (
- lines: CartLine[],
- productId: string,
- quantity: number
-): CartLine[] => {
- return lines.flatMap((line) => {
- if (line.product.id !== productId) {
- return [line]
- }
-
- const nextQuantity = clampQuantity(line.product, quantity)
-
- return nextQuantity === 0 ? [] : [{ ...line, quantity: nextQuantity }]
- })
-}
-
-/**
- * Удаляет одну продуктовую строку.
- */
-export const removeCartProduct = (lines: CartLine[], productId: string): CartLine[] => {
- return lines.filter((line) => line.product.id !== productId)
-}
-
-/**
- * Заменяет product snapshots authority-значениями перед checkout.
- */
-export const reconcileCartProducts = (
- lines: CartLine[],
- products: Product[]
-): CartReconciliation => {
- const productsById = new Map(products.map((product) => [product.id, product]))
- let hasChanges = false
- const nextLines = lines
- .map((line) => {
- const currentProduct = productsById.get(line.product.id)
-
- if (currentProduct === undefined) {
- return line
- }
-
- const hasProductChanged =
- currentProduct.version !== line.product.version ||
- currentProduct.priceCents !== line.product.priceCents ||
- currentProduct.currency !== line.product.currency ||
- currentProduct.stock !== line.product.stock
-
- if (hasProductChanged) {
- hasChanges = true
- }
-
- return {
- product: currentProduct,
- quantity: clampQuantity(currentProduct, line.quantity)
- }
- })
- .filter((line) => line.quantity > 0)
-
- if (nextLines.length !== lines.length) {
- hasChanges = true
- }
-
- return { lines: nextLines, hasChanges }
-}
-
-/**
- * Рассчитывает item count, subtotal и допустимость checkout.
- */
-export const calculateCartTotals = (lines: CartLine[]): CartTotals => {
- const itemCount = lines.reduce((sum, line) => sum + line.quantity, 0)
- const subtotalCents = lines.reduce(
- (sum, line) => sum + line.product.priceCents * line.quantity,
- 0
- )
- const firstCurrency = lines[0]?.product.currency ?? null
- const hasSingleCurrency = lines.every((line) => line.product.currency === firstCurrency)
-
- return {
- itemCount,
- subtotalCents,
- currency: hasSingleCurrency ? firstCurrency : null
- }
-}
diff --git a/examples/demo-frontend/src/domains/cart/cart.persistence.ts b/examples/demo-frontend/src/domains/cart/cart.persistence.ts
deleted file mode 100644
index 1fbdfa7..0000000
--- a/examples/demo-frontend/src/domains/cart/cart.persistence.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import { hasOwn, isArrayOf, isNumber, isOneOf, isRecord, isString } from '@/shared/lib/value-predicates'
-import {
- readJsonStorageResult,
- withBrowserStorageLock,
- writeJsonStorage
-} from '@/infra/browser-storage'
-
-import { isSupportedProductImageUrl } from '@/domains/catalog'
-import type { Product } from '@/domains/catalog'
-
-import { getCartProductQuantityLimit } from './cart.logic'
-import type { CartLine } from './types/cart.type'
-
-const CURRENCIES = ['USD', 'EUR'] as const
-const CART_STORAGE_KEY = 'demo-frontend:cart'
-const CART_STORAGE_LOCK = 'demo-frontend:cart-lock'
-
-/**
- * Создаёт revision, сохраняющую порядок обычных updates и forced recovery.
- */
-const createCartRevision = (currentRevision = 0): number => {
- return Math.max(currentRevision + 1, Date.now() * 1000)
-}
-
-/**
- * Persisted cart snapshot с cross-tab monotonic revision.
- */
-export type PersistedCartSnapshot = {
- /** Монотонная revision, увеличиваемая под storage lock. */
- revision: number
- /** Валидированные product snapshots и quantities. */
- lines: CartLine[]
-}
-
-/**
- * Результат чтения persisted cart repository.
- */
-export type PersistedCartReadResult =
- | {
- /** Repository доступен. */
- status: 'ready'
- /** Последний snapshot либо null до инициализации. */
- snapshot: PersistedCartSnapshot | null
- /** Требуется ли commit мигрированного legacy payload. */
- shouldCommit: boolean
- }
- | {
- /** Browser storage временно недоступен. */
- status: 'unavailable'
- }
-
-/**
- * Результат atomic cart transition.
- */
-export type PersistedCartUpdateResult =
- | {
- /** Snapshot записан с новой revision. */
- status: 'updated'
- /** Authority snapshot после transition. */
- snapshot: PersistedCartSnapshot
- }
-
- | {
- /** Transform не изменил authority snapshot. */
- status: 'unchanged'
- /** Текущий authority snapshot. */
- snapshot: PersistedCartSnapshot
- }
- | {
- /** Expected revision больше не актуальна. */
- status: 'stale'
- }
- | {
- /** Repository transition не удалось подтвердить. */
- status: 'unavailable'
- }
-
-/**
- * Результат проверки persisted cart revision.
- */
-export type PersistedCartRevisionStatus = 'current' | 'stale' | 'unavailable'
-
-/**
- * Проверяет persisted-снимок продукта перед восстановлением cart state.
- */
-const isStoredProduct = (value: unknown): value is Product => {
- return (
- isRecord(value) &&
- hasOwn(value, 'id') && isString(value.id) &&
- hasOwn(value, 'name') && isString(value.name) &&
- hasOwn(value, 'slug') && isString(value.slug) &&
- hasOwn(value, 'description') && isString(value.description) &&
- hasOwn(value, 'priceCents') && isNumber(value.priceCents) &&
- hasOwn(value, 'currency') && isOneOf(value.currency, CURRENCIES) &&
- hasOwn(value, 'categoryId') && isString(value.categoryId) &&
- hasOwn(value, 'stock') && isNumber(value.stock) &&
- hasOwn(value, 'rating') && isNumber(value.rating) &&
- hasOwn(value, 'imageUrl') && isString(value.imageUrl) && isSupportedProductImageUrl(value.imageUrl) &&
- hasOwn(value, 'createdAt') && isString(value.createdAt) &&
- hasOwn(value, 'version') && isNumber(value.version)
- )
-}
-
-/**
- * Проверяет persisted-строку корзины.
- */
-const isStoredCartLine = (value: unknown): value is CartLine => {
- return (
- isRecord(value) &&
- hasOwn(value, 'product') && isStoredProduct(value.product) &&
- hasOwn(value, 'quantity') && isNumber(value.quantity) &&
- Number.isInteger(value.quantity) &&
- value.quantity > 0 &&
- value.quantity <= getCartProductQuantityLimit(value.product)
- )
-}
-
-/**
- * Восстанавливает только полностью валидный persisted cart snapshot.
- */
-export const hydrateCartLines = (value: unknown): CartLine[] => {
- return isArrayOf(value, isStoredCartLine) ? value : []
-}
-
-/**
- * Проверяет persisted snapshot envelope после чтения storage.
- */
-const isPersistedCartSnapshot = (value: unknown): value is PersistedCartSnapshot => {
- return (
- isRecord(value) &&
- hasOwn(value, 'revision') &&
- isNumber(value.revision) &&
- Number.isInteger(value.revision) &&
- value.revision >= 1 &&
- hasOwn(value, 'lines') &&
- isArrayOf(value.lines, isStoredCartLine)
- )
-}
-
-/**
- * Читает snapshot и мигрирует legacy lines-array в memory representation.
- */
-export const readPersistedCartSnapshot = (): PersistedCartReadResult => {
- const result = readJsonStorageResult(CART_STORAGE_KEY)
-
- if (result.status === 'unavailable' || result.status === 'invalid') {
- return { status: 'unavailable' }
- }
-
- if (result.status === 'missing') {
- return { status: 'ready', snapshot: null, shouldCommit: true }
- }
-
- if (isPersistedCartSnapshot(result.value)) {
- return { status: 'ready', snapshot: result.value, shouldCommit: false }
- }
-
- return {
- status: 'ready',
- snapshot: {
- revision: createCartRevision(),
- lines: hydrateCartLines(result.value)
- },
- shouldCommit: true
- }
-}
-
-/**
- * Создаёт или мигрирует persisted cart snapshot под cross-tab lock.
- */
-export const initializePersistedCartSnapshot = async (): Promise => {
- try {
- return await withBrowserStorageLock(CART_STORAGE_LOCK, () => {
- const result = readPersistedCartSnapshot()
-
- if (result.status === 'unavailable') {
- return null
- }
-
- const snapshot = result.snapshot ?? { revision: createCartRevision(), lines: [] }
-
- if (result.shouldCommit && !writeJsonStorage(CART_STORAGE_KEY, snapshot)) {
- return null
- }
-
- return snapshot
- })
- } catch {
- return null
- }
-}
-
-/**
- * Перезаписывает unreadable cart безопасным пустым snapshot по явному действию пользователя.
- */
-export const resetPersistedCartSnapshot = async (
- minimumRevision = 0
-): Promise => {
- try {
- return await withBrowserStorageLock(CART_STORAGE_LOCK, () => {
- const snapshot: PersistedCartSnapshot = {
- revision: createCartRevision(minimumRevision),
- lines: []
- }
-
- return writeJsonStorage(CART_STORAGE_KEY, snapshot) ? snapshot : null
- })
- } catch {
- return null
- }
-}
-
-/**
- * Применяет transform к последнему persisted snapshot под cross-tab lock.
- */
-export const updatePersistedCartSnapshot = async (
- transform: (lines: CartLine[]) => CartLine[] | null,
- expectedRevision?: number
-): Promise => {
- try {
- return await withBrowserStorageLock(CART_STORAGE_LOCK, () => {
- const result = readPersistedCartSnapshot()
-
- if (result.status === 'unavailable') {
- return { status: 'unavailable' }
- }
-
- const currentSnapshot = result.snapshot ?? {
- revision: createCartRevision(),
- lines: []
- }
-
- if (
- expectedRevision !== undefined &&
- currentSnapshot.revision !== expectedRevision
- ) {
- return { status: 'stale' }
- }
-
- const nextLines = transform(currentSnapshot.lines)
-
- if (nextLines === null || nextLines === currentSnapshot.lines) {
- return { status: 'unchanged', snapshot: currentSnapshot }
- }
-
- const nextSnapshot: PersistedCartSnapshot = {
- revision: createCartRevision(currentSnapshot.revision),
- lines: nextLines
- }
-
- if (!writeJsonStorage(CART_STORAGE_KEY, nextSnapshot)) {
- return { status: 'unavailable' }
- }
-
- return { status: 'updated', snapshot: nextSnapshot }
- })
- } catch {
- return { status: 'unavailable' }
- }
-}
-
-/**
- * Сверяет expected revision с persisted authority под тем же lock.
- */
-export const isPersistedCartRevisionCurrent = async (
- expectedRevision: number
-): Promise => {
- try {
- return await withBrowserStorageLock(CART_STORAGE_LOCK, () => {
- const result = readPersistedCartSnapshot()
-
- if (result.status === 'unavailable') {
- return 'unavailable'
- }
-
- return result.snapshot?.revision === expectedRevision ? 'current' : 'stale'
- })
- } catch {
- return 'unavailable'
- }
-}
-
-/**
- * Подписывает cart owner на committed snapshots других вкладок.
- */
-export const subscribePersistedCartSnapshot = (
- listener: (result: PersistedCartReadResult) => void
-): (() => void) => {
- /**
- * Читает committed snapshot после cross-tab storage event.
- */
- const handleStorage = (event: StorageEvent): void => {
- if (event.key === CART_STORAGE_KEY) {
- listener(readPersistedCartSnapshot())
- }
- }
-
- window.addEventListener('storage', handleStorage)
-
- return () => window.removeEventListener('storage', handleStorage)
-}
diff --git a/examples/demo-frontend/src/domains/cart/cart.provider.tsx b/examples/demo-frontend/src/domains/cart/cart.provider.tsx
deleted file mode 100644
index d5e75b1..0000000
--- a/examples/demo-frontend/src/domains/cart/cart.provider.tsx
+++ /dev/null
@@ -1,225 +0,0 @@
-'use client'
-
-import { useEffect, useRef, useState } from 'react'
-
-import { CartContext } from './cart.context'
-import {
- addCartProduct,
- calculateCartTotals,
- reconcileCartProducts,
- removeCartProduct,
- setCartProductQuantity
-} from './cart.logic'
-import {
- initializePersistedCartSnapshot,
- isPersistedCartRevisionCurrent,
- resetPersistedCartSnapshot,
- subscribePersistedCartSnapshot,
- updatePersistedCartSnapshot
-} from './cart.persistence'
-import type { PersistedCartSnapshot } from './cart.persistence'
-import type { CartProviderProps } from './types/cart-provider-props.type'
-import type { CartContextValue, CartLine, CartReconciliationStatus } from './types/cart.type'
-import type { Product } from '@/domains/catalog'
-
-const CART_STORAGE_ERROR = {
- code: 'storage-unavailable',
- message: 'Cart storage is unavailable. Changes are paused to prevent cross-tab data loss.'
-} as const
-
-/**
- * Владеет корзиной в application scope и сохраняет её между перезагрузками.
- *
- * Используется для:
- * - координации каталога и checkout composition
- * - восстановления валидного cart snapshot из browser storage
- */
-export const CartProvider = (props: CartProviderProps) => {
- const { children } = props
- const [cartState, setCartState] = useState({
- lines: [] as CartLine[],
- revision: 0
- })
- const cartStateRef = useRef(cartState)
- const [isHydrated, setIsHydrated] = useState(false)
- const [error, setError] = useState(null)
- const lines = cartState.lines
-
- /**
- * Публикует только текущий или более новый persisted cart snapshot.
- */
- const publishSnapshot = (snapshot: PersistedCartSnapshot): void => {
- if (snapshot.revision < cartStateRef.current.revision) {
- return
- }
-
- const nextState = {
- lines: snapshot.lines,
- revision: snapshot.revision
- }
-
- cartStateRef.current = nextState
- setCartState(nextState)
- }
-
- useEffect(() => {
- let isActive = true
-
- void initializePersistedCartSnapshot().then((snapshot) => {
- if (isActive) {
- if (snapshot !== null) {
- publishSnapshot(snapshot)
- setError(null)
- } else {
- setError(CART_STORAGE_ERROR)
- }
-
- setIsHydrated(true)
- }
- })
-
- const unsubscribe = subscribePersistedCartSnapshot((result) => {
- if (isActive && result.status === 'ready' && result.snapshot !== null) {
- publishSnapshot(result.snapshot)
- setError(null)
- } else if (isActive && result.status === 'unavailable') {
- setError(CART_STORAGE_ERROR)
- }
- })
-
- return () => {
- isActive = false
- unsubscribe()
- }
- }, [])
-
- /**
- * Применяет cart transform к последнему cross-tab authority snapshot.
- */
- const updateLines = async (
- transform: (currentLines: CartLine[]) => CartLine[]
- ): Promise => {
- const result = await updatePersistedCartSnapshot(transform)
-
- if (result.status === 'updated' || result.status === 'unchanged') {
- publishSnapshot(result.snapshot)
- setError(null)
- } else if (result.status === 'unavailable') {
- setError(CART_STORAGE_ERROR)
- }
- }
-
- /**
- * Добавляет одну доступную единицу продукта.
- */
- const addProduct = async (product: Product): Promise => {
- await updateLines((currentLines) => addCartProduct(currentLines, product))
- }
-
- /**
- * Изменяет количество выбранного продукта.
- */
- const setQuantity = async (productId: string, quantity: number): Promise => {
- await updateLines((currentLines) => {
- return setCartProductQuantity(currentLines, productId, quantity)
- })
- }
-
- /**
- * Удаляет продукт из корзины.
- */
- const removeProduct = async (productId: string): Promise => {
- await updateLines((currentLines) => removeCartProduct(currentLines, productId))
- }
-
- /**
- * Обновляет stale snapshots перед checkout и сообщает о расхождении.
- */
- const reconcileProducts = (
- products: Product[],
- expectedRevision: number
- ): Promise => {
- return updatePersistedCartSnapshot((currentLines) => {
- const reconciliation = reconcileCartProducts(currentLines, products)
-
- return reconciliation.hasChanges ? reconciliation.lines : null
- }, expectedRevision).then((result) => {
- if (result.status === 'unavailable') {
- setError(CART_STORAGE_ERROR)
- return 'unavailable'
- }
-
- if (result.status === 'stale') {
- return 'stale'
- }
-
- publishSnapshot(result.snapshot)
- setError(null)
-
- return result.status === 'updated' ? 'updated' : 'unchanged'
- })
- }
-
- /**
- * Проверяет revision перед необратимым продолжением async checkout.
- */
- const isCurrentRevision: CartContextValue['isCurrentRevision'] = (revision) => {
- return isPersistedCartRevisionCurrent(revision).then((status) => {
- if (status === 'unavailable') {
- setError(CART_STORAGE_ERROR)
- }
-
- return status
- })
- }
-
- /**
- * Завершает checkout очисткой всех строк.
- */
- const clearCart: CartContextValue['clearCart'] = async (expectedRevision) => {
- const result = await updatePersistedCartSnapshot(() => [], expectedRevision)
-
- if (result.status === 'unavailable') {
- if (expectedRevision === undefined) {
- const recoveredSnapshot = await resetPersistedCartSnapshot(
- cartStateRef.current.revision
- )
-
- if (recoveredSnapshot !== null) {
- publishSnapshot(recoveredSnapshot)
- setError(null)
- return 'cleared'
- }
- }
-
- setError(CART_STORAGE_ERROR)
- return 'unavailable'
- }
-
- if (result.status === 'stale') {
- return 'stale'
- }
-
- publishSnapshot(result.snapshot)
- setError(null)
-
- return 'cleared'
- }
-
- const totals = calculateCartTotals(lines)
- const value: CartContextValue = {
- ...totals,
- lines,
- isHydrated,
- revision: cartState.revision,
- error,
- addProduct,
- setQuantity,
- removeProduct,
- reconcileProducts,
- isCurrentRevision,
- clearCart
- }
-
- return {children}
-}
diff --git a/examples/demo-frontend/src/domains/cart/hooks/use-cart.hook.ts b/examples/demo-frontend/src/domains/cart/hooks/use-cart.hook.ts
deleted file mode 100644
index 7710220..0000000
--- a/examples/demo-frontend/src/domains/cart/hooks/use-cart.hook.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-'use client'
-
-import { useContext } from 'react'
-
-import { CartContext } from '../cart.context'
-import type { CartContextValue } from '../types/cart.type'
-
-/**
- * Возвращает application-scoped API корзины.
- */
-export const useCart = (): CartContextValue => {
- const context = useContext(CartContext)
-
- if (context === null) {
- throw new Error('useCart must be used inside CartProvider')
- }
-
- return context
-}
diff --git a/examples/demo-frontend/src/domains/cart/index.ts b/examples/demo-frontend/src/domains/cart/index.ts
deleted file mode 100644
index 4b8eeed..0000000
--- a/examples/demo-frontend/src/domains/cart/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-'use client'
-
-export { CartProvider } from './cart.provider'
-export { getCartProductQuantityLimit } from './cart.logic'
-export { useCart } from './hooks/use-cart.hook'
-export type { CartProviderProps } from './types/cart-provider-props.type'
-export type {
- CartContextValue,
- CartClearStatus,
- CartError,
- CartLine,
- CartReconciliation,
- CartRevisionStatus,
- CartTotals
-} from './types/cart.type'
diff --git a/examples/demo-frontend/src/domains/cart/tests/cart.logic.test.ts b/examples/demo-frontend/src/domains/cart/tests/cart.logic.test.ts
deleted file mode 100644
index 1c7255b..0000000
--- a/examples/demo-frontend/src/domains/cart/tests/cart.logic.test.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import type { Product } from '@/domains/catalog'
-
-import {
- addCartProduct,
- calculateCartTotals,
- reconcileCartProducts,
- setCartProductQuantity
-} from '../cart.logic'
-
-const PRODUCT: Product = {
- id: 'product-test',
- name: 'Test object',
- slug: 'test-object',
- description: 'A deterministic product for cart owner tests.',
- priceCents: 2500,
- currency: 'USD',
- categoryId: 'category-test',
- stock: 2,
- rating: 4.5,
- imageUrl: 'https://picsum.photos/seed/test/640/480',
- createdAt: '2026-07-20T10:00:00.000Z',
- version: 1
-}
-
-describe('cart domain logic', () => {
- it('caps quantity at the current product stock', () => {
- const once = addCartProduct([], PRODUCT)
- const twice = addCartProduct(once, PRODUCT)
- const threeAttempts = addCartProduct(twice, PRODUCT)
-
- expect(threeAttempts).toEqual([{ product: PRODUCT, quantity: 2 }])
- })
-
- it('caps high-stock products at the order line contract limit', () => {
- const highStockProduct = { ...PRODUCT, stock: 100 }
- const lines = addCartProduct([], highStockProduct)
-
- expect(setCartProductQuantity(lines, highStockProduct.id, 21)).toEqual([
- { product: highStockProduct, quantity: 20 }
- ])
- })
-
- it('removes a line when quantity becomes zero', () => {
- const lines = addCartProduct([], PRODUCT)
-
- expect(setCartProductQuantity(lines, PRODUCT.id, 0)).toEqual([])
- })
-
- it('rejects checkout for mixed currencies while retaining item totals', () => {
- const eurProduct: Product = { ...PRODUCT, id: 'product-eur', currency: 'EUR' }
- const totals = calculateCartTotals([
- { product: PRODUCT, quantity: 1 },
- { product: eurProduct, quantity: 1 }
- ])
-
- expect(totals).toEqual({
- itemCount: 2,
- subtotalCents: 5000,
- currency: null
- })
- })
-
- it('reconciles a stale price and version before checkout', () => {
- const lines = [{ product: PRODUCT, quantity: 1 }]
- const latestProduct = { ...PRODUCT, priceCents: 3100, version: 2 }
-
- expect(reconcileCartProducts(lines, [latestProduct])).toEqual({
- lines: [{ product: latestProduct, quantity: 1 }],
- hasChanges: true
- })
- })
-})
diff --git a/examples/demo-frontend/src/domains/cart/tests/cart.persistence.test.ts b/examples/demo-frontend/src/domains/cart/tests/cart.persistence.test.ts
deleted file mode 100644
index 1123752..0000000
--- a/examples/demo-frontend/src/domains/cart/tests/cart.persistence.test.ts
+++ /dev/null
@@ -1,140 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-import {
- hydrateCartLines,
- initializePersistedCartSnapshot,
- readPersistedCartSnapshot,
- resetPersistedCartSnapshot,
- updatePersistedCartSnapshot
-} from '../cart.persistence'
-
-const STORED_LINE = {
- product: {
- id: 'product-test',
- name: 'Test object',
- slug: 'test-object',
- description: 'Persisted cart fixture.',
- priceCents: 2500,
- currency: 'USD' as const,
- categoryId: 'category-test',
- stock: 2,
- rating: 4.5,
- imageUrl: 'https://picsum.photos/seed/test/640/480',
- createdAt: '2026-07-20T10:00:00.000Z',
- version: 1
- },
- quantity: 1
-}
-
-/**
- * Создаёт localStorage для persisted cart CAS tests.
- */
-const createStorage = (): Storage => {
- const values = new Map()
-
- return {
- get length() {
- return values.size
- },
- clear: () => values.clear(),
- getItem: (key) => values.get(key) ?? null,
- key: (index) => [...values.keys()][index] ?? null,
- removeItem: (key) => values.delete(key),
- setItem: (key, value) => values.set(key, value)
- }
-}
-
-beforeEach(() => {
- vi.stubGlobal('window', {
- localStorage: createStorage(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn()
- })
-})
-
-afterEach(() => {
- vi.restoreAllMocks()
- vi.unstubAllGlobals()
-})
-
-describe('cart persistence boundary', () => {
- it('rejects a persisted product image outside the configured host', () => {
- const storedLines = [
- {
- ...STORED_LINE,
- product: {
- ...STORED_LINE.product,
- imageUrl: 'https://example.com/untrusted.png'
- }
- }
- ]
-
- expect(hydrateCartLines(storedLines)).toEqual([])
- })
-
- it('rejects a legacy quantity above the order line limit', () => {
- const storedLines = [
- {
- ...STORED_LINE,
- product: { ...STORED_LINE.product, stock: 100 },
- quantity: 21
- }
- ]
-
- expect(hydrateCartLines(storedLines)).toEqual([])
- })
-
- it('rejects a fractional persisted quantity', () => {
- expect(hydrateCartLines([{ ...STORED_LINE, quantity: 1.5 }])).toEqual([])
- })
-
- it('does not clear a newer cross-tab snapshot before its storage event arrives', async () => {
- const initialSnapshot = await initializePersistedCartSnapshot()
-
- expect(initialSnapshot).not.toBeNull()
-
- if (initialSnapshot === null) {
- return
- }
-
- const checkoutSnapshot = await updatePersistedCartSnapshot(() => [STORED_LINE])
-
- expect(checkoutSnapshot.status).toBe('updated')
-
- if (checkoutSnapshot.status !== 'updated') {
- return
- }
-
- await updatePersistedCartSnapshot((lines) => {
- const line = lines[0]
-
- return line === undefined ? lines : [{ ...line, quantity: 2 }]
- })
-
- const staleClear = await updatePersistedCartSnapshot(
- () => [],
- checkoutSnapshot.snapshot.revision
- )
- const current = readPersistedCartSnapshot()
-
- expect(staleClear).toEqual({ status: 'stale' })
- expect(current.status === 'ready' ? current.snapshot?.lines[0]?.quantity : null).toBe(2)
- })
-
- it('keeps forced recovery above the local revision when the clock moves backwards', async () => {
- const currentSnapshot = await initializePersistedCartSnapshot()
-
- expect(currentSnapshot).not.toBeNull()
-
- if (currentSnapshot === null) {
- return
- }
-
- vi.spyOn(Date, 'now').mockReturnValue(1)
- window.localStorage.setItem('demo-frontend:cart', '{broken-json')
-
- const recoveredSnapshot = await resetPersistedCartSnapshot(currentSnapshot.revision)
-
- expect(recoveredSnapshot?.revision).toBeGreaterThan(currentSnapshot.revision)
- })
-})
diff --git a/examples/demo-frontend/src/domains/cart/types/cart-provider-props.type.ts b/examples/demo-frontend/src/domains/cart/types/cart-provider-props.type.ts
deleted file mode 100644
index 5a20a38..0000000
--- a/examples/demo-frontend/src/domains/cart/types/cart-provider-props.type.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type { ReactNode } from 'react'
-
-/**
- * Props application-scoped cart provider.
- */
-export type CartProviderProps = {
- /** Поддерево, использующее cart API. */
- children: ReactNode
-}
diff --git a/examples/demo-frontend/src/domains/cart/types/cart.type.ts b/examples/demo-frontend/src/domains/cart/types/cart.type.ts
deleted file mode 100644
index 6d82f8b..0000000
--- a/examples/demo-frontend/src/domains/cart/types/cart.type.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import type { CatalogCurrency, Product } from '@/domains/catalog'
-
-/**
- * Одна продуктовая позиция корзины.
- */
-export type CartLine = {
- /** Снимок продукта на момент добавления. */
- product: Product
- /** Количество единиц от 1 до backend-лимита 20. */
- quantity: number
-}
-
-/**
- * Рассчитанные итоги текущей корзины.
- */
-export type CartTotals = {
- /** Суммарное число единиц. */
- itemCount: number
- /** Сумма в минимальных единицах при единой валюте. */
- subtotalCents: number
- /** Единая валюта либо null для смешанной корзины. */
- currency: CatalogCurrency | null
-}
-
-/**
- * Результат сверки cart snapshots с authority catalog.
- */
-export type CartReconciliation = {
- /** Строки с актуальными product snapshots. */
- lines: CartLine[]
- /** Изменился ли price, currency, stock, version или доступность. */
- hasChanges: boolean
-}
-
-/**
- * Результат CAS-сверки checkout snapshot с authority catalog.
- */
-export type CartReconciliationStatus = 'stale' | 'unavailable' | 'unchanged' | 'updated'
-
-/**
- * Результат conditional cart clear после checkout или fixture transition.
- */
-export type CartClearStatus = 'cleared' | 'stale' | 'unavailable'
-
-/**
- * Результат persisted revision precondition.
- */
-export type CartRevisionStatus = 'current' | 'stale' | 'unavailable'
-
-/**
- * Recoverable failure persisted cart repository.
- */
-export type CartError = {
- /** Стабильный код storage failure. */
- code: 'storage-unavailable'
- /** Сообщение для application-level feedback. */
- message: string
-}
-
-/**
- * Публичный API cart-домена в application scope.
- */
-export type CartContextValue = CartTotals & {
- /** Позиции корзины. */
- lines: CartLine[]
- /** Завершено ли восстановление persisted-снимка. */
- isHydrated: boolean
- /** Revision текущего immutable cart snapshot. */
- revision: number
- /** Последний recoverable repository failure. */
- error: CartError | null
- /** Добавляет одну единицу продукта с учётом stock. */
- addProduct: (product: Product) => Promise
- /** Изменяет количество или удаляет строку при нуле. */
- setQuantity: (productId: string, quantity: number) => Promise
- /** Удаляет продукт из корзины. */
- removeProduct: (productId: string) => Promise
- /** Заменяет snapshots только для ожидаемой cart revision. */
- reconcileProducts: (
- products: Product[],
- expectedRevision: number
- ) => Promise
- /** Проверяет, что async workflow всё ещё относится к cart snapshot. */
- isCurrentRevision: (revision: number) => Promise
- /** Очищает корзину безусловно либо только для ожидаемой revision. */
- clearCart: (expectedRevision?: number) => Promise
-}
diff --git a/examples/demo-frontend/src/domains/catalog/catalog.validation.ts b/examples/demo-frontend/src/domains/catalog/catalog.validation.ts
deleted file mode 100644
index c42c543..0000000
--- a/examples/demo-frontend/src/domains/catalog/catalog.validation.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { CreateProductInput, UpdateProductInput } from './types/catalog-command.type'
-import type { CatalogError } from './types/catalog-error.type'
-
-export const PRODUCT_IMAGE_PLACEHOLDER = '/product-placeholder.svg'
-
-/**
- * Проверяет, что remote image может быть безопасно отрисован Next Image.
- */
-export const isSupportedProductImageUrl = (value: string): boolean => {
- if (value === PRODUCT_IMAGE_PLACEHOLDER) {
- return true
- }
-
- try {
- const url = new URL(value)
-
- return url.protocol === 'https:' && url.hostname === 'picsum.photos'
- } catch {
- return false
- }
-}
-
-/**
- * Нормализует внешний image URL к гарантированно отображаемому product source.
- */
-export const normalizeProductImageUrl = (value: string): string => {
- return isSupportedProductImageUrl(value) ? value : PRODUCT_IMAGE_PLACEHOLDER
-}
-
-/**
- * Проверяет frontend product contract до необратимой backend mutation.
- */
-export const validateProductInput = (
- input: CreateProductInput | UpdateProductInput
-): CatalogError | null => {
- if (
- input.imageUrl !== undefined &&
- (
- input.imageUrl === PRODUCT_IMAGE_PLACEHOLDER ||
- !isSupportedProductImageUrl(input.imageUrl)
- )
- ) {
- return {
- code: 'validation',
- message: 'Use an HTTPS image hosted on picsum.photos.'
- }
- }
-
- return null
-}
diff --git a/examples/demo-frontend/src/domains/catalog/errors/catalog-error.mapper.ts b/examples/demo-frontend/src/domains/catalog/errors/catalog-error.mapper.ts
deleted file mode 100644
index 30830d1..0000000
--- a/examples/demo-frontend/src/domains/catalog/errors/catalog-error.mapper.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { readSimpleApiFailure } from '@/infra/simple-rest-api'
-
-import type { CatalogError } from '../types/catalog-error.type'
-
-/**
- * Переводит technical failure в ожидаемую ошибку catalog-домена.
- */
-export const mapCatalogError = (error: unknown): CatalogError => {
- const failure = readSimpleApiFailure(error)
-
- if (failure.code === 'SESSION_CHANGED') {
- return { code: 'forbidden', message: 'The active admin session changed. Start again.' }
- }
-
- if (failure.status === 404) {
- return { code: 'not-found', message: 'The requested product no longer exists.' }
- }
-
- if (failure.status === 409) {
- return { code: 'conflict', message: 'This product changed. Reload it before saving again.' }
- }
-
- if (failure.status === 403) {
- return { code: 'forbidden', message: 'Administrator access is required.' }
- }
-
- if (failure.status === 400 || failure.status === 422) {
- return { code: 'validation', message: 'Check the submitted product fields.' }
- }
-
- if (failure.status === 0) {
- return { code: 'service-unavailable', message: failure.message }
- }
-
- return { code: 'unknown', message: 'The catalog request could not be completed.' }
-}
diff --git a/examples/demo-frontend/src/domains/catalog/hooks/use-catalog-commands.hook.ts b/examples/demo-frontend/src/domains/catalog/hooks/use-catalog-commands.hook.ts
deleted file mode 100644
index 8602121..0000000
--- a/examples/demo-frontend/src/domains/catalog/hooks/use-catalog-commands.hook.ts
+++ /dev/null
@@ -1,113 +0,0 @@
-'use client'
-
-import {
- createSimpleAuthSessionRequest,
- simpleRestApi,
- useSimpleRestApiCache
-} from '@/infra/simple-rest-api'
-
-import { mapCatalogError } from '../errors/catalog-error.mapper'
-import { mapProduct } from '../mappers/catalog.mapper'
-import { validateProductInput } from '../catalog.validation'
-import type { CatalogCommands } from '../types/catalog-command.type'
-
-/**
- * Возвращает catalog commands, привязанные к текущему REST cache scope.
- */
-export const useCatalogCommands = (): CatalogCommands => {
- const cache = useSimpleRestApiCache()
-
- /**
- * Создаёт продукт с domain validation и backend RBAC.
- */
- const createProduct: CatalogCommands['createProduct'] = async (input, sessionKey) => {
- const validationError = validateProductInput(input)
-
- if (validationError !== null) {
- return { isSuccess: false, error: validationError }
- }
-
- try {
- const response = await simpleRestApi.products.simpleProductsCreate(
- input,
- createSimpleAuthSessionRequest(sessionKey)
- )
- const product = mapProduct(response.data)
-
- await cache.invalidateCatalog()
-
- return { isSuccess: true, data: product }
- } catch (error) {
- return { isSuccess: false, error: mapCatalogError(error) }
- }
- }
-
- /**
- * Сохраняет продукт с optimistic-lock версией последнего чтения.
- */
- const updateProduct: CatalogCommands['updateProduct'] = async (input, sessionKey) => {
- const validationError = validateProductInput(input)
-
- if (validationError !== null) {
- return { isSuccess: false, error: validationError }
- }
-
- const { id, ...changes } = input
-
- try {
- const response = await simpleRestApi.products.simpleProductsUpdate(
- { id },
- changes,
- createSimpleAuthSessionRequest(sessionKey)
- )
- const product = mapProduct(response.data)
-
- await cache.invalidateCatalog()
-
- return { isSuccess: true, data: product }
- } catch (error) {
- return { isSuccess: false, error: mapCatalogError(error) }
- }
- }
-
- /**
- * Удаляет продукт и ревалидирует только catalog resources.
- */
- const removeProduct: CatalogCommands['removeProduct'] = async (productId, sessionKey) => {
- try {
- await simpleRestApi.products.simpleProductsRemove(
- { id: productId },
- createSimpleAuthSessionRequest(sessionKey)
- )
- await cache.invalidateCatalog()
-
- return { isSuccess: true, data: { id: productId } }
- } catch (error) {
- return { isSuccess: false, error: mapCatalogError(error) }
- }
- }
-
- /**
- * Загружает свежие product snapshots перед checkout или conflict recovery.
- */
- const loadProducts: CatalogCommands['loadProducts'] = async (productIds) => {
- const uniqueProductIds = [...new Set(productIds)]
-
- try {
- const responses = await Promise.all(
- uniqueProductIds.map((productId) =>
- simpleRestApi.products.simpleProductsGet({ id: productId })
- )
- )
-
- return {
- isSuccess: true,
- data: responses.map((response) => mapProduct(response.data))
- }
- } catch (error) {
- return { isSuccess: false, error: mapCatalogError(error) }
- }
- }
-
- return { createProduct, updateProduct, removeProduct, loadProducts }
-}
diff --git a/examples/demo-frontend/src/domains/catalog/hooks/use-categories.hook.ts b/examples/demo-frontend/src/domains/catalog/hooks/use-categories.hook.ts
deleted file mode 100644
index e7aafcc..0000000
--- a/examples/demo-frontend/src/domains/catalog/hooks/use-categories.hook.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client'
-
-import { useGetCategories } from '@/infra/simple-rest-api'
-
-import { mapCatalogError } from '../errors/catalog-error.mapper'
-import { mapCategory } from '../mappers/catalog.mapper'
-import type { CategoriesState } from '../types/catalog-state.type'
-
-/**
- * Получает справочник категорий для фильтров и admin-форм.
- */
-export const useCategories = (): CategoriesState => {
- const query = useGetCategories()
-
- return {
- categories: query.data?.data.map(mapCategory) ?? [],
- isLoading: query.isLoading,
- error: query.error ? mapCatalogError(query.error) : null,
- reload: async () => {
- await query.mutate()
- }
- }
-}
diff --git a/examples/demo-frontend/src/domains/catalog/hooks/use-product-catalog.hook.ts b/examples/demo-frontend/src/domains/catalog/hooks/use-product-catalog.hook.ts
deleted file mode 100644
index e84c925..0000000
--- a/examples/demo-frontend/src/domains/catalog/hooks/use-product-catalog.hook.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-'use client'
-
-import { useGetProducts } from '@/infra/simple-rest-api'
-
-import { mapCatalogError } from '../errors/catalog-error.mapper'
-import { mapProduct } from '../mappers/catalog.mapper'
-import type { CatalogFilters } from '../types/catalog-query.type'
-import type { ProductCatalogState } from '../types/catalog-state.type'
-
-/**
- * Получает фильтрованную выдачу и публикует только catalog-модели.
- */
-export const useProductCatalog = (filters: CatalogFilters): ProductCatalogState => {
- const query = useGetProducts({
- page: filters.page,
- limit: filters.limit,
- search: filters.search || undefined,
- categoryId: filters.categoryId || undefined,
- sort: filters.sort
- })
- const products = query.data?.data.map(mapProduct) ?? []
- const meta = query.data?.meta
- const pagination = meta
- ? {
- page: meta.page,
- limit: meta.limit,
- total: meta.total,
- totalPages: meta.totalPages
- }
- : null
-
- return {
- products,
- pagination,
- isLoading: query.isLoading,
- isRefreshing: query.isValidating && query.data !== undefined,
- error: query.error ? mapCatalogError(query.error) : null,
- reload: async () => {
- await query.mutate()
- }
- }
-}
diff --git a/examples/demo-frontend/src/domains/catalog/hooks/use-product.hook.ts b/examples/demo-frontend/src/domains/catalog/hooks/use-product.hook.ts
deleted file mode 100644
index cc11ae3..0000000
--- a/examples/demo-frontend/src/domains/catalog/hooks/use-product.hook.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client'
-
-import { useGetProduct } from '@/infra/simple-rest-api'
-
-import { mapCatalogError } from '../errors/catalog-error.mapper'
-import { mapProduct } from '../mappers/catalog.mapper'
-import type { ProductState } from '../types/catalog-state.type'
-
-/**
- * Получает один продукт по route-параметру.
- */
-export const useProduct = (productId: string): ProductState => {
- const query = useGetProduct(productId ? { id: productId } : null)
-
- return {
- product: query.data ? mapProduct(query.data.data) : null,
- isLoading: query.isLoading,
- error: query.error ? mapCatalogError(query.error) : null,
- reload: async () => {
- await query.mutate()
- }
- }
-}
diff --git a/examples/demo-frontend/src/domains/catalog/index.ts b/examples/demo-frontend/src/domains/catalog/index.ts
deleted file mode 100644
index 6fe3e2b..0000000
--- a/examples/demo-frontend/src/domains/catalog/index.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-'use client'
-
-export { useCategories } from './hooks/use-categories.hook'
-export { useCatalogCommands } from './hooks/use-catalog-commands.hook'
-export { useProduct } from './hooks/use-product.hook'
-export { useProductCatalog } from './hooks/use-product-catalog.hook'
-export {
- isSupportedProductImageUrl,
- PRODUCT_IMAGE_PLACEHOLDER
-} from './catalog.validation'
-export type {
- CatalogCommands,
- CreateProductInput,
- RemovedProduct,
- UpdateProductInput
-} from './types/catalog-command.type'
-export type { CatalogError, CatalogErrorCode } from './types/catalog-error.type'
-export type {
- CatalogCurrency,
- CatalogPagination,
- Category,
- Product
-} from './types/catalog-model.type'
-export type { CatalogFilters, CatalogSort } from './types/catalog-query.type'
-export type {
- CategoriesState,
- ProductCatalogState,
- ProductState
-} from './types/catalog-state.type'
diff --git a/examples/demo-frontend/src/domains/catalog/mappers/catalog.mapper.ts b/examples/demo-frontend/src/domains/catalog/mappers/catalog.mapper.ts
deleted file mode 100644
index 05db80d..0000000
--- a/examples/demo-frontend/src/domains/catalog/mappers/catalog.mapper.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { SimpleCategoryDto, SimpleProductDto } from '@/infra/simple-rest-api'
-
-import type { Category, Product } from '../types/catalog-model.type'
-import { normalizeProductImageUrl } from '../catalog.validation'
-
-/**
- * Переводит product DTO в модель catalog-домена.
- */
-export const mapProduct = (product: SimpleProductDto): Product => {
- return {
- id: product.id,
- name: product.name,
- slug: product.slug,
- description: product.description,
- priceCents: product.priceCents,
- currency: product.currency,
- categoryId: product.categoryId,
- stock: product.stock,
- rating: product.rating,
- imageUrl: normalizeProductImageUrl(product.imageUrl),
- createdAt: product.createdAt,
- version: product.version
- }
-}
-
-/**
- * Переводит category DTO в модель catalog-домена.
- */
-export const mapCategory = (category: SimpleCategoryDto): Category => {
- return {
- id: category.id,
- name: category.name,
- slug: category.slug,
- productCount: category.productCount
- }
-}
diff --git a/examples/demo-frontend/src/domains/catalog/tests/catalog.mapper.test.ts b/examples/demo-frontend/src/domains/catalog/tests/catalog.mapper.test.ts
deleted file mode 100644
index eff59b1..0000000
--- a/examples/demo-frontend/src/domains/catalog/tests/catalog.mapper.test.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import type { SimpleProductDto } from '@/infra/simple-rest-api'
-
-import { mapProduct } from '../mappers/catalog.mapper'
-import { PRODUCT_IMAGE_PLACEHOLDER } from '../catalog.validation'
-
-const PRODUCT_DTO: SimpleProductDto = {
- id: 'product-keyboard',
- name: 'Mechanical Keyboard',
- slug: 'mechanical-keyboard',
- description: 'A deterministic keyboard.',
- priceCents: 12990,
- currency: 'USD',
- categoryId: 'category-electronics',
- stock: 24,
- rating: 4.8,
- imageUrl: 'https://picsum.photos/seed/keyboard/640/480',
- createdAt: '2026-07-20T10:00:00.000Z',
- version: 3
-}
-
-describe('catalog DTO mapping', () => {
- it('creates a domain product without retaining the DTO object', () => {
- const product = mapProduct(PRODUCT_DTO)
-
- expect(product).not.toBe(PRODUCT_DTO)
- expect(product).toMatchObject({
- id: 'product-keyboard',
- priceCents: 12990,
- version: 3
- })
- })
-
- it('replaces an unsupported remote image before it reaches Next Image', () => {
- const product = mapProduct({
- ...PRODUCT_DTO,
- imageUrl: 'https://example.com/untrusted.png'
- })
-
- expect(product.imageUrl).toBe(PRODUCT_IMAGE_PLACEHOLDER)
- })
-})
diff --git a/examples/demo-frontend/src/domains/catalog/tests/catalog.validation.test.ts b/examples/demo-frontend/src/domains/catalog/tests/catalog.validation.test.ts
deleted file mode 100644
index 66582d3..0000000
--- a/examples/demo-frontend/src/domains/catalog/tests/catalog.validation.test.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import { validateProductInput } from '../catalog.validation'
-import type { CreateProductInput } from '../types/catalog-command.type'
-
-const INPUT: CreateProductInput = {
- name: 'Safe object',
- description: 'A deterministic object with an allowed remote image.',
- priceCents: 9900,
- currency: 'USD',
- categoryId: 'category-electronics',
- stock: 10,
- imageUrl: 'https://picsum.photos/seed/safe-object/640/480'
-}
-
-describe('catalog product validation', () => {
- it('accepts the configured image host', () => {
- expect(validateProductInput(INPUT)).toBeNull()
- })
-
- it('rejects URLs that Next Image cannot render', () => {
- expect(
- validateProductInput({ ...INPUT, imageUrl: 'https://example.com/object.jpg' })?.code
- ).toBe('validation')
- expect(
- validateProductInput({ ...INPUT, imageUrl: 'http://picsum.photos/seed/object/640/480' })?.code
- ).toBe('validation')
- })
-})
diff --git a/examples/demo-frontend/src/domains/catalog/types/catalog-command.type.ts b/examples/demo-frontend/src/domains/catalog/types/catalog-command.type.ts
deleted file mode 100644
index b9193a5..0000000
--- a/examples/demo-frontend/src/domains/catalog/types/catalog-command.type.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import type { CatalogCurrency, Product } from './catalog-model.type'
-import type { CatalogError } from './catalog-error.type'
-import type { Result } from '@/shared/types/result.type'
-
-/**
- * Поля нового продукта, доступные администратору.
- */
-export type CreateProductInput = {
- /** Название продукта. */
- name: string
- /** Полное описание продукта. */
- description: string
- /** Цена в минимальных единицах. */
- priceCents: number
- /** Валюта цены. */
- currency: CatalogCurrency
- /** Идентификатор существующей категории. */
- categoryId: string
- /** Начальный остаток. */
- stock: number
- /** URL изображения. */
- imageUrl: string
-}
-
-/**
- * Изменение продукта с optimistic-lock версией.
- */
-export type UpdateProductInput = Partial & {
- /** Идентификатор изменяемого продукта. */
- id: string
- /** Последняя прочитанная версия. */
- version: number
-}
-
-/**
- * Результат удаления продукта.
- */
-export type RemovedProduct = Pick
-
-/**
- * Public mutation/read-through commands catalog-домена.
- */
-export type CatalogCommands = {
- /** Создаёт продукт в captured admin session. */
- createProduct: (
- input: CreateProductInput,
- sessionKey: string
- ) => Promise>
- /** Сохраняет optimistic-lock изменение в captured admin session. */
- updateProduct: (
- input: UpdateProductInput,
- sessionKey: string
- ) => Promise>
- /** Удаляет продукт в captured admin session. */
- removeProduct: (
- productId: string,
- sessionKey: string
- ) => Promise>
- /** Загружает authority snapshots для checkout reconciliation. */
- loadProducts: (productIds: string[]) => Promise>
-}
diff --git a/examples/demo-frontend/src/domains/catalog/types/catalog-error.type.ts b/examples/demo-frontend/src/domains/catalog/types/catalog-error.type.ts
deleted file mode 100644
index 32bd1d8..0000000
--- a/examples/demo-frontend/src/domains/catalog/types/catalog-error.type.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Коды ожидаемых ошибок каталога.
- */
-export type CatalogErrorCode =
- | 'not-found'
- | 'conflict'
- | 'forbidden'
- | 'validation'
- | 'service-unavailable'
- | 'unknown'
-
-/**
- * Безопасная ошибка catalog-домена.
- */
-export type CatalogError = {
- /** Код для выбора UI outcome. */
- code: CatalogErrorCode
- /** Сообщение без backend payload. */
- message: string
-}
diff --git a/examples/demo-frontend/src/domains/catalog/types/catalog-model.type.ts b/examples/demo-frontend/src/domains/catalog/types/catalog-model.type.ts
deleted file mode 100644
index 383b1f0..0000000
--- a/examples/demo-frontend/src/domains/catalog/types/catalog-model.type.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Валюта витрины, поддерживаемая Simple API.
- */
-export type CatalogCurrency = 'USD' | 'EUR'
-
-/**
- * Продукт каталога с optimistic-lock версией.
- */
-export type Product = {
- /** Стабильный идентификатор. */
- id: string
- /** Название продукта. */
- name: string
- /** URL-friendly имя. */
- slug: string
- /** Описание для карточки и detail-экрана. */
- description: string
- /** Цена в минимальных единицах валюты. */
- priceCents: number
- /** Валюта цены. */
- currency: CatalogCurrency
- /** Идентификатор категории. */
- categoryId: string
- /** Доступный остаток. */
- stock: number
- /** Рейтинг от нуля до пяти. */
- rating: number
- /** URL изображения продукта. */
- imageUrl: string
- /** ISO-дата создания. */
- createdAt: string
- /** Версия последнего прочитанного состояния. */
- version: number
-}
-
-/**
- * Категория выдачи продуктов.
- */
-export type Category = {
- /** Стабильный идентификатор. */
- id: string
- /** Отображаемое название. */
- name: string
- /** URL-friendly имя. */
- slug: string
- /** Число продуктов в текущем backend seed. */
- productCount: number
-}
-
-/**
- * Метаданные offset pagination.
- */
-export type CatalogPagination = {
- /** Текущая страница. */
- page: number
- /** Размер страницы. */
- limit: number
- /** Полное число совпадений. */
- total: number
- /** Полное число страниц. */
- totalPages: number
-}
diff --git a/examples/demo-frontend/src/domains/catalog/types/catalog-query.type.ts b/examples/demo-frontend/src/domains/catalog/types/catalog-query.type.ts
deleted file mode 100644
index 618add9..0000000
--- a/examples/demo-frontend/src/domains/catalog/types/catalog-query.type.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-/**
- * Поддерживаемая сортировка каталога.
- */
-export type CatalogSort = 'newest' | 'price-asc' | 'price-desc' | 'name'
-
-/**
- * Фильтры публичной выдачи каталога.
- */
-export type CatalogFilters = {
- /** Номер страницы. */
- page: number
- /** Число карточек на странице. */
- limit: number
- /** Поисковая строка. */
- search: string
- /** Выбранная категория или пустая строка. */
- categoryId: string
- /** Активная сортировка. */
- sort: CatalogSort
-}
diff --git a/examples/demo-frontend/src/domains/catalog/types/catalog-state.type.ts b/examples/demo-frontend/src/domains/catalog/types/catalog-state.type.ts
deleted file mode 100644
index d7d00fd..0000000
--- a/examples/demo-frontend/src/domains/catalog/types/catalog-state.type.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import type { CatalogError } from './catalog-error.type'
-import type { CatalogPagination, Category, Product } from './catalog-model.type'
-
-/**
- * Состояние фильтрованной выдачи продуктов.
- */
-export type ProductCatalogState = {
- /** Продукты текущей страницы. */
- products: Product[]
- /** Метаданные pagination, когда ответ получен. */
- pagination: CatalogPagination | null
- /** Признак первого запроса без данных. */
- isLoading: boolean
- /** Признак фоновой ревалидации. */
- isRefreshing: boolean
- /** Ожидаемая ошибка каталога. */
- error: CatalogError | null
- /** Повторяет текущий запрос. */
- reload: () => Promise
-}
-
-/**
- * Состояние справочника категорий.
- */
-export type CategoriesState = {
- /** Доступные категории. */
- categories: Category[]
- /** Признак первого запроса без данных. */
- isLoading: boolean
- /** Ожидаемая ошибка каталога. */
- error: CatalogError | null
- /** Повторяет запрос категорий. */
- reload: () => Promise
-}
-
-/**
- * Состояние детальной карточки продукта.
- */
-export type ProductState = {
- /** Продукт после успешной загрузки. */
- product: Product | null
- /** Признак первого запроса без данных. */
- isLoading: boolean
- /** Ожидаемая ошибка каталога. */
- error: CatalogError | null
- /** Повторяет detail-запрос. */
- reload: () => Promise
-}
diff --git a/examples/demo-frontend/src/domains/demo-control/demo-control.constants.ts b/examples/demo-frontend/src/domains/demo-control/demo-control.constants.ts
deleted file mode 100644
index f00a3be..0000000
--- a/examples/demo-frontend/src/domains/demo-control/demo-control.constants.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { DemoScenario, DemoScenarioOption } from './types/demo-control.type'
-
-/**
- * UI-метаданные сценариев, доступные даже при отказе testing endpoint.
- */
-export const DEMO_SCENARIO_OPTIONS: readonly DemoScenarioOption[] = [
- { value: 'normal', label: 'Normal', description: 'Happy path responses' },
- { value: 'slow', label: 'Slow', description: 'Visible loading states' },
- { value: 'timeout', label: 'Timeout', description: 'Client abort after 5 seconds' },
- { value: 'server-error', label: '500', description: 'Deterministic server failure' },
- { value: 'rate-limited', label: '429', description: 'Rate-limit response' },
- { value: 'empty', label: 'Empty', description: 'Valid empty collections' },
- { value: 'expired-auth', label: 'Expired auth', description: 'Protected requests return 401' },
- { value: 'forbidden', label: 'Forbidden', description: 'Protected requests return 403' },
- { value: 'conflict', label: 'Conflict', description: 'Mutations return 409' }
-]
-
-/**
- * Возвращает fallback-описание известного сценария.
- */
-export const getDemoScenarioOption = (scenario: DemoScenario): DemoScenarioOption => {
- return DEMO_SCENARIO_OPTIONS.find((option) => option.value === scenario) ?? DEMO_SCENARIO_OPTIONS[0]
-}
diff --git a/examples/demo-frontend/src/domains/demo-control/errors/demo-control-error.mapper.ts b/examples/demo-frontend/src/domains/demo-control/errors/demo-control-error.mapper.ts
deleted file mode 100644
index 27e7e14..0000000
--- a/examples/demo-frontend/src/domains/demo-control/errors/demo-control-error.mapper.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { readSimpleApiFailure } from '@/infra/simple-rest-api'
-
-import type { DemoControlError } from '../types/demo-control.type'
-
-/**
- * Переводит technical failure в ожидаемую demo-control ошибку.
- */
-export const mapDemoControlError = (error: unknown): DemoControlError => {
- const failure = readSimpleApiFailure(error)
-
- if (failure.status === 0) {
- return { code: 'service-unavailable', message: failure.message }
- }
-
- return { code: 'rejected', message: 'The fixture rejected this control action.' }
-}
diff --git a/examples/demo-frontend/src/domains/demo-control/hooks/use-demo-control.hook.ts b/examples/demo-frontend/src/domains/demo-control/hooks/use-demo-control.hook.ts
deleted file mode 100644
index d164b62..0000000
--- a/examples/demo-frontend/src/domains/demo-control/hooks/use-demo-control.hook.ts
+++ /dev/null
@@ -1,236 +0,0 @@
-'use client'
-
-import { useEffect, useEffectEvent, useRef, useState, useSyncExternalStore } from 'react'
-
-import {
- DEMO_SCENARIOS,
- getDemoFixtureChange,
- getDemoScenario,
- getServerDemoFixtureChange,
- getServerDemoScenario,
- publishDemoFixtureChange,
- setDemoScenario,
- simpleRestApi,
- subscribeDemoFixtureChange,
- subscribeDemoScenario,
- useSimpleRestApiCache,
- useGetDemoScenarios,
- useGetHealth
-} from '@/infra/simple-rest-api'
-import { isOneOf } from '@/shared/lib/value-predicates'
-import type { Result } from '@/shared/types/result.type'
-
-import { DEMO_SCENARIO_OPTIONS } from '../demo-control.constants'
-import { mapDemoControlError } from '../errors/demo-control-error.mapper'
-import type {
- DemoControl,
- DemoControlError,
- DemoScenario,
- DemoSeed
-} from '../types/demo-control.type'
-
-const FIXTURE_SYNC_ERROR: DemoControlError = {
- code: 'service-unavailable',
- message: 'Backend data changed, but cross-tab synchronization is unavailable. Reload this page.'
-}
-
-/**
- * Предоставляет UI управляемые backend-сценарии и testing actions.
- */
-export const useDemoControl = (): DemoControl => {
- const cache = useSimpleRestApiCache()
- const scenario = useSyncExternalStore(
- subscribeDemoScenario,
- getDemoScenario,
- getServerDemoScenario
- )
- const fixtureChange = useSyncExternalStore(
- subscribeDemoFixtureChange,
- getDemoFixtureChange,
- getServerDemoFixtureChange
- )
- const previousScenarioRef = useRef(scenario)
- const previousFixtureChangeRef = useRef(fixtureChange)
- const healthQuery = useGetHealth({ refreshInterval: 15_000 })
- const scenariosQuery = useGetDemoScenarios()
- const [isMutating, setIsMutating] = useState(false)
- const [message, setMessage] = useState(null)
- const [error, setError] = useState(null)
- const descriptionsByScenario = new Map()
-
- const synchronizeScenarioCache = useEffectEvent(() => {
- void cache.invalidateAll()
- })
- const synchronizeFixtureCache = useEffectEvent((change: string) => {
- if (change.startsWith('role:')) {
- void cache.invalidateOrders()
- return
- }
-
- void cache.invalidateAll()
- })
-
- useEffect(() => {
- if (previousScenarioRef.current !== scenario) {
- previousScenarioRef.current = scenario
- synchronizeScenarioCache()
- }
- }, [scenario])
-
- useEffect(() => {
- if (previousFixtureChangeRef.current !== fixtureChange) {
- previousFixtureChangeRef.current = fixtureChange
- synchronizeFixtureCache(fixtureChange)
- }
- }, [fixtureChange])
-
- scenariosQuery.data?.data.forEach((item) => {
- if (!isOneOf(item.name, DEMO_SCENARIOS)) {
- return
- }
-
- descriptionsByScenario.set(item.name, item.description)
- })
-
- const scenarios = DEMO_SCENARIO_OPTIONS.map((option) => ({
- ...option,
- description: descriptionsByScenario.get(option.value) ?? option.description
- }))
-
- /**
- * Переключает request header и принудительно обновляет активные queries.
- */
- const selectScenario = async (nextScenario: DemoScenario): Promise => {
- previousScenarioRef.current = nextScenario
- setDemoScenario(nextScenario)
- setMessage(null)
- setError(null)
- await cache.invalidateAll()
- }
-
- /**
- * Сбрасывает backend state к малому deterministic seed.
- */
- const resetData = async (): Promise> => {
- setIsMutating(true)
- setError(null)
-
- try {
- const response = await simpleRestApi.testing.simpleTestingReset()
- const nextMessage = response.data.message
-
- setMessage(nextMessage)
- await cache.invalidateAll()
- const changeToken = publishDemoFixtureChange('data')
-
- previousFixtureChangeRef.current = getDemoFixtureChange()
-
- if (changeToken === null) {
- setMessage(null)
- setError(FIXTURE_SYNC_ERROR)
- return { isSuccess: false, error: FIXTURE_SYNC_ERROR }
- }
-
- return { isSuccess: true, data: nextMessage }
- } catch (actionError) {
- const nextError = mapDemoControlError(actionError)
- setError(nextError)
-
- return { isSuccess: false, error: nextError }
- } finally {
- setIsMutating(false)
- }
- }
-
- /**
- * Загружает малый или большой deterministic seed.
- */
- const seedData = async (seed: DemoSeed): Promise> => {
- setIsMutating(true)
- setError(null)
-
- try {
- const response = await simpleRestApi.testing.simpleTestingSeed({ preset: seed })
- const nextMessage = response.data.message
-
- setMessage(nextMessage)
- await cache.invalidateAll()
- const changeToken = publishDemoFixtureChange('data')
-
- previousFixtureChangeRef.current = getDemoFixtureChange()
-
- if (changeToken === null) {
- setMessage(null)
- setError(FIXTURE_SYNC_ERROR)
- return { isSuccess: false, error: FIXTURE_SYNC_ERROR }
- }
-
- return { isSuccess: true, data: nextMessage }
- } catch (actionError) {
- const nextError = mapDemoControlError(actionError)
- setError(nextError)
-
- return { isSuccess: false, error: nextError }
- } finally {
- setIsMutating(false)
- }
- }
-
- /**
- * Меняет роль fixture-пользователя без выпуска новой JWT-пары.
- */
- const changeRole = async (
- userId: string,
- role: 'admin' | 'customer'
- ): Promise> => {
- setIsMutating(true)
- setError(null)
-
- try {
- const response = await simpleRestApi.testing.simpleTestingChangeRole({ userId }, { role })
- const nextMessage = `${response.data.name} is now ${response.data.role}.`
-
- setMessage(nextMessage)
- await cache.invalidateOrders()
- const changeToken = publishDemoFixtureChange('role')
-
- previousFixtureChangeRef.current = getDemoFixtureChange()
-
- if (changeToken === null) {
- setMessage(null)
- setError(FIXTURE_SYNC_ERROR)
- return { isSuccess: false, error: FIXTURE_SYNC_ERROR }
- }
-
- return { isSuccess: true, data: nextMessage }
- } catch (actionError) {
- const nextError = mapDemoControlError(actionError)
- setError(nextError)
-
- return { isSuccess: false, error: nextError }
- } finally {
- setIsMutating(false)
- }
- }
-
- const health = healthQuery.error
- ? 'offline'
- : healthQuery.isLoading || healthQuery.data === undefined
- ? 'checking'
- : 'online'
-
- return {
- scenario,
- scenarios,
- health,
- apiVersion: healthQuery.data?.data.version ?? null,
- isMutating,
- message,
- error,
- fixtureChange,
- selectScenario,
- resetData,
- seedData,
- changeRole
- }
-}
diff --git a/examples/demo-frontend/src/domains/demo-control/hooks/use-on-demo-fixture-change.hook.ts b/examples/demo-frontend/src/domains/demo-control/hooks/use-on-demo-fixture-change.hook.ts
deleted file mode 100644
index 3d05894..0000000
--- a/examples/demo-frontend/src/domains/demo-control/hooks/use-on-demo-fixture-change.hook.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-'use client'
-
-import { useEffect, useEffectEvent, useRef } from 'react'
-
-import {
- getDemoFixtureChange,
- subscribeDemoFixtureChange
-} from '@/infra/simple-rest-api'
-
-import type { DemoFixtureChangeKind } from '../types/demo-control.type'
-
-/**
- * Извлекает устойчивый domain kind из technical fixture token.
- */
-const getFixtureChangeKind = (token: string): DemoFixtureChangeKind | null => {
- if (token.startsWith('data:')) {
- return 'data'
- }
-
- if (token.startsWith('role:')) {
- return 'role'
- }
-
- return null
-}
-
-/**
- * Подписывает composition-local state на будущие fixture transitions.
- */
-export const useOnDemoFixtureChange = (
- onChange: (kind: DemoFixtureChangeKind) => void
-): void => {
- const previousTokenRef = useRef(getDemoFixtureChange())
- const handleChange = useEffectEvent(() => {
- const token = getDemoFixtureChange()
-
- if (token === previousTokenRef.current) {
- return
- }
-
- previousTokenRef.current = token
- const kind = getFixtureChangeKind(token)
-
- if (kind !== null) {
- onChange(kind)
- }
- })
-
- useEffect(() => subscribeDemoFixtureChange(handleChange), [])
-}
diff --git a/examples/demo-frontend/src/domains/demo-control/index.ts b/examples/demo-frontend/src/domains/demo-control/index.ts
deleted file mode 100644
index 6faf923..0000000
--- a/examples/demo-frontend/src/domains/demo-control/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client'
-
-export { DEMO_SCENARIO_OPTIONS } from './demo-control.constants'
-export { useDemoControl } from './hooks/use-demo-control.hook'
-export { useOnDemoFixtureChange } from './hooks/use-on-demo-fixture-change.hook'
-export type {
- DemoControl,
- DemoControlError,
- DemoFixtureChangeKind,
- DemoHealth,
- DemoScenario,
- DemoScenarioOption,
- DemoSeed
-} from './types/demo-control.type'
diff --git a/examples/demo-frontend/src/domains/demo-control/types/demo-control.type.ts b/examples/demo-frontend/src/domains/demo-control/types/demo-control.type.ts
deleted file mode 100644
index 802f6fd..0000000
--- a/examples/demo-frontend/src/domains/demo-control/types/demo-control.type.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import type { Result } from '@/shared/types/result.type'
-
-/**
- * Управляемый сценарий архитектурной демонстрации.
- */
-export type DemoScenario =
- | 'normal'
- | 'slow'
- | 'timeout'
- | 'server-error'
- | 'rate-limited'
- | 'empty'
- | 'expired-auth'
- | 'forbidden'
- | 'conflict'
-
-/**
- * Seed deterministic fixture-данных.
- */
-export type DemoSeed = 'small' | 'large'
-
-/**
- * Семантический вид fixture transition для composition-local state.
- */
-export type DemoFixtureChangeKind = 'data' | 'role'
-
-/**
- * Состояние доступности backend fixture.
- */
-export type DemoHealth = 'checking' | 'online' | 'offline'
-
-/**
- * Описание одного управляемого сценария.
- */
-export type DemoScenarioOption = {
- /** Значение X-Demo-Scenario. */
- value: DemoScenario
- /** Короткое имя для control UI. */
- label: string
- /** Frontend concern, который демонстрирует сценарий. */
- description: string
-}
-
-/**
- * Ожидаемая ошибка testing-сценария.
- */
-export type DemoControlError = {
- /** Стабильный UI-код. */
- code: 'service-unavailable' | 'rejected'
- /** Сообщение без transport payload. */
- message: string
-}
-
-/**
- * Публичный API demo-control домена.
- */
-export type DemoControl = {
- /** Текущий request-local сценарий. */
- scenario: DemoScenario
- /** Доступные управляемые сценарии. */
- scenarios: DemoScenarioOption[]
- /** Состояние health endpoint с учётом выбранного сценария. */
- health: DemoHealth
- /** Версия доступного Simple API. */
- apiVersion: string | null
- /** Выполняется ли reset, seed или role mutation. */
- isMutating: boolean
- /** Последнее сообщение testing API. */
- message: string | null
- /** Последняя ожидаемая ошибка control action. */
- error: DemoControlError | null
- /** Непрозрачный cross-tab token последнего fixture transition. */
- fixtureChange: string
- /** Переключает заголовок и ревалидирует REST cache. */
- selectScenario: (scenario: DemoScenario) => Promise
- /** Восстанавливает малый deterministic seed. */
- resetData: () => Promise>
- /** Загружает выбранный deterministic seed. */
- seedData: (seed: DemoSeed) => Promise>
- /** Меняет роль fixture-пользователя без повторного login. */
- changeRole: (
- userId: string,
- role: 'admin' | 'customer'
- ) => Promise>
-}
diff --git a/examples/demo-frontend/src/domains/orders/errors/order-error.mapper.ts b/examples/demo-frontend/src/domains/orders/errors/order-error.mapper.ts
deleted file mode 100644
index dd572b1..0000000
--- a/examples/demo-frontend/src/domains/orders/errors/order-error.mapper.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { readSimpleApiFailure } from '@/infra/simple-rest-api'
-
-import type { OrderError } from '../types/order-error.type'
-
-/**
- * Переводит technical failure в ожидаемую ошибку orders-домена.
- */
-export const mapOrderError = (error: unknown): OrderError => {
- const failure = readSimpleApiFailure(error)
-
- if (failure.code === 'EMPTY_ORDER') {
- return { code: 'empty-order', message: 'Add at least one product before checkout.' }
- }
-
- if (failure.code === 'INSUFFICIENT_STOCK') {
- return { code: 'insufficient-stock', message: 'Stock changed before the order was created.' }
- }
-
- if (failure.code === 'PRODUCT_CHANGED') {
- return { code: 'product-changed', message: 'A product changed after confirmation. Review the cart.' }
- }
-
- if (failure.code === 'DUPLICATE_ORDER_PRODUCT') {
- return { code: 'invalid-quantity', message: 'Each product may appear only once in an order.' }
- }
-
- if (failure.code === 'UNSUPPORTED_ORDER_CURRENCY') {
- return { code: 'unsupported-currency', message: 'The Simple API checkout accepts USD only.' }
- }
-
- if (failure.code === 'ORDER_CANNOT_BE_CANCELLED') {
- return { code: 'cannot-cancel', message: 'Only pending or paid orders can be cancelled.' }
- }
-
- if (failure.code === 'SESSION_CHANGED') {
- return { code: 'unauthorized', message: 'The active session changed. Start the action again.' }
- }
-
- if (failure.status === 404) {
- return { code: 'not-found', message: 'The order is not available.' }
- }
-
- if (failure.status === 401 || failure.status === 403) {
- return { code: 'unauthorized', message: 'Sign in with an account that can access this order.' }
- }
-
- if (failure.status === 0) {
- return { code: 'service-unavailable', message: failure.message }
- }
-
- return { code: 'unknown', message: 'The order request could not be completed.' }
-}
diff --git a/examples/demo-frontend/src/domains/orders/hooks/use-order-commands.hook.ts b/examples/demo-frontend/src/domains/orders/hooks/use-order-commands.hook.ts
deleted file mode 100644
index fd01f23..0000000
--- a/examples/demo-frontend/src/domains/orders/hooks/use-order-commands.hook.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-'use client'
-
-import {
- createSimpleAuthSessionRequest,
- simpleRestApi,
- useSimpleRestApiCache
-} from '@/infra/simple-rest-api'
-
-import { mapOrderError } from '../errors/order-error.mapper'
-import { mapOrder } from '../mappers/order.mapper'
-import { validateOrderDraft } from '../order.rules'
-import type { OrderCommands } from '../types/order-command.type'
-
-/**
- * Возвращает order commands, привязанные к текущему REST cache scope.
- */
-export const useOrderCommands = (): OrderCommands => {
- const cache = useSimpleRestApiCache()
-
- /**
- * Создаёт order только после domain-owned draft validation.
- */
- const createOrder: OrderCommands['createOrder'] = async (lines, sessionKey) => {
- const validationError = validateOrderDraft(lines)
-
- if (validationError !== null) {
- return { isSuccess: false, error: validationError }
- }
-
- try {
- const response = await simpleRestApi.orders.simpleOrdersCreate(
- {
- items: lines.map((line) => ({
- productId: line.productId,
- quantity: line.quantity,
- expectedVersion: line.expectedVersion,
- expectedUnitPriceCents: line.expectedUnitPriceCents
- }))
- },
- createSimpleAuthSessionRequest(sessionKey)
- )
- const order = mapOrder(response.data)
-
- await cache.invalidateOrders()
-
- return { isSuccess: true, data: order }
- } catch (error) {
- return { isSuccess: false, error: mapOrderError(error) }
- }
- }
-
- /**
- * Запрашивает cancel transition и ревалидирует только orders.
- */
- const cancelOrder: OrderCommands['cancelOrder'] = async (orderId, sessionKey) => {
- try {
- const response = await simpleRestApi.orders.simpleOrdersCancel(
- { id: orderId },
- createSimpleAuthSessionRequest(sessionKey)
- )
- const order = mapOrder(response.data)
-
- await cache.invalidateOrders()
-
- return { isSuccess: true, data: order }
- } catch (error) {
- return { isSuccess: false, error: mapOrderError(error) }
- }
- }
-
- return { createOrder, cancelOrder }
-}
diff --git a/examples/demo-frontend/src/domains/orders/hooks/use-orders.hook.ts b/examples/demo-frontend/src/domains/orders/hooks/use-orders.hook.ts
deleted file mode 100644
index f77c94c..0000000
--- a/examples/demo-frontend/src/domains/orders/hooks/use-orders.hook.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-'use client'
-
-import { useGetOrders } from '@/infra/simple-rest-api'
-
-import { mapOrderError } from '../errors/order-error.mapper'
-import { mapOrder } from '../mappers/order.mapper'
-import type { OrdersState } from '../types/orders-state.type'
-
-/**
- * Получает историю только после восстановления авторизованной сессии.
- */
-export const useOrders = (isEnabled: boolean): OrdersState => {
- const query = useGetOrders(isEnabled ? { page: 1, limit: 50 } : null)
-
- return {
- orders: query.data?.data.map(mapOrder) ?? [],
- isLoading: isEnabled && query.isLoading,
- error: query.error ? mapOrderError(query.error) : null,
- reload: async () => {
- await query.mutate()
- }
- }
-}
diff --git a/examples/demo-frontend/src/domains/orders/index.ts b/examples/demo-frontend/src/domains/orders/index.ts
deleted file mode 100644
index c85c765..0000000
--- a/examples/demo-frontend/src/domains/orders/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-'use client'
-
-export { useOrderCommands } from './hooks/use-order-commands.hook'
-export { useOrders } from './hooks/use-orders.hook'
-export { canCancelOrder, validateOrderDraft } from './order.rules'
-export type { OrderCommands } from './types/order-command.type'
-export type { OrderError, OrderErrorCode } from './types/order-error.type'
-export type { Order, OrderDraftLine, OrderLine, OrderStatus } from './types/order.type'
-export type { OrdersState } from './types/orders-state.type'
diff --git a/examples/demo-frontend/src/domains/orders/mappers/order.mapper.ts b/examples/demo-frontend/src/domains/orders/mappers/order.mapper.ts
deleted file mode 100644
index 8b38f99..0000000
--- a/examples/demo-frontend/src/domains/orders/mappers/order.mapper.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { SimpleOrderDto } from '@/infra/simple-rest-api'
-
-import type { Order } from '../types/order.type'
-
-/**
- * Переводит order DTO в модель orders-домена.
- */
-export const mapOrder = (order: SimpleOrderDto): Order => {
- return {
- id: order.id,
- userId: order.userId,
- status: order.status,
- lines: order.items.map((item) => ({
- productId: item.productId,
- productName: item.productName,
- quantity: item.quantity,
- unitPriceCents: item.unitPriceCents
- })),
- totalCents: order.totalCents,
- currency: order.currency,
- createdAt: order.createdAt
- }
-}
diff --git a/examples/demo-frontend/src/domains/orders/order.rules.ts b/examples/demo-frontend/src/domains/orders/order.rules.ts
deleted file mode 100644
index 01e881e..0000000
--- a/examples/demo-frontend/src/domains/orders/order.rules.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import { isNonEmptyArray } from '@/shared/lib/value-predicates'
-
-import type { OrderError } from './types/order-error.type'
-import type { Order, OrderDraftLine } from './types/order.type'
-
-/**
- * Максимальное количество одной позиции по order contract Simple API.
- */
-export const MAX_ORDER_LINE_QUANTITY = 20
-
-/**
- * Проверяет order draft до отправки authority backend scenario.
- */
-export const validateOrderDraft = (lines: OrderDraftLine[]): OrderError | null => {
- if (!isNonEmptyArray(lines)) {
- return { code: 'empty-order', message: 'Add at least one product before checkout.' }
- }
-
- const hasInvalidQuantity = lines.some(
- (line) =>
- !Number.isInteger(line.quantity) ||
- line.quantity < 1 ||
- line.quantity > MAX_ORDER_LINE_QUANTITY
- )
-
- if (hasInvalidQuantity) {
- return {
- code: 'invalid-quantity',
- message: `Each order line must contain between 1 and ${MAX_ORDER_LINE_QUANTITY} units.`
- }
- }
-
- if (lines.some((line) => line.quantity > line.availableStock)) {
- return {
- code: 'insufficient-stock',
- message: 'Available stock changed. Review the cart before checkout.'
- }
- }
-
- if (lines.some((line) => line.currency !== 'USD')) {
- return {
- code: 'unsupported-currency',
- message: 'The Simple API checkout accepts USD products only.'
- }
- }
-
- return null
-}
-
-/**
- * Определяет доступность cancel transition по authority order status.
- */
-export const canCancelOrder = (order: Order): boolean => {
- return order.status === 'pending' || order.status === 'paid'
-}
diff --git a/examples/demo-frontend/src/domains/orders/tests/order.mapper.test.ts b/examples/demo-frontend/src/domains/orders/tests/order.mapper.test.ts
deleted file mode 100644
index f9ecaba..0000000
--- a/examples/demo-frontend/src/domains/orders/tests/order.mapper.test.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import type { SimpleOrderDto } from '@/infra/simple-rest-api'
-
-import { mapOrder } from '../mappers/order.mapper'
-
-describe('orders DTO mapping', () => {
- it('renames transport items to domain lines', () => {
- const dto: SimpleOrderDto = {
- id: 'order-001',
- userId: 'user-customer',
- status: 'paid',
- items: [
- {
- productId: 'product-keyboard',
- productName: 'Mechanical Keyboard',
- quantity: 1,
- unitPriceCents: 12990
- }
- ],
- totalCents: 12990,
- currency: 'USD',
- createdAt: '2026-07-20T10:00:00.000Z'
- }
-
- const order = mapOrder(dto)
-
- expect(order.lines).toEqual([
- {
- productId: 'product-keyboard',
- productName: 'Mechanical Keyboard',
- quantity: 1,
- unitPriceCents: 12990
- }
- ])
- expect('items' in order).toBe(false)
- })
-})
diff --git a/examples/demo-frontend/src/domains/orders/tests/order.rules.test.ts b/examples/demo-frontend/src/domains/orders/tests/order.rules.test.ts
deleted file mode 100644
index 4a12957..0000000
--- a/examples/demo-frontend/src/domains/orders/tests/order.rules.test.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import { canCancelOrder, validateOrderDraft } from '../order.rules'
-import type { Order, OrderDraftLine } from '../types/order.type'
-
-const VALID_LINE: OrderDraftLine = {
- productId: 'product-keyboard',
- quantity: 1,
- availableStock: 24,
- currency: 'USD',
- expectedVersion: 1,
- expectedUnitPriceCents: 12990
-}
-
-const ORDER: Order = {
- id: 'order-001',
- userId: 'user-customer',
- status: 'pending',
- lines: [],
- totalCents: 12990,
- currency: 'USD',
- createdAt: '2026-07-20T10:00:00.000Z'
-}
-
-describe('orders domain rules', () => {
- it('accepts a supported draft and rejects quantity above backend limit', () => {
- expect(validateOrderDraft([VALID_LINE])).toBeNull()
- expect(validateOrderDraft([{ ...VALID_LINE, quantity: 21 }])?.code).toBe('invalid-quantity')
- })
-
- it('owns the USD checkout constraint', () => {
- expect(validateOrderDraft([{ ...VALID_LINE, currency: 'EUR' }])?.code).toBe(
- 'unsupported-currency'
- )
- })
-
- it('publishes cancel capabilities from order status', () => {
- expect(canCancelOrder(ORDER)).toBe(true)
- expect(canCancelOrder({ ...ORDER, status: 'shipped' })).toBe(false)
- expect(canCancelOrder({ ...ORDER, status: 'cancelled' })).toBe(false)
- })
-})
diff --git a/examples/demo-frontend/src/domains/orders/types/order-command.type.ts b/examples/demo-frontend/src/domains/orders/types/order-command.type.ts
deleted file mode 100644
index ef02bf6..0000000
--- a/examples/demo-frontend/src/domains/orders/types/order-command.type.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Result } from '@/shared/types/result.type'
-
-import type { OrderError } from './order-error.type'
-import type { Order, OrderDraftLine } from './order.type'
-
-/**
- * Public commands orders-домена в текущем REST cache scope.
- */
-export type OrderCommands = {
- /** Создаёт заказ из validated draft в captured auth scope. */
- createOrder: (
- lines: OrderDraftLine[],
- sessionKey: string
- ) => Promise>
- /** Запрашивает допустимый backend transition в cancelled. */
- cancelOrder: (
- orderId: string,
- sessionKey: string
- ) => Promise>
-}
diff --git a/examples/demo-frontend/src/domains/orders/types/order-error.type.ts b/examples/demo-frontend/src/domains/orders/types/order-error.type.ts
deleted file mode 100644
index 92ea888..0000000
--- a/examples/demo-frontend/src/domains/orders/types/order-error.type.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Коды ожидаемых ошибок заказа.
- */
-export type OrderErrorCode =
- | 'empty-order'
- | 'invalid-quantity'
- | 'unsupported-currency'
- | 'not-found'
- | 'cannot-cancel'
- | 'insufficient-stock'
- | 'product-changed'
- | 'unauthorized'
- | 'service-unavailable'
- | 'unknown'
-
-/**
- * Безопасная ошибка orders-домена.
- */
-export type OrderError = {
- /** Код для выбора route outcome. */
- code: OrderErrorCode
- /** Сообщение без transport payload. */
- message: string
-}
diff --git a/examples/demo-frontend/src/domains/orders/types/order.type.ts b/examples/demo-frontend/src/domains/orders/types/order.type.ts
deleted file mode 100644
index 7cf4f0c..0000000
--- a/examples/demo-frontend/src/domains/orders/types/order.type.ts
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Состояние заказа Simple storefront.
- */
-export type OrderStatus = 'pending' | 'paid' | 'shipped' | 'cancelled'
-
-/**
- * Одна строка созданного заказа.
- */
-export type OrderLine = {
- /** Идентификатор продукта. */
- productId: string
- /** Название продукта на момент заказа. */
- productName: string
- /** Заказанное количество. */
- quantity: number
- /** Цена единицы в минимальных единицах. */
- unitPriceCents: number
-}
-
-/**
- * Заказ текущего пользователя или видимый администратору заказ.
- */
-export type Order = {
- /** Стабильный идентификатор. */
- id: string
- /** Идентификатор владельца. */
- userId: string
- /** Текущее состояние. */
- status: OrderStatus
- /** Зафиксированные строки. */
- lines: OrderLine[]
- /** Итог в минимальных единицах. */
- totalCents: number
- /** Валюта итога. */
- currency: 'USD' | 'EUR'
- /** ISO-дата создания. */
- createdAt: string
-}
-
-/**
- * Domain draft line checkout-сценария.
- */
-export type OrderDraftLine = {
- /** Идентификатор продукта. */
- productId: string
- /** Запрошенное количество. */
- quantity: number
- /** Последний известный stock для preflight validation. */
- availableStock: number
- /** Валюта authority snapshot. */
- currency: 'USD' | 'EUR'
- /** Версия product snapshot, подтверждённая пользователем. */
- expectedVersion: number
- /** Цена product snapshot, подтверждённая пользователем. */
- expectedUnitPriceCents: number
-}
diff --git a/examples/demo-frontend/src/domains/orders/types/orders-state.type.ts b/examples/demo-frontend/src/domains/orders/types/orders-state.type.ts
deleted file mode 100644
index 00126b2..0000000
--- a/examples/demo-frontend/src/domains/orders/types/orders-state.type.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { OrderError } from './order-error.type'
-import type { Order } from './order.type'
-
-/**
- * Состояние доступной пользователю истории заказов.
- */
-export type OrdersState = {
- /** Заказы текущей страницы. */
- orders: Order[]
- /** Признак первого защищённого запроса. */
- isLoading: boolean
- /** Ожидаемая ошибка orders-домена. */
- error: OrderError | null
- /** Повторяет запрос истории. */
- reload: () => Promise
-}
diff --git a/examples/demo-frontend/src/infra/browser-storage/browser-storage.ts b/examples/demo-frontend/src/infra/browser-storage/browser-storage.ts
deleted file mode 100644
index b1dde42..0000000
--- a/examples/demo-frontend/src/infra/browser-storage/browser-storage.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-/**
- * Результат чтения browser storage без смешения missing и runtime failure.
- */
-export type JsonStorageReadResult =
- | {
- /** Значение отсутствует. */
- status: 'missing'
- }
- | {
- /** JSON успешно прочитан и разобран. */
- status: 'value'
- /** Неизвестное значение внешней persistence boundary. */
- value: unknown
- /** Исходный serialized payload для conditional transitions. */
- rawValue: string
- }
- | {
- /** Persisted строка не является валидным JSON. */
- status: 'invalid'
- /** Исходный payload для conditional recovery. */
- rawValue: string
- }
- | {
- /** Storage или persisted JSON недоступны. */
- status: 'unavailable'
- }
-
-const fallbackLocks = new Map>()
-
-/**
- * Читает JSON и сохраняет различие между missing value и storage failure.
- */
-export const readJsonStorageResult = (key: string): JsonStorageReadResult => {
- if (typeof window === 'undefined') {
- return { status: 'missing' }
- }
-
- let rawValue: string | null
-
- try {
- rawValue = window.localStorage.getItem(key)
- } catch {
- return { status: 'unavailable' }
- }
-
- if (rawValue === null) {
- return { status: 'missing' }
- }
-
- try {
- return { status: 'value', value: JSON.parse(rawValue), rawValue }
- } catch {
- return { status: 'invalid', rawValue }
- }
-}
-
-/**
- * Читает JSON-значение из browser storage без доступа к нему при SSR.
- */
-export const readJsonStorage = (key: string): unknown => {
- const result = readJsonStorageResult(key)
-
- return result.status === 'value' ? result.value : null
-}
-
-/**
- * Сохраняет сериализуемое значение в browser storage.
- */
-export const writeJsonStorage = (key: string, value: unknown): boolean => {
- if (typeof window === 'undefined') {
- return false
- }
-
- try {
- window.localStorage.setItem(key, JSON.stringify(value))
- return true
- } catch {
- return false
- }
-}
-
-/**
- * Удаляет значение из browser storage.
- */
-export const removeStorageValue = (key: string): boolean => {
- if (typeof window === 'undefined') {
- return false
- }
-
- try {
- window.localStorage.removeItem(key)
- return true
- } catch {
- return false
- }
-}
-
-/**
- * Сериализует storage transition между вкладками через Web Locks.
- *
- * Browser runtime без Web Locks завершается fail-closed; test/SSR realms
- * используют однопроцессную очередь, когда navigator отсутствует.
- */
-export const withBrowserStorageLock = async (
- name: string,
- action: () => Promise | T
-): Promise => {
- if (typeof navigator !== 'undefined') {
- if (navigator.locks === undefined) {
- throw new Error('Web Locks are required for cross-tab storage transitions.')
- }
-
- return navigator.locks.request(name, action)
- }
-
- const previousLock = fallbackLocks.get(name) ?? Promise.resolve()
- let releaseLock = (): void => undefined
- const currentLock = new Promise((resolve) => {
- releaseLock = resolve
- })
- const queuedLock = previousLock.then(() => currentLock)
-
- fallbackLocks.set(name, queuedLock)
- await previousLock
-
- try {
- return await action()
- } finally {
- releaseLock()
-
- if (fallbackLocks.get(name) === queuedLock) {
- fallbackLocks.delete(name)
- }
- }
-}
diff --git a/examples/demo-frontend/src/infra/browser-storage/index.ts b/examples/demo-frontend/src/infra/browser-storage/index.ts
deleted file mode 100644
index 842ed29..0000000
--- a/examples/demo-frontend/src/infra/browser-storage/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export {
- readJsonStorage,
- readJsonStorageResult,
- removeStorageValue,
- withBrowserStorageLock,
- writeJsonStorage
-} from './browser-storage'
-export type { JsonStorageReadResult } from './browser-storage'
diff --git a/examples/demo-frontend/src/infra/simple-auth-session/index.ts b/examples/demo-frontend/src/infra/simple-auth-session/index.ts
deleted file mode 100644
index dd4d2b1..0000000
--- a/examples/demo-frontend/src/infra/simple-auth-session/index.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-export {
- clearSimpleAuthSession,
- clearSimpleAuthSessionScope,
- discardSimpleAuthSession,
- getSimpleAuthSession,
- readSimpleAuthSession,
- replaceSimpleAuthSession,
- rotateSimpleAuthSession,
- subscribeSimpleAuthSession,
- withSimpleAuthRefreshLock
-} from './simple-auth-session'
-export type {
- SimpleAuthSession,
- SimpleAuthSessionIdentity,
- SimpleAuthSessionReadResult,
- SimpleAuthTokens
-} from './types/simple-auth-tokens.type'
diff --git a/examples/demo-frontend/src/infra/simple-auth-session/simple-auth-session.ts b/examples/demo-frontend/src/infra/simple-auth-session/simple-auth-session.ts
deleted file mode 100644
index dd5a1c9..0000000
--- a/examples/demo-frontend/src/infra/simple-auth-session/simple-auth-session.ts
+++ /dev/null
@@ -1,296 +0,0 @@
-import {
- readJsonStorageResult,
- removeStorageValue,
- withBrowserStorageLock,
- writeJsonStorage
-} from '@/infra/browser-storage'
-import { hasOwn, isNumber, isRecord, isString } from '@/shared/lib/value-predicates'
-
-import type {
- SimpleAuthSession,
- SimpleAuthSessionIdentity,
- SimpleAuthSessionListener,
- SimpleAuthSessionReadResult,
- SimpleAuthTokens
-} from './types/simple-auth-tokens.type'
-
-const SIMPLE_AUTH_SESSION_KEY = 'demo-frontend:simple-auth-session'
-const SIMPLE_AUTH_SESSION_LOCK = 'demo-frontend:simple-auth-session-lock'
-const SIMPLE_AUTH_REFRESH_LOCK = 'demo-frontend:simple-auth-refresh-lock'
-const sessionListeners = new Set()
-
-/**
- * Проверяет сохранённый technical JWT payload.
- */
-const isSimpleAuthTokens = (value: unknown): value is SimpleAuthTokens => {
- return (
- isRecord(value) &&
- hasOwn(value, 'accessToken') &&
- isString(value.accessToken) &&
- hasOwn(value, 'refreshToken') &&
- isString(value.refreshToken) &&
- hasOwn(value, 'expiresIn') &&
- isNumber(value.expiresIn)
- )
-}
-
-/**
- * Проверяет persisted technical session перед использованием транспортом.
- */
-const isSimpleAuthSession = (value: unknown): value is SimpleAuthSession => {
- return (
- isRecord(value) &&
- hasOwn(value, 'sessionId') &&
- isString(value.sessionId) &&
- hasOwn(value, 'revision') &&
- isString(value.revision) &&
- hasOwn(value, 'tokens') &&
- isSimpleAuthTokens(value.tokens)
- )
-}
-
-/**
- * Создаёт непрозрачный идентификатор session или token revision.
- */
-const createSessionIdentifier = (): string => {
- return globalThis.crypto.randomUUID()
-}
-
-/**
- * Уведомляет активных владельцев auth-state о persisted transition.
- */
-const notifySessionListeners = (session: SimpleAuthSession | null): void => {
- sessionListeners.forEach((listener) => listener({ status: 'ready', session }))
-}
-
-/**
- * Сравнивает persisted session с ожидаемой CAS identity.
- */
-const hasExpectedIdentity = (
- session: SimpleAuthSession,
- expected: SimpleAuthSessionIdentity
-): boolean => {
- return session.sessionId === expected.sessionId && session.revision === expected.revision
-}
-
-/**
- * Возвращает валидную persisted technical session.
- */
-export const getSimpleAuthSession = (): SimpleAuthSession | null => {
- const result = readSimpleAuthSession()
-
- return result.status === 'ready' ? result.session : null
-}
-
-/**
- * Читает persisted auth state без смешения missing и storage failure.
- */
-export const readSimpleAuthSession = (): SimpleAuthSessionReadResult => {
- const result = readJsonStorageResult(SIMPLE_AUTH_SESSION_KEY)
-
- if (result.status === 'missing') {
- return { status: 'ready', session: null }
- }
-
- if (result.status === 'unavailable') {
- return { status: 'unavailable', observedValue: null }
- }
-
- if (result.status === 'invalid') {
- return { status: 'unavailable', observedValue: result.rawValue }
- }
-
- if (!isSimpleAuthSession(result.value)) {
- return { status: 'unavailable', observedValue: result.rawValue }
- }
-
- return { status: 'ready', session: result.value }
-}
-
-/**
- * Создаёт новую session scope после успешного login.
- */
-export const replaceSimpleAuthSession = (
- tokens: SimpleAuthTokens
-): Promise => {
- return withBrowserStorageLock(SIMPLE_AUTH_SESSION_LOCK, () => {
- const session: SimpleAuthSession = {
- sessionId: createSessionIdentifier(),
- revision: createSessionIdentifier(),
- tokens
- }
-
- if (!writeJsonStorage(SIMPLE_AUTH_SESSION_KEY, session)) {
- return null
- }
-
- notifySessionListeners(session)
-
- return session
- }).catch(() => null)
-}
-
-/**
- * Атомарно вращает JWT только пока исходная session revision актуальна.
- */
-export const rotateSimpleAuthSession = (
- expected: SimpleAuthSessionIdentity,
- tokens: SimpleAuthTokens
-): Promise => {
- return withBrowserStorageLock(SIMPLE_AUTH_SESSION_LOCK, () => {
- const currentResult = readSimpleAuthSession()
-
- if (currentResult.status !== 'ready') {
- return null
- }
-
- const currentSession = currentResult.session
-
- if (currentSession === null || !hasExpectedIdentity(currentSession, expected)) {
- return null
- }
-
- const nextSession: SimpleAuthSession = {
- sessionId: currentSession.sessionId,
- revision: createSessionIdentifier(),
- tokens
- }
-
- if (!writeJsonStorage(SIMPLE_AUTH_SESSION_KEY, nextSession)) {
- return null
- }
-
- notifySessionListeners(nextSession)
-
- return nextSession
- }).catch(() => null)
-}
-
-/**
- * Очищает session только если stale request всё ещё владеет её revision.
- */
-export const clearSimpleAuthSession = (
- expected?: SimpleAuthSessionIdentity
-): Promise => {
- return withBrowserStorageLock(SIMPLE_AUTH_SESSION_LOCK, () => {
- const currentResult = readSimpleAuthSession()
-
- if (currentResult.status !== 'ready') {
- return false
- }
-
- const currentSession = currentResult.session
-
- if (currentSession === null) {
- return true
- }
-
- if (expected !== undefined && !hasExpectedIdentity(currentSession, expected)) {
- return false
- }
-
- if (!removeStorageValue(SIMPLE_AUTH_SESSION_KEY)) {
- return false
- }
-
- notifySessionListeners(null)
-
- return true
- }).catch(() => false)
-}
-
-/**
- * Закрывает logical session независимо от конкурентной JWT-ротации.
- */
-export const clearSimpleAuthSessionScope = (
- sessionId: string
-): Promise => {
- return withBrowserStorageLock(SIMPLE_AUTH_SESSION_LOCK, () => {
- const currentResult = readSimpleAuthSession()
-
- if (currentResult.status !== 'ready') {
- return false
- }
-
- const currentSession = currentResult.session
-
- if (currentSession === null) {
- return true
- }
-
- if (currentSession.sessionId !== sessionId) {
- return false
- }
-
- if (!removeStorageValue(SIMPLE_AUTH_SESSION_KEY)) {
- return false
- }
-
- notifySessionListeners(null)
-
- return true
- }).catch(() => false)
-}
-
-/**
- * Удаляет unreadable или corrupt session без предварительного чтения payload.
- */
-export const discardSimpleAuthSession = (observedValue: string): Promise => {
- return withBrowserStorageLock(SIMPLE_AUTH_SESSION_LOCK, () => {
- const currentResult = readJsonStorageResult(SIMPLE_AUTH_SESSION_KEY)
-
- if (currentResult.status === 'missing') {
- return true
- }
-
- if (
- currentResult.status === 'unavailable' ||
- currentResult.rawValue !== observedValue
- ) {
- return false
- }
-
- if (!removeStorageValue(SIMPLE_AUTH_SESSION_KEY)) {
- return false
- }
-
- notifySessionListeners(null)
-
- return true
- }).catch(() => false)
-}
-
-/**
- * Выполняет refresh-flow эксклюзивно для всех вкладок одного origin.
- */
-export const withSimpleAuthRefreshLock = (
- action: () => Promise
-): Promise => withBrowserStorageLock(SIMPLE_AUTH_REFRESH_LOCK, action)
-
-/**
- * Подписывает auth owner на local и cross-tab transitions с обязательной очисткой.
- */
-export const subscribeSimpleAuthSession = (listener: SimpleAuthSessionListener): (() => void) => {
- sessionListeners.add(listener)
-
- /**
- * Передаёт cross-tab storage transition текущему subscriber.
- */
- const handleStorage = (event: StorageEvent): void => {
- if (event.key === SIMPLE_AUTH_SESSION_KEY) {
- listener(readSimpleAuthSession())
- }
- }
-
- if (typeof window !== 'undefined') {
- window.addEventListener('storage', handleStorage)
- }
-
- return () => {
- sessionListeners.delete(listener)
-
- if (typeof window !== 'undefined') {
- window.removeEventListener('storage', handleStorage)
- }
- }
-}
diff --git a/examples/demo-frontend/src/infra/simple-auth-session/tests/simple-auth-session.test.ts b/examples/demo-frontend/src/infra/simple-auth-session/tests/simple-auth-session.test.ts
deleted file mode 100644
index f5ceb8f..0000000
--- a/examples/demo-frontend/src/infra/simple-auth-session/tests/simple-auth-session.test.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-import {
- clearSimpleAuthSession,
- clearSimpleAuthSessionScope,
- discardSimpleAuthSession,
- getSimpleAuthSession,
- readSimpleAuthSession,
- replaceSimpleAuthSession,
- rotateSimpleAuthSession
-} from '../simple-auth-session'
-import type { SimpleAuthTokens } from '../types/simple-auth-tokens.type'
-
-const TOKENS_A: SimpleAuthTokens = {
- accessToken: 'access-a',
- refreshToken: 'refresh-a',
- expiresIn: 60
-}
-
-const TOKENS_B: SimpleAuthTokens = {
- accessToken: 'access-b',
- refreshToken: 'refresh-b',
- expiresIn: 60
-}
-
-/**
- * Создаёт изолированный localStorage для CAS unit tests.
- */
-const createStorage = (): Storage => {
- const values = new Map()
-
- return {
- get length() {
- return values.size
- },
- clear: () => values.clear(),
- getItem: (key) => values.get(key) ?? null,
- key: (index) => [...values.keys()][index] ?? null,
- removeItem: (key) => values.delete(key),
- setItem: (key, value) => values.set(key, value)
- }
-}
-
-beforeEach(() => {
- vi.stubGlobal('window', {
- localStorage: createStorage(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn()
- })
-})
-
-afterEach(() => {
- vi.unstubAllGlobals()
-})
-
-describe('simple auth session CAS', () => {
- it('rotates revision without changing logical session id', async () => {
- const firstSession = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(firstSession).not.toBeNull()
-
- if (firstSession === null) {
- return
- }
-
- const rotatedSession = await rotateSimpleAuthSession(firstSession, TOKENS_B)
-
- expect(rotatedSession?.sessionId).toBe(firstSession.sessionId)
- expect(rotatedSession?.revision).not.toBe(firstSession.revision)
- expect(rotatedSession?.tokens).toEqual(TOKENS_B)
- })
-
- it('rejects stale save and clear after a newer revision wins', async () => {
- const firstSession = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(firstSession).not.toBeNull()
-
- if (firstSession === null) {
- return
- }
-
- const rotatedSession = await rotateSimpleAuthSession(firstSession, TOKENS_B)
-
- expect(rotatedSession).not.toBeNull()
- await expect(rotateSimpleAuthSession(firstSession, TOKENS_A)).resolves.toBeNull()
- await expect(clearSimpleAuthSession(firstSession)).resolves.toBe(false)
- expect(getSimpleAuthSession()?.tokens).toEqual(TOKENS_B)
- })
-
- it('does not publish a session when storage commit fails', async () => {
- vi.spyOn(window.localStorage, 'setItem').mockImplementation(() => {
- throw new Error('Storage denied')
- })
-
- await expect(replaceSimpleAuthSession(TOKENS_A)).resolves.toBeNull()
- expect(getSimpleAuthSession()).toBeNull()
- })
-
- it('reports a failed clear while persisted state remains', async () => {
- const session = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(session).not.toBeNull()
-
- if (session === null) {
- return
- }
-
- vi.spyOn(window.localStorage, 'removeItem').mockImplementation(() => {
- throw new Error('Storage denied')
- })
-
- await expect(clearSimpleAuthSession(session)).resolves.toBe(false)
- expect(getSimpleAuthSession()).toEqual(session)
- })
-
- it('distinguishes a storage read failure from a missing session', async () => {
- const session = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(session).not.toBeNull()
-
- if (session === null) {
- return
- }
-
- vi.spyOn(window.localStorage, 'getItem').mockImplementation(() => {
- throw new Error('Storage denied')
- })
-
- expect(readSimpleAuthSession()).toEqual({
- status: 'unavailable',
- observedValue: null
- })
- await expect(clearSimpleAuthSessionScope(session.sessionId)).resolves.toBe(false)
- })
-
- it('discards a corrupt persisted session without parsing it', async () => {
- window.localStorage.setItem('demo-frontend:simple-auth-session', '{broken-json')
- const corruptResult = readSimpleAuthSession()
-
- expect(corruptResult).toEqual({
- status: 'unavailable',
- observedValue: '{broken-json'
- })
-
- if (corruptResult.status !== 'unavailable' || corruptResult.observedValue === null) {
- return
- }
-
- await expect(discardSimpleAuthSession(corruptResult.observedValue)).resolves.toBe(true)
- expect(readSimpleAuthSession()).toEqual({ status: 'ready', session: null })
- })
-
- it('does not discard a new login that replaced the observed corrupt payload', async () => {
- window.localStorage.setItem('demo-frontend:simple-auth-session', '{broken-json')
- const corruptResult = readSimpleAuthSession()
-
- if (corruptResult.status !== 'unavailable' || corruptResult.observedValue === null) {
- return
- }
-
- const replacementSession = await replaceSimpleAuthSession(TOKENS_B)
-
- await expect(discardSimpleAuthSession(corruptResult.observedValue)).resolves.toBe(false)
- expect(getSimpleAuthSession()).toEqual(replacementSession)
- })
-})
diff --git a/examples/demo-frontend/src/infra/simple-auth-session/types/simple-auth-tokens.type.ts b/examples/demo-frontend/src/infra/simple-auth-session/types/simple-auth-tokens.type.ts
deleted file mode 100644
index dacbc5a..0000000
--- a/examples/demo-frontend/src/infra/simple-auth-session/types/simple-auth-tokens.type.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-/**
- * Технические JWT-данные текущей browser-сессии.
- */
-export type SimpleAuthTokens = {
- /** Короткоживущий токен защищённых запросов. */
- accessToken: string
- /** Ротируемый токен обновления сессии. */
- refreshToken: string
- /** Срок действия access-токена в секундах. */
- expiresIn: number
-}
-
-/**
- * Идентификаторы одной логической сессии и текущей JWT-ротации.
- */
-export type SimpleAuthSessionIdentity = {
- /** Стабильная область жизни от login до logout. */
- sessionId: string
- /** Меняется при каждой успешной JWT-ротации. */
- revision: string
-}
-
-/**
- * Persisted technical state одной browser-сессии.
- */
-export type SimpleAuthSession = SimpleAuthSessionIdentity & {
- /** Текущая JWT-пара. */
- tokens: SimpleAuthTokens
-}
-
-/**
- * Результат чтения persisted technical session.
- */
-export type SimpleAuthSessionReadResult =
- | {
- /** Storage доступен и session проверена. */
- status: 'ready'
- /** Активная session либо null для подтверждённого отсутствия. */
- session: SimpleAuthSession | null
- }
- | {
- /** Storage или persisted payload временно недоступны. */
- status: 'unavailable'
- /** Raw payload для CAS-recovery либо null при I/O failure. */
- observedValue: string | null
- }
-
-/**
- * Подписчик на изменение technical auth state.
- */
-export type SimpleAuthSessionListener = (result: SimpleAuthSessionReadResult) => void
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/auth-session-request.ts b/examples/demo-frontend/src/infra/simple-rest-api/auth-session-request.ts
deleted file mode 100644
index 66721bf..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/auth-session-request.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import type { RequestParams } from './generated'
-
-const EXPECTED_SESSION_HEADER = 'X-Demo-Expected-Session'
-
-/**
- * Technical failure raised before a protected request can cross session scopes.
- */
-export class SimpleAuthSessionChangedError extends Error {
- public constructor() {
- super('The active auth session changed before the request was sent.')
- this.name = 'SimpleAuthSessionChangedError'
- }
-}
-
-/**
- * Создаёт request params, привязывающие protected mutation к logical session.
- */
-export const createSimpleAuthSessionRequest = (sessionId: string): RequestParams => {
- return {
- headers: {
- [EXPECTED_SESSION_HEADER]: sessionId
- }
- }
-}
-
-/**
- * Извлекает client-only session precondition перед отправкой HTTP-запроса.
- */
-export const takeExpectedSimpleAuthSession = (headers: Headers): string | null => {
- const expectedSessionId = headers.get(EXPECTED_SESSION_HEADER)
-
- headers.delete(EXPECTED_SESSION_HEADER)
-
- return expectedSessionId
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/cache.ts b/examples/demo-frontend/src/infra/simple-rest-api/cache.ts
deleted file mode 100644
index edf129c..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/cache.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-'use client'
-
-import { useSWRConfig } from 'swr'
-import type { Arguments } from 'swr'
-
-import { isArray, isString } from '@/shared/lib/value-predicates'
-
-import type { SimpleRestApiCache } from './types/simple-rest-api-cache.type'
-
-/**
- * Возвращает endpoint из ключа текущего REST-модуля.
- */
-const getSimpleRestApiEndpoint = (key?: Arguments): string | null => {
- if (!isArray(key) || key[0] !== 'simple-rest-api' || !isString(key[1])) {
- return null
- }
-
- return key[1]
-}
-
-/**
- * Предоставляет resource-scoped invalidation внутри ближайшего SWR provider.
- */
-export const useSimpleRestApiCache = (): SimpleRestApiCache => {
- const { mutate } = useSWRConfig()
-
- /**
- * Ревалидирует catalog products и category counts.
- */
- const invalidateCatalog = async (): Promise => {
- await mutate(
- (key) => {
- const endpoint = getSimpleRestApiEndpoint(key)
-
- return endpoint?.startsWith('/api/v1/products') === true ||
- endpoint?.startsWith('/api/v1/categories') === true
- },
- undefined,
- { revalidate: true }
- )
- }
-
- /**
- * Ревалидирует только защищённые order resources.
- */
- const invalidateOrders = async (): Promise => {
- await mutate(
- (key) => getSimpleRestApiEndpoint(key)?.startsWith('/api/v1/orders') === true,
- undefined,
- { revalidate: true }
- )
- }
-
- /**
- * Ревалидирует все ресурсы после смены request scenario или fixture seed.
- */
- const invalidateAll = async (): Promise => {
- await mutate(
- (key) => getSimpleRestApiEndpoint(key) !== null,
- undefined,
- { revalidate: true }
- )
- }
-
- return { invalidateCatalog, invalidateOrders, invalidateAll }
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/client.ts b/examples/demo-frontend/src/infra/simple-rest-api/client.ts
deleted file mode 100644
index 201e0b9..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/client.ts
+++ /dev/null
@@ -1,106 +0,0 @@
-import {
- clearSimpleAuthSession,
- getSimpleAuthSession
-} from '@/infra/simple-auth-session'
-import type { SimpleAuthSessionIdentity } from '@/infra/simple-auth-session'
-import { isArray } from '@/shared/lib/value-predicates'
-
-import { SIMPLE_REST_API_BASE_URL, SIMPLE_REST_API_TIMEOUT_MS } from './config/simple-rest-api.config'
-import { ApiError, HttpClient } from './generated'
-import type { QueryParamsType, RequestContext } from './generated'
-import {
- SimpleAuthSessionChangedError,
- takeExpectedSimpleAuthSession
-} from './auth-session-request'
-import { refreshSimpleAccessToken } from './refresh-access-token'
-import { getDemoScenario } from './request-context'
-
-const requestSessionByContext = new WeakMap()
-
-/**
- * Сериализует query тем же URLSearchParams-форматом, который используют SWR-ключи.
- */
-const serializeSimpleQuery = (query: QueryParamsType): string => {
- const searchParams = new URLSearchParams()
-
- Object.entries(query).forEach(([key, value]) => {
- if (value === undefined) {
- return
- }
-
- if (isArray(value)) {
- value.forEach((item) => searchParams.append(key, String(item)))
- return
- }
-
- searchParams.set(key, String(value))
- })
-
- return searchParams.toString()
-}
-
-/**
- * Транспорт Simple API с JWT refresh и управляемыми demo-заголовками.
- */
-export const simpleHttpClient = new HttpClient({
- baseUrl: SIMPLE_REST_API_BASE_URL,
- timeout: SIMPLE_REST_API_TIMEOUT_MS,
- paramsSerializer: serializeSimpleQuery,
- onRequest: (params, context) => {
- const headers = new Headers(params.headers)
- const session = getSimpleAuthSession()
- const expectedSessionId = takeExpectedSimpleAuthSession(headers)
-
- headers.set('X-Demo-Scenario', getDemoScenario())
-
- if (
- params.secure &&
- expectedSessionId !== null &&
- session?.sessionId !== expectedSessionId
- ) {
- throw new SimpleAuthSessionChangedError()
- }
-
- if (params.secure && session !== null) {
- requestSessionByContext.set(context, {
- sessionId: session.sessionId,
- revision: session.revision
- })
-
- if (!headers.has('Authorization')) {
- headers.set('Authorization', `Bearer ${session.tokens.accessToken}`)
- }
- }
-
- return { ...params, headers }
- },
- onError: async (error, context) => {
- const requestSession = requestSessionByContext.get(context)
- const isUnauthorized =
- error instanceof ApiError && error.status === 401 && context.request.secure === true
-
- if (!isUnauthorized || requestSession === undefined) {
- throw error
- }
-
- if (context.retryCount > 0) {
- await clearSimpleAuthSession(requestSession)
- throw error
- }
-
- const nextRevision = await refreshSimpleAccessToken(requestSession)
- const currentSession = getSimpleAuthSession()
-
- const canRetry =
- nextRevision !== null &&
- currentSession !== null &&
- currentSession.sessionId === requestSession.sessionId &&
- currentSession.revision === nextRevision
-
- if (!canRetry) {
- throw error
- }
-
- return context.retry()
- }
-})
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/config/simple-rest-api.config.ts b/examples/demo-frontend/src/infra/simple-rest-api/config/simple-rest-api.config.ts
deleted file mode 100644
index f273da8..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/config/simple-rest-api.config.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Browser-visible адрес Simple API, фиксируемый Next.js во время build.
- */
-export const SIMPLE_REST_API_BASE_URL = process.env.NEXT_PUBLIC_SIMPLE_API_URL ?? 'http://localhost:3001'
-
-/**
- * Таймаут отделяет управляемый timeout-сценарий от обычного slow-сценария.
- */
-export const SIMPLE_REST_API_TIMEOUT_MS = 5_000
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/errors/index.ts b/examples/demo-frontend/src/infra/simple-rest-api/errors/index.ts
deleted file mode 100644
index 3f1125f..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/errors/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { readSimpleApiFailure } from './read-simple-api-failure'
-export type { SimpleApiFailure } from './simple-api-failure.type'
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/errors/read-simple-api-failure.ts b/examples/demo-frontend/src/infra/simple-rest-api/errors/read-simple-api-failure.ts
deleted file mode 100644
index f59331d..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/errors/read-simple-api-failure.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { hasOwn, isRecord, isString } from '@/shared/lib/value-predicates'
-
-import { ApiError } from '../generated'
-import { SimpleAuthSessionChangedError } from '../auth-session-request'
-import type { SimpleApiFailure } from './simple-api-failure.type'
-
-/**
- * Нормализует ошибку транспорта без публикации generated ApiError наружу.
- */
-export const readSimpleApiFailure = (error: unknown): SimpleApiFailure => {
- if (error instanceof SimpleAuthSessionChangedError) {
- return {
- status: 401,
- code: 'SESSION_CHANGED',
- message: 'The active auth session changed before the request was sent.'
- }
- }
-
- if (error instanceof ApiError) {
- const payload = error.error
- const code = isRecord(payload) && hasOwn(payload, 'code') && isString(payload.code)
- ? payload.code
- : 'HTTP_ERROR'
- const message = isRecord(payload) && hasOwn(payload, 'message') && isString(payload.message)
- ? payload.message
- : 'Simple API returned an error.'
-
- return { status: error.status, code, message }
- }
-
- if (error instanceof Error && error.name === 'AbortError') {
- return {
- status: 0,
- code: 'REQUEST_TIMEOUT',
- message: 'Simple API did not respond before the request timeout.'
- }
- }
-
- return {
- status: 0,
- code: 'NETWORK_ERROR',
- message: 'Simple API is unavailable. Start demo-backend on port 3001.'
- }
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/errors/simple-api-failure.type.ts b/examples/demo-frontend/src/infra/simple-rest-api/errors/simple-api-failure.type.ts
deleted file mode 100644
index cb146c1..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/errors/simple-api-failure.type.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Безопасное техническое описание сбоя Simple API.
- */
-export type SimpleApiFailure = {
- /** HTTP-статус или ноль для transport failure. */
- status: number
- /** Стабильный backend-код или локальный transport-код. */
- code: string
- /** Сообщение, пригодное для последующего доменного перевода. */
- message: string
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/create-api-client.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/create-api-client.ts
deleted file mode 100644
index 79af50b..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/create-api-client.ts
+++ /dev/null
@@ -1,60 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type { ApiRequestClient } from "./http-client";
-
-export type ApiOperation =
- (client: TClient, ...args: any[]) => any;
-
-export type ApiTree = {
- readonly [key: string]: ApiOperation | ApiTree;
-};
-
-export type BoundApi = {
- readonly [K in keyof TTree]: TTree[K] extends (
- client: TClient,
- ...args: infer Args
- ) => infer Result
- ? (...args: Args) => Result
- : TTree[K] extends ApiTree
- ? BoundApi
- : never;
-};
-
-export const createApiClient = <
- TClient extends ApiRequestClient,
- const TTree extends ApiTree,
->(
- client: TClient,
- tree: TTree,
-): BoundApi => {
- const bindNode = (
- node: ApiOperation | ApiTree,
- ): unknown => {
- if (typeof node === "function") {
- return (...args: unknown[]) => node(client, ...args);
- }
-
- return Object.fromEntries(
- Object.entries(node).map(([key, value]) => [
- key,
- bindNode(value as ApiOperation | ApiTree),
- ]),
- );
- };
-
- return bindNode(tree) as BoundApi;
-};
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/data-contracts.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/data-contracts.ts
deleted file mode 100644
index a8d8d7b..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/data-contracts.ts
+++ /dev/null
@@ -1,743 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-export interface HealthDataDto {
- /** @example "simple" */
- application: HealthDataDtoApplicationEnum;
- /** @example "ok" */
- status: HealthDataDtoStatusEnum;
- /** @format date-time */
- timestamp: string;
- /** @example "1.0.0" */
- version: string;
-}
-
-export interface HealthResponseDto {
- data: HealthDataDto;
-}
-
-export interface LoginDto {
- /**
- * @format email
- * @example "admin@demo.local"
- */
- email: string;
- /**
- * @format password
- * @minLength 8
- * @example "demo1234"
- */
- password: string;
-}
-
-export interface JwtTokensDto {
- accessToken: string;
- refreshToken: string;
- /**
- * Access-token lifetime in seconds.
- * @example 60
- */
- expiresIn: number;
- /** @example "Bearer" */
- tokenType: JwtTokensDtoTokenTypeEnum;
-}
-
-export interface SimpleUserDto {
- /** @example "user-admin" */
- id: string;
- /**
- * @format email
- * @example "admin@demo.local"
- */
- email: string;
- /** @example "Demo Admin" */
- name: string;
- role: SimpleUserDtoRoleEnum;
- /** @example "https://i.pravatar.cc/160?img=12" */
- avatarUrl: object | null;
-}
-
-export interface JwtAuthDataDto {
- tokens: JwtTokensDto;
- user: SimpleUserDto;
-}
-
-export interface JwtAuthResponseDto {
- data: JwtAuthDataDto;
-}
-
-export interface ErrorDetailDto {
- /** @example "email" */
- field?: string;
- /** @example "must be an email" */
- message: string;
- /** @example "isEmail" */
- code?: string;
-}
-
-export interface ErrorResponseDto {
- /** @example 404 */
- statusCode: number;
- /** @example "PRODUCT_NOT_FOUND" */
- code: string;
- /** @example "Product not found" */
- message: string;
- details: ErrorDetailDto[];
- /**
- * @format date-time
- * @example "2026-07-30T12:00:00.000Z"
- */
- timestamp: string;
- /** @example "/api/v1/products/product-404" */
- path: string;
- /** @example "req-5c9f7a3d" */
- requestId: string;
-}
-
-export interface RefreshTokenDto {
- /** Refresh token returned by login or the previous refresh call. */
- refreshToken: string;
-}
-
-export interface SimpleUserResponseDto {
- data: SimpleUserDto;
-}
-
-export type Object = object;
-
-export interface SimpleProductDto {
- /** @example "product-keyboard" */
- id: string;
- /** @example "Mechanical Keyboard" */
- name: string;
- /** @example "mechanical-keyboard" */
- slug: string;
- /** @example "Hot-swappable compact keyboard." */
- description: string;
- /**
- * Price in the smallest currency unit.
- * @example 12990
- */
- priceCents: number;
- /** @example "USD" */
- currency: SimpleProductDtoCurrencyEnum;
- /** @example "category-electronics" */
- categoryId: string;
- /**
- * @min 0
- * @example 24
- */
- stock: number;
- /**
- * @min 0
- * @max 5
- * @example 4.8
- */
- rating: number;
- /**
- * @format uri
- * @example "https://picsum.photos/seed/keyboard/640/480"
- */
- imageUrl: string;
- /** @format date-time */
- createdAt: string;
- /**
- * Optimistic-lock version.
- * @example 1
- */
- version: number;
-}
-
-export interface PageMetaDto {
- /**
- * @min 1
- * @example 1
- */
- page: number;
- /**
- * @min 1
- * @example 20
- */
- limit: number;
- /**
- * @min 0
- * @example 48
- */
- total: number;
- /**
- * @min 0
- * @example 3
- */
- totalPages: number;
-}
-
-export interface ProductsResponseDto {
- data: SimpleProductDto[];
- meta: PageMetaDto;
-}
-
-export interface ProductResponseDto {
- data: SimpleProductDto;
-}
-
-export interface CreateProductDto {
- /** @example "USB-C Dock" */
- name: string;
- /** @example "Dock with HDMI, Ethernet and power delivery." */
- description: string;
- /**
- * @min 0
- * @example 8990
- */
- priceCents: number;
- /** @example "USD" */
- currency: CreateProductDtoCurrencyEnum;
- /** @example "category-electronics" */
- categoryId: string;
- /**
- * @min 0
- * @example 15
- */
- stock: number;
- /**
- * @format uri
- * @example "https://picsum.photos/seed/dock/640/480"
- */
- imageUrl: string;
-}
-
-export interface UpdateProductDto {
- /** @example "USB-C Dock" */
- name?: string;
- /** @example "Dock with HDMI, Ethernet and power delivery." */
- description?: string;
- /**
- * @min 0
- * @example 8990
- */
- priceCents?: number;
- /** @example "USD" */
- currency?: UpdateProductDtoCurrencyEnum;
- /** @example "category-electronics" */
- categoryId?: string;
- /**
- * @min 0
- * @example 15
- */
- stock?: number;
- /**
- * @format uri
- * @example "https://picsum.photos/seed/dock/640/480"
- */
- imageUrl?: string;
- /**
- * Version last read by the frontend.
- * @min 1
- * @example 1
- */
- version: number;
-}
-
-export interface MutationResultDto {
- /** @example "product-001" */
- id: string;
- /** @example true */
- success: boolean;
-}
-
-export interface MutationResponseDto {
- data: MutationResultDto;
-}
-
-export interface SimpleCategoryDto {
- /** @example "category-electronics" */
- id: string;
- /** @example "Electronics" */
- name: string;
- /** @example "electronics" */
- slug: string;
- /** @example 4 */
- productCount: number;
-}
-
-export interface CategoriesResponseDto {
- data: SimpleCategoryDto[];
-}
-
-export interface CategoryResponseDto {
- data: SimpleCategoryDto;
-}
-
-export interface SimpleOrderItemDto {
- /** @example "product-keyboard" */
- productId: string;
- /** @example "Mechanical Keyboard" */
- productName: string;
- /** @example 1 */
- quantity: number;
- /** @example 12990 */
- unitPriceCents: number;
-}
-
-export interface SimpleOrderDto {
- /** @example "order-001" */
- id: string;
- /** @example "user-customer" */
- userId: string;
- status: SimpleOrderDtoStatusEnum;
- items: SimpleOrderItemDto[];
- /** @example 17980 */
- totalCents: number;
- /** @example "USD" */
- currency: SimpleOrderDtoCurrencyEnum;
- /** @format date-time */
- createdAt: string;
-}
-
-export interface OrdersResponseDto {
- data: SimpleOrderDto[];
- meta: PageMetaDto;
-}
-
-export interface OrderResponseDto {
- data: SimpleOrderDto;
-}
-
-export interface CreateOrderItemDto {
- /** @example "product-keyboard" */
- productId: string;
- /**
- * @min 1
- * @max 20
- * @example 1
- */
- quantity: number;
- /**
- * @min 1
- * @example 1
- */
- expectedVersion: number;
- /**
- * @min 0
- * @example 12990
- */
- expectedUnitPriceCents: number;
-}
-
-export interface CreateOrderDto {
- items: CreateOrderItemDto[];
-}
-
-export interface ScenarioDto {
- /** @example "slow" */
- name: string;
- /** @example "Delays the response to exercise loading and cancellation states." */
- description: string;
-}
-
-export interface ScenariosResponseDto {
- data: ScenarioDto[];
-}
-
-export interface TestingActionDataDto {
- /** @example true */
- success: boolean;
- /** @example "State reset to the default deterministic seed." */
- message: string;
-}
-
-export interface TestingActionResponseDto {
- data: TestingActionDataDto;
-}
-
-export interface ChangeSimpleRoleDto {
- /** @example "customer" */
- role: ChangeSimpleRoleDtoRoleEnum;
-}
-
-/** @example "simple" */
-export type HealthDataDtoApplicationEnum = "simple" | "complex";
-
-/** @example "ok" */
-export type HealthDataDtoStatusEnum = "ok";
-
-/** @example "Bearer" */
-export type JwtTokensDtoTokenTypeEnum = "Bearer";
-
-export type SimpleUserDtoRoleEnum = "admin" | "customer";
-
-/** @example "USD" */
-export type SimpleProductDtoCurrencyEnum = "USD" | "EUR";
-
-/** @example "USD" */
-export type CreateProductDtoCurrencyEnum = "USD" | "EUR";
-
-/** @example "USD" */
-export type UpdateProductDtoCurrencyEnum = "USD" | "EUR";
-
-export type SimpleOrderDtoStatusEnum =
- | "pending"
- | "paid"
- | "shipped"
- | "cancelled";
-
-/** @example "USD" */
-export type SimpleOrderDtoCurrencyEnum = "USD" | "EUR";
-
-/** @example "customer" */
-export type ChangeSimpleRoleDtoRoleEnum = "admin" | "customer";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleHealthHealthParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleAuthLoginParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleAuthRefreshParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleAuthLogoutParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleUsersMeParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleProductsListParams {
- /**
- * @min 1
- * @default 1
- */
- page?: Object;
- /**
- * @min 1
- * @max 100
- * @default 20
- */
- limit?: Object;
- /** @example "keyboard" */
- search?: string;
- /** @example "category-electronics" */
- categoryId?: string;
- /** @default "newest" */
- sort?: SortEnum;
-}
-
-/** @default "newest" */
-export type SortEnum = "newest" | "price-asc" | "price-desc" | "name";
-
-/** @default "newest" */
-export type SimpleProductsListParams1SortEnum =
- | "newest"
- | "price-asc"
- | "price-desc"
- | "name";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleProductsListParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleProductsCreateParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleProductsGetParams {
- id: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleProductsGetParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleProductsUpdateParams {
- id: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleProductsUpdateParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleProductsRemoveParams {
- id: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleProductsRemoveParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleCategoriesListParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleCategoriesGetParams {
- id: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleCategoriesGetParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleOrdersListParams {
- /**
- * @min 1
- * @default 1
- */
- page?: Object;
- /**
- * @min 1
- * @max 100
- * @default 20
- */
- limit?: Object;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleOrdersListParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleOrdersCreateParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleOrdersGetParams {
- id: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleOrdersGetParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleOrdersCancelParams {
- id: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleOrdersCancelParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleTestingScenariosParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleTestingResetParamsXDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export interface SimpleTestingSeedParams {
- preset: PresetEnum;
-}
-
-export type PresetEnum = "small" | "large";
-
-export type SimpleTestingSeedParams1PresetEnum = "small" | "large";
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleTestingSeedParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
-
-export type SimpleTestingSeedParams1Enum = "small" | "large";
-
-export interface SimpleTestingChangeRoleParams {
- userId: string;
-}
-
-/** Forces a deterministic frontend-testing scenario for this request. */
-export type SimpleTestingChangeRoleParams1XDemoScenarioEnum =
- | "normal"
- | "slow"
- | "timeout"
- | "server-error"
- | "rate-limited"
- | "empty"
- | "expired-auth"
- | "forbidden"
- | "conflict"
- | "large-dataset";
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/http-client.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/http-client.ts
deleted file mode 100644
index 4b6fc6e..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/http-client.ts
+++ /dev/null
@@ -1,543 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-export type QueryParamsType = Record;
-export type ResponseFormat = keyof Omit;
-
-export interface FullRequestParams extends Omit {
- /** set parameter to `true` to mark this request as protected */
- secure?: boolean;
- /** request path */
- path: string;
- /** content type of request body */
- type?: ContentType;
- /** query params */
- query?: QueryParamsType;
- /** format of response (i.e. response.json() -> format: "json") */
- format?: ResponseFormat;
- /** request body */
- body?: unknown;
- /** base url */
- baseUrl?: string;
- /** request cancellation token */
- cancelToken?: CancelToken;
- /** request timeout in milliseconds */
- timeout?: number;
-}
-
-export type RequestParams = Omit<
- FullRequestParams,
- "body" | "method" | "query" | "path"
->;
-
-export interface RequestContext {
- url: string;
- request: FullRequestParams;
- retryCount: number;
- retry: () => Promise;
-}
-
-export type RequestInterceptor = (
- params: FullRequestParams,
- context: RequestContext,
-) => FullRequestParams | Promise;
-
-export type ResponseInterceptor = (
- response: HttpResponse,
- context: RequestContext,
-) => HttpResponse | Promise>;
-
-export type ErrorInterceptor = (
- error: unknown,
- context: RequestContext,
-) => TResult | Promise;
-
-export type ParamsSerializer = (query: QueryParamsType) => string;
-export type ResponseParser = (
- response: Response,
- format?: ResponseFormat,
-) => unknown | Promise;
-
-export interface ApiRequestClient {
- request(params: FullRequestParams): Promise;
-}
-
-export interface ApiConfig
- extends Omit {
- baseUrl?: string;
- customFetch?: typeof fetch;
- paramsSerializer?: ParamsSerializer;
- responseParser?: ResponseParser;
- onRequest?: RequestInterceptor;
- onResponse?: ResponseInterceptor;
- onError?: ErrorInterceptor;
-}
-
-export interface HttpResponse
- extends Response {
- data: D;
- error: E;
-}
-
-export class ApiError extends Error {
- public readonly status: number;
- public readonly statusText: string;
- public readonly response: Response;
- public readonly data: unknown;
- public readonly error: E;
- public readonly request: FullRequestParams;
-
- constructor(
- response: Response,
- request: FullRequestParams,
- data: unknown,
- error: E,
- ) {
- super(
- `Request failed with status ${response.status} ${response.statusText}`.trim(),
- );
- this.name = "ApiError";
- this.status = response.status;
- this.statusText = response.statusText;
- this.response = response;
- this.data = data;
- this.error = error;
- this.request = request;
- }
-}
-
-export type CancelToken = Symbol | string | number;
-
-export enum ContentType {
- Json = "application/json",
- JsonApi = "application/vnd.api+json",
- FormData = "multipart/form-data",
- UrlEncoded = "application/x-www-form-urlencoded",
- Text = "text/plain",
-}
-
-export class HttpClient
- implements ApiRequestClient
-{
- public baseUrl: string = "http://localhost:3001";
- private abortControllers = new Map();
- private customFetch: typeof fetch = (...fetchParams) => fetch(...fetchParams);
- private paramsSerializer?: ParamsSerializer;
- private responseParser?: ResponseParser;
- private onRequest?: RequestInterceptor;
- private onResponse?: ResponseInterceptor;
- private onError?: ErrorInterceptor;
-
- private baseRequestParams: RequestParams = {
- credentials: "same-origin",
- headers: {},
- redirect: "follow",
- referrerPolicy: "no-referrer",
- };
-
- constructor({
- baseUrl,
- customFetch,
- paramsSerializer,
- responseParser,
- onRequest,
- onResponse,
- onError,
- ...baseRequestParams
- }: ApiConfig = {}) {
- if (typeof baseUrl === "string") {
- this.baseUrl = baseUrl;
- }
-
- this.customFetch = customFetch || this.customFetch;
- this.paramsSerializer = paramsSerializer;
- this.responseParser = responseParser;
- this.onRequest = onRequest;
- this.onResponse = onResponse;
- this.onError = onError;
- this.baseRequestParams = this.mergeRequestParams(
- this.baseRequestParams,
- baseRequestParams,
- );
- }
-
- protected encodeQueryParam(key: string, value: any) {
- const encodedKey = encodeURIComponent(key);
- return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
- }
-
- protected addQueryParam(query: QueryParamsType, key: string) {
- return this.encodeQueryParam(key, query[key]);
- }
-
- protected addArrayQueryParam(query: QueryParamsType, key: string) {
- const value = query[key];
- return value.map((v: any) => this.encodeQueryParam(key, v)).join("&");
- }
-
- protected toQueryString(rawQuery?: QueryParamsType): string {
- const query = rawQuery || {};
-
- if (this.paramsSerializer) {
- return this.paramsSerializer(query);
- }
-
- const keys = Object.keys(query).filter(
- (key) => "undefined" !== typeof query[key],
- );
- return keys
- .map((key) =>
- Array.isArray(query[key])
- ? this.addArrayQueryParam(query, key)
- : this.addQueryParam(query, key),
- )
- .join("&");
- }
-
- protected addQueryParams(rawQuery?: QueryParamsType): string {
- const queryString = this.toQueryString(rawQuery);
- return queryString ? `?${queryString}` : "";
- }
-
- protected buildRequestUrl(
- baseUrl: string | undefined,
- path: string,
- query?: QueryParamsType,
- ): string {
- return `${baseUrl || this.baseUrl || ""}${path}${this.addQueryParams(query)}`;
- }
-
- protected createRequestContext(
- request: FullRequestParams,
- retryCount: number,
- retry: () => Promise,
- ): RequestContext {
- return {
- url: this.buildRequestUrl(request.baseUrl, request.path, request.query),
- request,
- retryCount,
- retry,
- };
- }
-
- protected updateRequestContext(
- context: RequestContext,
- request: FullRequestParams,
- ) {
- context.request = request;
- context.url = this.buildRequestUrl(
- request.baseUrl,
- request.path,
- request.query,
- );
- }
-
- protected mergeHeaders(
- ...headers: Array
- ): HeadersInit {
- const mergedHeaders = new Headers();
-
- headers.forEach((headers) => {
- if (!headers) {
- return;
- }
-
- new Headers(headers).forEach((value, key) =>
- mergedHeaders.set(key, value),
- );
- });
-
- return Object.fromEntries(mergedHeaders.entries());
- }
-
- protected mergeRequestParams>(
- params1: T,
- params2?: Partial,
- ): T {
- return {
- ...params1,
- ...(params2 || {}),
- headers: this.mergeHeaders(params1.headers, params2?.headers),
- } as T;
- }
-
- protected createAbortSignal = (
- cancelToken: CancelToken,
- ): AbortSignal | undefined => {
- if (this.abortControllers.has(cancelToken)) {
- const abortController = this.abortControllers.get(cancelToken);
- if (abortController) {
- return abortController.signal;
- }
- return void 0;
- }
-
- const abortController = new AbortController();
- this.abortControllers.set(cancelToken, abortController);
- return abortController.signal;
- };
-
- protected createRequestSignal = (
- signal?: AbortSignal | null,
- cancelToken?: CancelToken,
- timeout?: number,
- ): { signal: AbortSignal | null; cleanup: () => void } => {
- const signals: AbortSignal[] = [];
- let timeoutId: ReturnType | undefined;
-
- if (signal) {
- signals.push(signal);
- }
-
- if (cancelToken) {
- const cancelSignal = this.createAbortSignal(cancelToken);
- if (cancelSignal) {
- signals.push(cancelSignal);
- }
- }
-
- if (typeof timeout === "number" && timeout > 0) {
- const timeoutController = new AbortController();
- timeoutId = setTimeout(() => timeoutController.abort(), timeout);
- signals.push(timeoutController.signal);
- }
-
- const cleanupTimeout = () => {
- if (timeoutId) {
- clearTimeout(timeoutId);
- }
- };
-
- if (signals.length === 0) {
- return { signal: null, cleanup: cleanupTimeout };
- }
-
- if (signals.length === 1) {
- return { signal: signals[0] || null, cleanup: cleanupTimeout };
- }
-
- const abortController = new AbortController();
- const abortRequest = () => abortController.abort();
-
- signals.forEach((signal) => {
- if (signal.aborted) {
- abortController.abort();
- } else {
- signal.addEventListener("abort", abortRequest, { once: true });
- }
- });
-
- return {
- signal: abortController.signal,
- cleanup: () => {
- cleanupTimeout();
- signals.forEach((signal) =>
- signal.removeEventListener("abort", abortRequest),
- );
- },
- };
- };
-
- public abortRequest = (cancelToken: CancelToken) => {
- const abortController = this.abortControllers.get(cancelToken);
-
- if (abortController) {
- abortController.abort();
- this.abortControllers.delete(cancelToken);
- }
- };
-
- private contentFormatters: Record any> = {
- [ContentType.Json]: (input: any) =>
- input !== null && (typeof input === "object" || typeof input === "string")
- ? JSON.stringify(input)
- : input,
- [ContentType.JsonApi]: (input: any) =>
- input !== null && (typeof input === "object" || typeof input === "string")
- ? JSON.stringify(input)
- : input,
- [ContentType.Text]: (input: any) =>
- input !== null && typeof input !== "string"
- ? JSON.stringify(input)
- : input,
- [ContentType.FormData]: (input: any) => {
- if (input instanceof FormData) {
- return input;
- }
-
- return Object.keys(input || {}).reduce((formData, key) => {
- const property = input[key];
- formData.append(
- key,
- property instanceof Blob
- ? property
- : typeof property === "object" && property !== null
- ? JSON.stringify(property)
- : `${property}`,
- );
- return formData;
- }, new FormData());
- },
- [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
- };
-
- protected parseResponse = async (
- response: Response,
- responseFormat?: ResponseFormat,
- ): Promise> => {
- const parsedResponse = response as HttpResponse;
- parsedResponse.data = null as unknown as T;
- parsedResponse.error = null as unknown as E;
-
- if (!responseFormat && !this.responseParser) {
- return parsedResponse;
- }
-
- const responseToParse = response.clone();
-
- await Promise.resolve(
- this.responseParser
- ? this.responseParser(responseToParse, responseFormat)
- : responseToParse[responseFormat as ResponseFormat](),
- )
- .then((data) => {
- if (parsedResponse.ok) {
- parsedResponse.data = data as T;
- } else {
- parsedResponse.error = data as E;
- }
- })
- .catch((error) => {
- if (parsedResponse.ok) {
- throw error;
- }
-
- parsedResponse.error = error as E;
- });
-
- return parsedResponse;
- };
-
- public request = async (
- requestParams: FullRequestParams,
- ) => {
- return this.requestWithRetry(requestParams, 0);
- };
-
- private requestWithRetry = async (
- requestParams: FullRequestParams,
- retryCount: number,
- ): Promise => {
- let request = this.mergeRequestParams(
- this.baseRequestParams,
- requestParams,
- ) as FullRequestParams;
- request.baseUrl = request.baseUrl || this.baseUrl;
- request.secure =
- typeof request.secure === "boolean"
- ? request.secure
- : this.baseRequestParams.secure;
-
- const context = this.createRequestContext(request, retryCount, () =>
- this.requestWithRetry(requestParams, retryCount + 1),
- );
-
- let cleanupSignal = () => {};
- let cancelToken: CancelToken | undefined;
-
- const cleanupRequest = () => {
- cleanupSignal();
- if (cancelToken) {
- this.abortControllers.delete(cancelToken);
- }
- };
-
- try {
- if (this.onRequest) {
- request = await this.onRequest(request, context);
- this.updateRequestContext(context, request);
- }
-
- const {
- body,
- secure,
- path,
- type,
- query,
- format,
- baseUrl,
- cancelToken: requestCancelToken,
- timeout,
- ...params
- } = request;
-
- cancelToken = requestCancelToken;
- const { signal, cleanup } = this.createRequestSignal(
- params.signal,
- cancelToken,
- timeout,
- );
- cleanupSignal = cleanup;
-
- const payloadFormatter = this.contentFormatters[type || ContentType.Json];
- const responseFormat = format;
- const response = await this.customFetch(context.url, {
- ...params,
- headers: this.mergeHeaders(
- params.headers,
- type && type !== ContentType.FormData
- ? { "Content-Type": type }
- : undefined,
- ),
- signal,
- body:
- typeof body === "undefined" || body === null
- ? null
- : payloadFormatter(body),
- });
-
- const parsedResponse = await this.parseResponse(
- response,
- responseFormat,
- );
-
- if (!parsedResponse.ok) {
- throw new ApiError(
- parsedResponse,
- request,
- parsedResponse.error || parsedResponse.data,
- parsedResponse.error,
- );
- }
-
- const finalResponse = this.onResponse
- ? await this.onResponse(parsedResponse, context)
- : parsedResponse;
-
- return finalResponse.data;
- } catch (error) {
- cleanupRequest();
-
- if (this.onError) {
- return this.onError(error, context);
- }
-
- throw error;
- } finally {
- cleanupRequest();
- }
- };
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/index.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/index.ts
deleted file mode 100644
index 7e3c3c7..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/index.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-export { createApiClient } from "./create-api-client";
-export type { ApiOperation, ApiTree, BoundApi } from "./create-api-client";
-export type * from "./data-contracts";
-export { ApiError, ContentType, HttpClient } from "./http-client";
-export type {
- ApiConfig,
- ApiRequestClient,
- ErrorInterceptor,
- FullRequestParams,
- HttpResponse,
- ParamsSerializer,
- QueryParamsType,
- RequestContext,
- RequestInterceptor,
- RequestParams,
- ResponseFormat,
- ResponseInterceptor,
- ResponseParser,
-} from "./http-client";
-export * from "./operations";
-export * as operations from "./operations";
-export { operationsTree } from "./operations-tree";
-export type { OperationsTree } from "./operations-tree";
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations-tree.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations-tree.ts
deleted file mode 100644
index 649fdaf..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations-tree.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import { simpleAuthLogin } from "./operations/simple-auth-login";
-import { simpleAuthLogout } from "./operations/simple-auth-logout";
-import { simpleAuthRefresh } from "./operations/simple-auth-refresh";
-import { simpleCategoriesGet } from "./operations/simple-categories-get";
-import { simpleCategoriesList } from "./operations/simple-categories-list";
-import { simpleHealthHealth } from "./operations/simple-health-health";
-import { simpleOrdersCancel } from "./operations/simple-orders-cancel";
-import { simpleOrdersCreate } from "./operations/simple-orders-create";
-import { simpleOrdersGet } from "./operations/simple-orders-get";
-import { simpleOrdersList } from "./operations/simple-orders-list";
-import { simpleProductsCreate } from "./operations/simple-products-create";
-import { simpleProductsGet } from "./operations/simple-products-get";
-import { simpleProductsList } from "./operations/simple-products-list";
-import { simpleProductsRemove } from "./operations/simple-products-remove";
-import { simpleProductsUpdate } from "./operations/simple-products-update";
-import { simpleTestingChangeRole } from "./operations/simple-testing-change-role";
-import { simpleTestingReset } from "./operations/simple-testing-reset";
-import { simpleTestingScenarios } from "./operations/simple-testing-scenarios";
-import { simpleTestingSeed } from "./operations/simple-testing-seed";
-import { simpleUsersMe } from "./operations/simple-users-me";
-
-export const operationsTree = {
- health: {
- simpleHealthHealth: simpleHealthHealth,
- },
- auth: {
- simpleAuthLogin: simpleAuthLogin,
- simpleAuthRefresh: simpleAuthRefresh,
- simpleAuthLogout: simpleAuthLogout,
- },
- users: {
- simpleUsersMe: simpleUsersMe,
- },
- products: {
- simpleProductsList: simpleProductsList,
- simpleProductsCreate: simpleProductsCreate,
- simpleProductsGet: simpleProductsGet,
- simpleProductsUpdate: simpleProductsUpdate,
- simpleProductsRemove: simpleProductsRemove,
- },
- categories: {
- simpleCategoriesList: simpleCategoriesList,
- simpleCategoriesGet: simpleCategoriesGet,
- },
- orders: {
- simpleOrdersList: simpleOrdersList,
- simpleOrdersCreate: simpleOrdersCreate,
- simpleOrdersGet: simpleOrdersGet,
- simpleOrdersCancel: simpleOrdersCancel,
- },
- testing: {
- simpleTestingScenarios: simpleTestingScenarios,
- simpleTestingReset: simpleTestingReset,
- simpleTestingSeed: simpleTestingSeed,
- simpleTestingChangeRole: simpleTestingChangeRole,
- },
-} as const;
-
-export type OperationsTree = typeof operationsTree;
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/index.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/index.ts
deleted file mode 100644
index dfd52ee..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/index.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-export { simpleAuthLogin } from "./simple-auth-login";
-export { simpleAuthLogout } from "./simple-auth-logout";
-export { simpleAuthRefresh } from "./simple-auth-refresh";
-export { simpleCategoriesGet } from "./simple-categories-get";
-export { simpleCategoriesList } from "./simple-categories-list";
-export { simpleHealthHealth } from "./simple-health-health";
-export { simpleOrdersCancel } from "./simple-orders-cancel";
-export { simpleOrdersCreate } from "./simple-orders-create";
-export { simpleOrdersGet } from "./simple-orders-get";
-export { simpleOrdersList } from "./simple-orders-list";
-export { simpleProductsCreate } from "./simple-products-create";
-export { simpleProductsGet } from "./simple-products-get";
-export { simpleProductsList } from "./simple-products-list";
-export { simpleProductsRemove } from "./simple-products-remove";
-export { simpleProductsUpdate } from "./simple-products-update";
-export { simpleTestingChangeRole } from "./simple-testing-change-role";
-export { simpleTestingReset } from "./simple-testing-reset";
-export { simpleTestingScenarios } from "./simple-testing-scenarios";
-export { simpleTestingSeed } from "./simple-testing-seed";
-export { simpleUsersMe } from "./simple-users-me";
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-login.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-login.ts
deleted file mode 100644
index d72bf98..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-login.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- JwtAuthResponseDto,
- LoginDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Auth
- * @name SimpleAuthLogin
- * @summary Login and receive JWT access/refresh tokens
- * @request POST:/api/v1/auth/login
- */
-export const simpleAuthLogin = (
- http: ApiRequestClient,
- data: LoginDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/auth/login`,
- method: "POST",
- body: data,
- type: ContentType.Json,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-logout.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-logout.ts
deleted file mode 100644
index 9fdcbf6..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-logout.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type { ErrorResponseDto, RefreshTokenDto } from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Auth
- * @name SimpleAuthLogout
- * @summary Revoke a refresh token; the operation is idempotent
- * @request POST:/api/v1/auth/logout
- */
-export const simpleAuthLogout = (
- http: ApiRequestClient,
- data: RefreshTokenDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/auth/logout`,
- method: "POST",
- body: data,
- type: ContentType.Json,
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-refresh.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-refresh.ts
deleted file mode 100644
index 470569a..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-auth-refresh.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- JwtAuthResponseDto,
- RefreshTokenDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Auth
- * @name SimpleAuthRefresh
- * @summary Rotate a refresh token and issue a new token pair
- * @request POST:/api/v1/auth/refresh
- */
-export const simpleAuthRefresh = (
- http: ApiRequestClient,
- data: RefreshTokenDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/auth/refresh`,
- method: "POST",
- body: data,
- type: ContentType.Json,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-categories-get.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-categories-get.ts
deleted file mode 100644
index 96b52d4..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-categories-get.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- CategoryResponseDto,
- ErrorResponseDto,
- SimpleCategoriesGetParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Categories
- * @name SimpleCategoriesGet
- * @summary Get one category
- * @request GET:/api/v1/categories/{id}
- */
-export const simpleCategoriesGet = (
- http: ApiRequestClient,
- { id, ...query }: SimpleCategoriesGetParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/categories/${id}`,
- method: "GET",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-categories-list.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-categories-list.ts
deleted file mode 100644
index 583e759..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-categories-list.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type { CategoriesResponseDto } from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Categories
- * @name SimpleCategoriesList
- * @summary List product categories
- * @request GET:/api/v1/categories
- */
-export const simpleCategoriesList = (
- http: ApiRequestClient,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/categories`,
- method: "GET",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-health-health.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-health-health.ts
deleted file mode 100644
index fe3dcef..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-health-health.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type { HealthResponseDto } from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Health
- * @name SimpleHealthHealth
- * @summary Check Simple API availability
- * @request GET:/api/v1/health
- */
-export const simpleHealthHealth = (
- http: ApiRequestClient,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/health`,
- method: "GET",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-cancel.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-cancel.ts
deleted file mode 100644
index f335867..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-cancel.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- OrderResponseDto,
- SimpleOrdersCancelParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Orders
- * @name SimpleOrdersCancel
- * @summary Cancel an order if its state permits the transition
- * @request POST:/api/v1/orders/{id}/cancel
- * @secure
- */
-export const simpleOrdersCancel = (
- http: ApiRequestClient,
- { id, ...query }: SimpleOrdersCancelParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/orders/${id}/cancel`,
- method: "POST",
- secure: true,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-create.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-create.ts
deleted file mode 100644
index d8160c8..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-create.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- CreateOrderDto,
- ErrorResponseDto,
- OrderResponseDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Orders
- * @name SimpleOrdersCreate
- * @summary Create an order and validate product stock
- * @request POST:/api/v1/orders
- * @secure
- */
-export const simpleOrdersCreate = (
- http: ApiRequestClient,
- data: CreateOrderDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/orders`,
- method: "POST",
- body: data,
- secure: true,
- type: ContentType.Json,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-get.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-get.ts
deleted file mode 100644
index 6b8afe0..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-get.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- OrderResponseDto,
- SimpleOrdersGetParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Orders
- * @name SimpleOrdersGet
- * @summary Get one visible order
- * @request GET:/api/v1/orders/{id}
- * @secure
- */
-export const simpleOrdersGet = (
- http: ApiRequestClient,
- { id, ...query }: SimpleOrdersGetParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/orders/${id}`,
- method: "GET",
- secure: true,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-list.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-list.ts
deleted file mode 100644
index 6f58ef0..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-orders-list.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- OrdersResponseDto,
- SimpleOrdersListParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Orders
- * @name SimpleOrdersList
- * @summary List orders visible to the current user
- * @request GET:/api/v1/orders
- * @secure
- */
-export const simpleOrdersList = (
- http: ApiRequestClient,
- query: SimpleOrdersListParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/orders`,
- method: "GET",
- query: query,
- secure: true,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-create.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-create.ts
deleted file mode 100644
index f3b0e8f..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-create.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- CreateProductDto,
- ErrorResponseDto,
- ProductResponseDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Products
- * @name SimpleProductsCreate
- * @summary Create a product as an administrator
- * @request POST:/api/v1/products
- * @secure
- */
-export const simpleProductsCreate = (
- http: ApiRequestClient,
- data: CreateProductDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/products`,
- method: "POST",
- body: data,
- secure: true,
- type: ContentType.Json,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-get.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-get.ts
deleted file mode 100644
index 82846b5..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-get.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- ProductResponseDto,
- SimpleProductsGetParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Products
- * @name SimpleProductsGet
- * @summary Get one product with ETag support
- * @request GET:/api/v1/products/{id}
- */
-export const simpleProductsGet = (
- http: ApiRequestClient,
- { id, ...query }: SimpleProductsGetParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/products/${id}`,
- method: "GET",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-list.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-list.ts
deleted file mode 100644
index d56ac7c..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-list.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- ProductsResponseDto,
- SimpleProductsListParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Products
- * @name SimpleProductsList
- * @summary List products using offset pagination, filters and sorting
- * @request GET:/api/v1/products
- */
-export const simpleProductsList = (
- http: ApiRequestClient,
- query: SimpleProductsListParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/products`,
- method: "GET",
- query: query,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-remove.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-remove.ts
deleted file mode 100644
index 8599013..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-remove.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- MutationResponseDto,
- SimpleProductsRemoveParams,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Products
- * @name SimpleProductsRemove
- * @summary Delete a product as an administrator
- * @request DELETE:/api/v1/products/{id}
- * @secure
- */
-export const simpleProductsRemove = (
- http: ApiRequestClient,
- { id, ...query }: SimpleProductsRemoveParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/products/${id}`,
- method: "DELETE",
- secure: true,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-update.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-update.ts
deleted file mode 100644
index 0c00587..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-products-update.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- ProductResponseDto,
- SimpleProductsUpdateParams,
- UpdateProductDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Products
- * @name SimpleProductsUpdate
- * @summary Update a product using optimistic locking
- * @request PATCH:/api/v1/products/{id}
- * @secure
- */
-export const simpleProductsUpdate = (
- http: ApiRequestClient,
- { id, ...query }: SimpleProductsUpdateParams,
- data: UpdateProductDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/products/${id}`,
- method: "PATCH",
- body: data,
- secure: true,
- type: ContentType.Json,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-change-role.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-change-role.ts
deleted file mode 100644
index e2d108e..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-change-role.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ChangeSimpleRoleDto,
- ErrorResponseDto,
- SimpleTestingChangeRoleParams,
- SimpleUserResponseDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-import { ContentType } from "../http-client";
-
-/**
- * No description
- *
- * @tags Testing
- * @name SimpleTestingChangeRole
- * @summary Change a user role to exercise dynamic access control
- * @request POST:/api/v1/testing/users/{userId}/role
- */
-export const simpleTestingChangeRole = (
- http: ApiRequestClient,
- { userId, ...query }: SimpleTestingChangeRoleParams,
- data: ChangeSimpleRoleDto,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/testing/users/${userId}/role`,
- method: "POST",
- body: data,
- type: ContentType.Json,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-reset.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-reset.ts
deleted file mode 100644
index 3aa993c..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-reset.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type { TestingActionResponseDto } from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Testing
- * @name SimpleTestingReset
- * @summary Reset all Simple API state and token revocations
- * @request POST:/api/v1/testing/reset
- */
-export const simpleTestingReset = (
- http: ApiRequestClient,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/testing/reset`,
- method: "POST",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-scenarios.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-scenarios.ts
deleted file mode 100644
index 671cf9d..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-scenarios.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type { ScenariosResponseDto } from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Testing
- * @name SimpleTestingScenarios
- * @summary List deterministic X-Demo-Scenario values
- * @request GET:/api/v1/testing/scenarios
- */
-export const simpleTestingScenarios = (
- http: ApiRequestClient,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/testing/scenarios`,
- method: "GET",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-seed.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-seed.ts
deleted file mode 100644
index 42ae9f4..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-testing-seed.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- SimpleTestingSeedParams,
- TestingActionResponseDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Testing
- * @name SimpleTestingSeed
- * @summary Select a small or large deterministic dataset
- * @request POST:/api/v1/testing/seed/{preset}
- */
-export const simpleTestingSeed = (
- http: ApiRequestClient,
- { preset, ...query }: SimpleTestingSeedParams,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/testing/seed/${preset}`,
- method: "POST",
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-users-me.ts b/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-users-me.ts
deleted file mode 100644
index 04401f8..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/generated/operations/simple-users-me.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-/* eslint-disable */
-/* tslint:disable */
-// @ts-nocheck
-
-/*
- * ----------------------------------------------------------------------
- * ## АВТОМАТИЧЕСКИ СГЕНЕРИРОВАННЫЙ ФАЙЛ ##
- * ## ##
- * ## Не редактируйте вручную: изменения будут перезаписаны. ##
- * ## Для изменений перегенерируйте клиент. ##
- * ## ##
- * ## Генератор: @gromlab/api-codegen ##
- * ## Репозиторий: https://gromlab.ru/gromov/api-codegen ##
- * ----------------------------------------------------------------------
- */
-
-import type {
- ErrorResponseDto,
- SimpleUserResponseDto,
-} from "../data-contracts";
-import type { ApiRequestClient, RequestParams } from "../http-client";
-
-/**
- * No description
- *
- * @tags Users
- * @name SimpleUsersMe
- * @summary Get the authenticated user
- * @request GET:/api/v1/users/me
- * @secure
- */
-export const simpleUsersMe = (
- http: ApiRequestClient,
- requestParams: RequestParams = {},
-) =>
- http.request({
- path: `/api/v1/users/me`,
- method: "GET",
- secure: true,
- format: "json",
- ...requestParams,
- });
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/index.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/index.ts
deleted file mode 100644
index 5fb47a0..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/index.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-'use client'
-
-export { getCategoriesKey, useGetCategories } from './use-get-categories.hook'
-export { getDemoScenariosKey, useGetDemoScenarios } from './use-get-demo-scenarios.hook'
-export { getHealthKey, useGetHealth } from './use-get-health.hook'
-export { getOrdersKey, useGetOrders } from './use-get-orders.hook'
-export { getProductKey, useGetProduct } from './use-get-product.hook'
-export { getProductsKey, useGetProducts } from './use-get-products.hook'
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/lib/create-query-string.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/lib/create-query-string.ts
deleted file mode 100644
index 83dab1b..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/lib/create-query-string.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Значение query-параметра, поддерживаемое REST-ключами.
- */
-type QueryValue = boolean | number | string | null | undefined
-
-/**
- * Собирает query string, пропуская отсутствующие значения.
- */
-export const createQueryString = (query: Record): string => {
- const searchParams = new URLSearchParams()
-
- Object.entries(query).forEach(([key, value]) => {
- if (value === null || value === undefined || value === '') {
- return
- }
-
- searchParams.set(key, String(value))
- })
-
- const search = searchParams.toString()
-
- return search ? `?${search}` : ''
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-categories.hook.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-categories.hook.ts
deleted file mode 100644
index d9d9657..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-categories.hook.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import useSWR from 'swr'
-import type { SWRConfiguration, SWRResponse } from 'swr'
-
-import { simpleHttpClient } from '../client'
-import type { CategoriesResponseDto } from '../generated'
-import { simpleCategoriesList } from '../generated/operations/simple-categories-list'
-
-/**
- * Возвращает SWR-ключ списка категорий.
- */
-export const getCategoriesKey = (): readonly ['simple-rest-api', '/api/v1/categories'] => {
- return ['simple-rest-api', '/api/v1/categories'] as const
-}
-
-/**
- * Получает категории каталога без доменной интерпретации DTO.
- */
-export const useGetCategories = (
- config?: SWRConfiguration
-): SWRResponse => {
- return useSWR(
- getCategoriesKey(),
- () => simpleCategoriesList(simpleHttpClient),
- config
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-demo-scenarios.hook.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-demo-scenarios.hook.ts
deleted file mode 100644
index aa2a91a..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-demo-scenarios.hook.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import useSWR from 'swr'
-import type { SWRConfiguration, SWRResponse } from 'swr'
-
-import { simpleHttpClient } from '../client'
-import type { ScenariosResponseDto } from '../generated'
-import { simpleTestingScenarios } from '../generated/operations/simple-testing-scenarios'
-
-/**
- * Возвращает SWR-ключ справочника demo-сценариев.
- */
-export const getDemoScenariosKey = (): readonly [
- 'simple-rest-api',
- '/api/v1/testing/scenarios'
-] => {
- return ['simple-rest-api', '/api/v1/testing/scenarios'] as const
-}
-
-/**
- * Получает описания управляемых сценариев fixture-бэкенда.
- */
-export const useGetDemoScenarios = (
- config?: SWRConfiguration
-): SWRResponse => {
- return useSWR(
- getDemoScenariosKey(),
- () => simpleTestingScenarios(simpleHttpClient),
- config
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-health.hook.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-health.hook.ts
deleted file mode 100644
index bd29066..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-health.hook.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import useSWR from 'swr'
-import type { SWRConfiguration, SWRResponse } from 'swr'
-
-import { simpleHttpClient } from '../client'
-import type { HealthResponseDto } from '../generated'
-import { simpleHealthHealth } from '../generated/operations/simple-health-health'
-
-/**
- * Возвращает SWR-ключ проверки доступности Simple API.
- */
-export const getHealthKey = (): readonly ['simple-rest-api', '/api/v1/health'] => {
- return ['simple-rest-api', '/api/v1/health'] as const
-}
-
-/**
- * Получает технический health-снимок Simple API.
- */
-export const useGetHealth = (
- config?: SWRConfiguration
-): SWRResponse => {
- return useSWR(
- getHealthKey(),
- () => simpleHealthHealth(simpleHttpClient),
- config
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-orders.hook.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-orders.hook.ts
deleted file mode 100644
index 52a4016..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-orders.hook.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import useSWR from 'swr'
-import type { SWRConfiguration, SWRResponse } from 'swr'
-
-import { simpleHttpClient } from '../client'
-import type { OrdersResponseDto } from '../generated'
-import { simpleOrdersList } from '../generated/operations/simple-orders-list'
-import type { SimpleOrdersQuery } from '../types'
-import { toGeneratedOrdersQuery } from '../types/to-generated-query'
-import { createQueryString } from './lib/create-query-string'
-
-/**
- * Возвращает SWR-ключ заказов или блокирует защищённый запрос.
- */
-export const getOrdersKey = (
- params?: SimpleOrdersQuery | null
-): readonly ['simple-rest-api', string] | null => {
- if (params === null) {
- return null
- }
-
- const query = params ?? {}
- const path = `/api/v1/orders${createQueryString({ page: query.page, limit: query.limit })}`
-
- return ['simple-rest-api', path] as const
-}
-
-/**
- * Получает доступные текущему пользователю заказы.
- */
-export const useGetOrders = (
- params?: SimpleOrdersQuery | null,
- config?: SWRConfiguration
-): SWRResponse => {
- const query = toGeneratedOrdersQuery(params ?? {})
-
- return useSWR(
- getOrdersKey(params),
- () => simpleOrdersList(simpleHttpClient, query),
- config
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-product.hook.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-product.hook.ts
deleted file mode 100644
index b5a91ef..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-product.hook.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import useSWR from 'swr'
-import type { SWRConfiguration, SWRResponse } from 'swr'
-
-import { simpleHttpClient } from '../client'
-import type { ProductResponseDto, SimpleProductsGetParams } from '../generated'
-import { simpleProductsGet } from '../generated/operations/simple-products-get'
-
-/**
- * Возвращает SWR-ключ продукта или блокирует запрос без id.
- */
-export const getProductKey = (
- params?: SimpleProductsGetParams | null
-): readonly ['simple-rest-api', string] | null => {
- if (!params?.id) {
- return null
- }
-
- return ['simple-rest-api', `/api/v1/products/${params.id}`] as const
-}
-
-/**
- * Получает продукт по идентификатору с отложенным запуском.
- */
-export const useGetProduct = (
- params?: SimpleProductsGetParams | null,
- config?: SWRConfiguration
-): SWRResponse => {
- const requestParams = params?.id ? params : { id: '' }
-
- return useSWR(
- getProductKey(params),
- () => simpleProductsGet(simpleHttpClient, requestParams),
- config
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-products.hook.ts b/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-products.hook.ts
deleted file mode 100644
index 1cfab34..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/hooks/use-get-products.hook.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import useSWR from 'swr'
-import type { SWRConfiguration, SWRResponse } from 'swr'
-
-import { simpleHttpClient } from '../client'
-import type { ProductsResponseDto } from '../generated'
-import { simpleProductsList } from '../generated/operations/simple-products-list'
-import type { SimpleProductsQuery } from '../types'
-import { toGeneratedProductsQuery } from '../types/to-generated-query'
-import { createQueryString } from './lib/create-query-string'
-
-/**
- * Возвращает endpoint query для выдачи продуктов.
- */
-const getProductsPath = (params: SimpleProductsQuery): string => {
- return `/api/v1/products${createQueryString({
- page: params.page,
- limit: params.limit,
- search: params.search,
- categoryId: params.categoryId,
- sort: params.sort
- })}`
-}
-
-/**
- * Возвращает SWR-ключ текущей фильтрованной выдачи продуктов.
- */
-export const getProductsKey = (
- params: SimpleProductsQuery = {}
-): readonly ['simple-rest-api', string] => {
- return ['simple-rest-api', getProductsPath(params)] as const
-}
-
-/**
- * Получает одну страницу продуктов без доменной интерпретации DTO.
- */
-export const useGetProducts = (
- params: SimpleProductsQuery = {},
- config?: SWRConfiguration
-): SWRResponse => {
- const query = toGeneratedProductsQuery(params)
-
- return useSWR(
- getProductsKey(params),
- () => simpleProductsList(simpleHttpClient, query),
- config
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/index.ts b/examples/demo-frontend/src/infra/simple-rest-api/index.ts
deleted file mode 100644
index c3c5122..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/index.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-export { useSimpleRestApiCache } from './cache'
-export { createSimpleAuthSessionRequest } from './auth-session-request'
-export { simpleHttpClient } from './client'
-export { readSimpleApiFailure } from './errors'
-export type {
- SimpleCategoryDto,
- SimpleOrderDto,
- SimpleProductDto,
- SimpleUserDto
-} from './generated'
-export * from './hooks'
-export {
- DEMO_SCENARIOS,
- getDemoFixtureChange,
- getDemoScenario,
- getServerDemoFixtureChange,
- getServerDemoScenario,
- publishDemoFixtureChange,
- setDemoScenario,
- subscribeDemoFixtureChange,
- subscribeDemoScenario
-} from './request-context'
-export { simpleRestApi } from './rest-api'
-export { SimpleRestApiProvider } from './simple-rest-api.provider'
-export type { DemoScenario, SimpleOrdersQuery, SimpleProductsQuery } from './types'
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/refresh-access-token.ts b/examples/demo-frontend/src/infra/simple-rest-api/refresh-access-token.ts
deleted file mode 100644
index d74cca9..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/refresh-access-token.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import {
- clearSimpleAuthSession,
- getSimpleAuthSession,
- rotateSimpleAuthSession,
- withSimpleAuthRefreshLock
-} from '@/infra/simple-auth-session'
-import type { SimpleAuthSessionIdentity } from '@/infra/simple-auth-session'
-
-import { SIMPLE_REST_API_BASE_URL, SIMPLE_REST_API_TIMEOUT_MS } from './config/simple-rest-api.config'
-import { ApiError, HttpClient } from './generated'
-import { simpleAuthRefresh } from './generated/operations/simple-auth-refresh'
-import { getDemoScenario } from './request-context'
-
-const refreshPromisesByRevision = new Map>()
-
-/**
- * Возвращает revision, уже созданную конкурентным refresh той же session.
- */
-const getConcurrentRevision = (expected: SimpleAuthSessionIdentity): string | null => {
- const currentSession = getSimpleAuthSession()
-
- if (
- currentSession !== null &&
- currentSession.sessionId === expected.sessionId &&
- currentSession.revision !== expected.revision
- ) {
- return currentSession.revision
- }
-
- return null
-}
-
-/**
- * Выполняет CAS-ротацию refresh token через транспорт без retry-цикла.
- */
-const performTokenRefresh = async (expected: SimpleAuthSessionIdentity): Promise => {
- return withSimpleAuthRefreshLock(async () => {
- const session = getSimpleAuthSession()
-
- if (session === null || session.sessionId !== expected.sessionId) {
- return null
- }
-
- if (session.revision !== expected.revision) {
- return session.revision
- }
-
- const refreshHttpClient = new HttpClient({
- baseUrl: SIMPLE_REST_API_BASE_URL,
- timeout: SIMPLE_REST_API_TIMEOUT_MS,
- onRequest: (params) => {
- const headers = new Headers(params.headers)
- headers.set('X-Demo-Scenario', getDemoScenario())
-
- return { ...params, headers }
- }
- })
-
- try {
- const response = await simpleAuthRefresh(refreshHttpClient, {
- refreshToken: session.tokens.refreshToken
- })
- const rotatedSession = await rotateSimpleAuthSession(expected, response.data.tokens)
-
- return rotatedSession?.revision ?? getConcurrentRevision(expected)
- } catch (error) {
- if (error instanceof ApiError && error.status === 401) {
- const concurrentRevision = getConcurrentRevision(expected)
-
- if (concurrentRevision !== null) {
- return concurrentRevision
- }
-
- await clearSimpleAuthSession(expected)
- }
-
- throw error
- }
- })
-}
-
-/**
- * Дедуплицирует refresh только внутри одной исходной session revision.
- */
-export const refreshSimpleAccessToken = (
- expected: SimpleAuthSessionIdentity
-): Promise => {
- const existingPromise = refreshPromisesByRevision.get(expected.revision)
-
- if (existingPromise !== undefined) {
- return existingPromise
- }
-
- const refreshPromise = performTokenRefresh(expected).finally(() => {
- refreshPromisesByRevision.delete(expected.revision)
- })
-
- refreshPromisesByRevision.set(expected.revision, refreshPromise)
-
- return refreshPromise
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/request-context.ts b/examples/demo-frontend/src/infra/simple-rest-api/request-context.ts
deleted file mode 100644
index 06b060d..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/request-context.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-import { readJsonStorage, writeJsonStorage } from '@/infra/browser-storage'
-import { isOneOf, isString } from '@/shared/lib/value-predicates'
-
-import type { DemoScenario, DemoScenarioListener } from './types/demo-scenario.type'
-
-const DEMO_SCENARIO_KEY = 'demo-frontend:scenario'
-const DEMO_FIXTURE_CHANGE_KEY = 'demo-frontend:fixture-change'
-const demoScenarioListeners = new Set()
-const demoFixtureListeners = new Set()
-let currentFixtureChange = ''
-
-/**
- * Допустимые значения X-Demo-Scenario для Simple API.
- */
-export const DEMO_SCENARIOS = [
- 'normal',
- 'slow',
- 'timeout',
- 'server-error',
- 'rate-limited',
- 'empty',
- 'expired-auth',
- 'forbidden',
- 'conflict'
-] as const satisfies readonly DemoScenario[]
-
-/**
- * Возвращает сценарий, выбранный для последующих запросов Simple API.
- */
-export const getDemoScenario = (): DemoScenario => {
- const value = readJsonStorage(DEMO_SCENARIO_KEY)
-
- return isOneOf(value, DEMO_SCENARIOS) ? value : 'normal'
-}
-
-/**
- * Возвращает стабильное SSR-значение внешнего store.
- */
-export const getServerDemoScenario = (): DemoScenario => {
- return 'normal'
-}
-
-/**
- * Изменяет X-Demo-Scenario для будущих запросов.
- */
-export const setDemoScenario = (scenario: DemoScenario): void => {
- if (writeJsonStorage(DEMO_SCENARIO_KEY, scenario)) {
- demoScenarioListeners.forEach((listener) => listener())
- }
-}
-
-/**
- * Подписывает UI на local и cross-tab изменения request scenario.
- */
-export const subscribeDemoScenario = (listener: DemoScenarioListener): (() => void) => {
- demoScenarioListeners.add(listener)
-
- /**
- * Передаёт изменение request scenario из другой вкладки.
- */
- const handleStorage = (event: StorageEvent): void => {
- if (event.key === DEMO_SCENARIO_KEY) {
- listener()
- }
- }
-
- if (typeof window !== 'undefined') {
- window.addEventListener('storage', handleStorage)
- }
-
- return () => {
- demoScenarioListeners.delete(listener)
-
- if (typeof window !== 'undefined') {
- window.removeEventListener('storage', handleStorage)
- }
- }
-}
-
-/**
- * Возвращает последний непрозрачный fixture transition token.
- */
-export const getDemoFixtureChange = (): string => {
- if (currentFixtureChange !== '') {
- return currentFixtureChange
- }
-
- const value = readJsonStorage(DEMO_FIXTURE_CHANGE_KEY)
-
- currentFixtureChange = isString(value) ? value : ''
-
- return currentFixtureChange
-}
-
-/**
- * Возвращает стабильный SSR snapshot fixture transitions.
- */
-export const getServerDemoFixtureChange = (): string => {
- return ''
-}
-
-/**
- * Публикует fixture transition всем вкладкам одного origin.
- */
-export const publishDemoFixtureChange = (kind: 'data' | 'role'): string | null => {
- const token = `${kind}:${globalThis.crypto.randomUUID()}`
- const isPersisted = writeJsonStorage(DEMO_FIXTURE_CHANGE_KEY, token)
-
- currentFixtureChange = token
- demoFixtureListeners.forEach((listener) => listener())
-
- return isPersisted ? token : null
-}
-
-/**
- * Подписывает cache и composition owners на fixture transitions.
- */
-export const subscribeDemoFixtureChange = (
- listener: DemoScenarioListener
-): (() => void) => {
- demoFixtureListeners.add(listener)
-
- /**
- * Передаёт fixture transition из другой вкладки.
- */
- const handleStorage = (event: StorageEvent): void => {
- if (event.key === DEMO_FIXTURE_CHANGE_KEY) {
- currentFixtureChange = ''
- listener()
- }
- }
-
- if (typeof window !== 'undefined') {
- window.addEventListener('storage', handleStorage)
- }
-
- return () => {
- demoFixtureListeners.delete(listener)
-
- if (typeof window !== 'undefined') {
- window.removeEventListener('storage', handleStorage)
- }
- }
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/rest-api.ts b/examples/demo-frontend/src/infra/simple-rest-api/rest-api.ts
deleted file mode 100644
index dfea875..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/rest-api.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { createApiClient, operationsTree } from './generated'
-import { simpleHttpClient } from './client'
-
-/**
- * Полный bound-клиент Simple API для submit-сценариев доменных модулей.
- */
-export const simpleRestApi = createApiClient(simpleHttpClient, operationsTree)
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/simple-rest-api.provider.tsx b/examples/demo-frontend/src/infra/simple-rest-api/simple-rest-api.provider.tsx
deleted file mode 100644
index 16c12c9..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/simple-rest-api.provider.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-'use client'
-
-import { useState } from 'react'
-import { SWRConfig } from 'swr'
-import type { Cache } from 'swr'
-
-import type { SimpleRestApiProviderProps } from './types/simple-rest-api-provider-props.type'
-
-/**
- * Владеет отдельным SWR cache на одну application auth scope.
- *
- * Используется для:
- * - изоляции protected DTO между logical sessions
- * - единой lifecycle-aware query policy REST-модуля
- */
-export const SimpleRestApiProvider = (props: SimpleRestApiProviderProps) => {
- const { children, isPaused = false } = props
- const [cache] = useState>(() => new Map())
-
- return (
- cache,
- dedupingInterval: 2_000,
- revalidateOnFocus: false,
- shouldRetryOnError: false,
- isPaused: () => isPaused
- }}
- >
- {children}
-
- )
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/tests/auth-session-request.test.ts b/examples/demo-frontend/src/infra/simple-rest-api/tests/auth-session-request.test.ts
deleted file mode 100644
index 4728ed3..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/tests/auth-session-request.test.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-import { replaceSimpleAuthSession } from '@/infra/simple-auth-session'
-import type { SimpleAuthTokens } from '@/infra/simple-auth-session'
-
-import { createSimpleAuthSessionRequest } from '../auth-session-request'
-import { simpleHttpClient } from '../client'
-import { readSimpleApiFailure } from '../errors'
-
-const TOKENS_A: SimpleAuthTokens = {
- accessToken: 'access-a',
- refreshToken: 'refresh-a',
- expiresIn: 60
-}
-const TOKENS_B: SimpleAuthTokens = {
- accessToken: 'access-b',
- refreshToken: 'refresh-b',
- expiresIn: 60
-}
-
-/**
- * Создаёт browser storage для session-bound transport test.
- */
-const createStorage = (): Storage => {
- const values = new Map()
-
- return {
- get length() {
- return values.size
- },
- clear: () => values.clear(),
- getItem: (key) => values.get(key) ?? null,
- key: (index) => [...values.keys()][index] ?? null,
- removeItem: (key) => values.delete(key),
- setItem: (key, value) => values.set(key, value)
- }
-}
-
-beforeEach(() => {
- vi.stubGlobal('window', {
- localStorage: createStorage(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn()
- })
-})
-
-afterEach(() => {
- vi.unstubAllGlobals()
-})
-
-describe('session-bound REST requests', () => {
- it('stops a protected mutation before fetch when the logical session changed', async () => {
- const fetchMock = vi.fn()
-
- vi.stubGlobal('fetch', fetchMock)
- const firstSession = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(firstSession).not.toBeNull()
-
- if (firstSession === null) {
- return
- }
-
- await replaceSimpleAuthSession(TOKENS_B)
-
- const error = await simpleHttpClient.request({
- path: '/api/v1/orders',
- method: 'POST',
- secure: true,
- ...createSimpleAuthSessionRequest(firstSession.sessionId)
- }).catch((requestError: unknown) => requestError)
-
- expect(readSimpleApiFailure(error).code).toBe('SESSION_CHANGED')
- expect(fetchMock).not.toHaveBeenCalled()
- })
-})
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/tests/query-keys.test.ts b/examples/demo-frontend/src/infra/simple-rest-api/tests/query-keys.test.ts
deleted file mode 100644
index d8d3a45..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/tests/query-keys.test.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import { getOrdersKey } from '../hooks/use-get-orders.hook'
-import { getProductKey } from '../hooks/use-get-product.hook'
-import { getProductsKey } from '../hooks/use-get-products.hook'
-
-describe('Simple REST API SWR keys', () => {
- it('matches the actual filtered products endpoint', () => {
- expect(
- getProductsKey({
- page: 2,
- limit: 12,
- search: 'desk lamp',
- categoryId: 'category-home',
- sort: 'price-asc'
- })
- ).toEqual([
- 'simple-rest-api',
- '/api/v1/products?page=2&limit=12&search=desk+lamp&categoryId=category-home&sort=price-asc'
- ])
- })
-
- it('blocks protected and detail requests until required input is ready', () => {
- expect(getOrdersKey(null)).toBeNull()
- expect(getProductKey(null)).toBeNull()
- })
-})
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/tests/refresh-race.test.ts b/examples/demo-frontend/src/infra/simple-rest-api/tests/refresh-race.test.ts
deleted file mode 100644
index ef161aa..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/tests/refresh-race.test.ts
+++ /dev/null
@@ -1,116 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
-
-import {
- getSimpleAuthSession,
- replaceSimpleAuthSession
-} from '@/infra/simple-auth-session'
-import type { SimpleAuthTokens } from '@/infra/simple-auth-session'
-
-import { refreshSimpleAccessToken } from '../refresh-access-token'
-
-const TOKENS_A: SimpleAuthTokens = {
- accessToken: 'access-a',
- refreshToken: 'refresh-a',
- expiresIn: 60
-}
-
-const TOKENS_B: SimpleAuthTokens = {
- accessToken: 'access-b',
- refreshToken: 'refresh-b',
- expiresIn: 60
-}
-
-const ROTATED_TOKENS_A: SimpleAuthTokens = {
- accessToken: 'access-a-rotated',
- refreshToken: 'refresh-a-rotated',
- expiresIn: 60
-}
-
-/**
- * Создаёт browser storage для transport race tests.
- */
-const createStorage = (): Storage => {
- const values = new Map()
-
- return {
- get length() {
- return values.size
- },
- clear: () => values.clear(),
- getItem: (key) => values.get(key) ?? null,
- key: (index) => [...values.keys()][index] ?? null,
- removeItem: (key) => values.delete(key),
- setItem: (key, value) => values.set(key, value)
- }
-}
-
-beforeEach(() => {
- vi.stubGlobal('window', {
- localStorage: createStorage(),
- addEventListener: vi.fn(),
- removeEventListener: vi.fn()
- })
-})
-
-afterEach(() => {
- vi.unstubAllGlobals()
-})
-
-describe('JWT refresh session races', () => {
- it('does not overwrite a newer login with a delayed refresh response', async () => {
- let resolveFetch!: (response: Response) => void
- const fetchPromise = new Promise((resolveResponse) => {
- resolveFetch = resolveResponse
- })
- vi.stubGlobal('fetch', vi.fn(() => fetchPromise))
- const firstSession = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(firstSession).not.toBeNull()
-
- if (firstSession === null) {
- return
- }
-
- const pendingRefresh = refreshSimpleAccessToken(firstSession)
-
- const replacementSession = await replaceSimpleAuthSession(TOKENS_B)
- resolveFetch(
- new Response(JSON.stringify({ data: { tokens: ROTATED_TOKENS_A } }), {
- status: 200,
- headers: { 'Content-Type': 'application/json' }
- })
- )
-
- await expect(pendingRefresh).resolves.toBeNull()
- expect(getSimpleAuthSession()).toEqual(replacementSession)
- })
-
- it('does not clear a newer login when stale refresh is rejected', async () => {
- let resolveFetch!: (response: Response) => void
- const fetchPromise = new Promise((resolveResponse) => {
- resolveFetch = resolveResponse
- })
- vi.stubGlobal('fetch', vi.fn(() => fetchPromise))
- const firstSession = await replaceSimpleAuthSession(TOKENS_A)
-
- expect(firstSession).not.toBeNull()
-
- if (firstSession === null) {
- return
- }
-
- const pendingRefresh = refreshSimpleAccessToken(firstSession)
-
- const replacementSession = await replaceSimpleAuthSession(TOKENS_B)
- resolveFetch(
- new Response(JSON.stringify({ code: 'REFRESH_TOKEN_REUSED', message: 'Rejected' }), {
- status: 401,
- statusText: 'Unauthorized',
- headers: { 'Content-Type': 'application/json' }
- })
- )
-
- await expect(pendingRefresh).rejects.toThrow('401 Unauthorized')
- expect(getSimpleAuthSession()).toEqual(replacementSession)
- })
-})
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/types/demo-scenario.type.ts b/examples/demo-frontend/src/infra/simple-rest-api/types/demo-scenario.type.ts
deleted file mode 100644
index 0b1ad8d..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/types/demo-scenario.type.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * Управляемый сетевой сценарий fixture-бэкенда.
- */
-export type DemoScenario =
- | 'normal'
- | 'slow'
- | 'timeout'
- | 'server-error'
- | 'rate-limited'
- | 'empty'
- | 'expired-auth'
- | 'forbidden'
- | 'conflict'
-
-/**
- * Подписчик на изменение активного сетевого сценария.
- */
-export type DemoScenarioListener = () => void
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/types/index.ts b/examples/demo-frontend/src/infra/simple-rest-api/types/index.ts
deleted file mode 100644
index bf62fdf..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/types/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type { DemoScenario } from './demo-scenario.type'
-export type { SimpleOrdersQuery, SimpleProductsQuery } from './simple-api-query.type'
-export type { SimpleRestApiCache } from './simple-rest-api-cache.type'
-export type { SimpleRestApiProviderProps } from './simple-rest-api-provider-props.type'
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/types/simple-api-query.type.ts b/examples/demo-frontend/src/infra/simple-rest-api/types/simple-api-query.type.ts
deleted file mode 100644
index b8947bf..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/types/simple-api-query.type.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import type { SortEnum } from '../generated'
-
-/**
- * Query-параметры выдачи продуктов с исправленными числовыми полями OpenAPI.
- */
-export type SimpleProductsQuery = {
- /** Номер страницы, начиная с единицы. */
- page?: number
- /** Максимальное число продуктов на странице. */
- limit?: number
- /** Поиск по названию и описанию. */
- search?: string
- /** Фильтр по идентификатору категории. */
- categoryId?: string
- /** Порядок сортировки выдачи. */
- sort?: SortEnum
-}
-
-/**
- * Query-параметры списка заказов с исправленными числовыми полями OpenAPI.
- */
-export type SimpleOrdersQuery = {
- /** Номер страницы, начиная с единицы. */
- page?: number
- /** Максимальное число заказов на странице. */
- limit?: number
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/types/simple-rest-api-cache.type.ts b/examples/demo-frontend/src/infra/simple-rest-api/types/simple-rest-api-cache.type.ts
deleted file mode 100644
index 2f11fd4..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/types/simple-rest-api-cache.type.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Bound cache operations текущего Simple REST API provider scope.
- */
-export type SimpleRestApiCache = {
- /** Ревалидирует products и categories. */
- invalidateCatalog: () => Promise
- /** Ревалидирует protected order resources. */
- invalidateOrders: () => Promise
- /** Ревалидирует все Simple API resources. */
- invalidateAll: () => Promise
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/types/simple-rest-api-provider-props.type.ts b/examples/demo-frontend/src/infra/simple-rest-api/types/simple-rest-api-provider-props.type.ts
deleted file mode 100644
index 5d3e531..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/types/simple-rest-api-provider-props.type.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import type { ReactNode } from 'react'
-
-/**
- * Props application-scoped REST cache provider.
- */
-export type SimpleRestApiProviderProps = {
- /** Поддерево одного auth-aware cache scope. */
- children: ReactNode
- /** Приостанавливает queries до определения auth cache scope. */
- isPaused?: boolean
-}
diff --git a/examples/demo-frontend/src/infra/simple-rest-api/types/to-generated-query.ts b/examples/demo-frontend/src/infra/simple-rest-api/types/to-generated-query.ts
deleted file mode 100644
index 96e1676..0000000
--- a/examples/demo-frontend/src/infra/simple-rest-api/types/to-generated-query.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type {
- SimpleOrdersListParams,
- SimpleProductsListParams
-} from '../generated'
-import type { SimpleOrdersQuery, SimpleProductsQuery } from './simple-api-query.type'
-
-/**
- * Адаптирует корректные числовые query-поля к неточному generated OpenAPI-типу.
- *
- * Nest Swagger описал преобразуемые number-поля как object; runtime-контракт остаётся числовым.
- */
-export const toGeneratedProductsQuery = (query: SimpleProductsQuery): SimpleProductsListParams => {
- return query as unknown as SimpleProductsListParams
-}
-
-/**
- * Адаптирует pagination к неточному generated OpenAPI-типу заказов.
- */
-export const toGeneratedOrdersQuery = (query: SimpleOrdersQuery): SimpleOrdersListParams => {
- return query as unknown as SimpleOrdersListParams
-}
diff --git a/examples/demo-frontend/src/shared/lib/format-date.test.ts b/examples/demo-frontend/src/shared/lib/format-date.test.ts
deleted file mode 100644
index 45ec667..0000000
--- a/examples/demo-frontend/src/shared/lib/format-date.test.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { describe, expect, it } from 'vitest'
-
-import { formatDate } from './format-date'
-
-describe('formatDate', () => {
- it('uses an explicit UTC calendar day', () => {
- expect(formatDate('2026-07-20T00:30:00.000Z')).toBe('Jul 20, 2026')
- })
-})
diff --git a/examples/demo-frontend/src/shared/lib/format-date.ts b/examples/demo-frontend/src/shared/lib/format-date.ts
deleted file mode 100644
index 4df3734..0000000
--- a/examples/demo-frontend/src/shared/lib/format-date.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/**
- * Форматирует ISO-дату в короткую человекочитаемую дату.
- */
-export const formatDate = (value: string): string => {
- return new Intl.DateTimeFormat('en-US', {
- day: '2-digit',
- month: 'short',
- year: 'numeric',
- timeZone: 'UTC'
- }).format(new Date(value))
-}
diff --git a/examples/demo-frontend/src/shared/lib/format-money.ts b/examples/demo-frontend/src/shared/lib/format-money.ts
deleted file mode 100644
index 1d598b7..0000000
--- a/examples/demo-frontend/src/shared/lib/format-money.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Форматирует сумму в минимальных единицах валюты для витрины.
- */
-export const formatMoney = (valueCents: number, currency: string): string => {
- return new Intl.NumberFormat('en-US', {
- style: 'currency',
- currency
- }).format(valueCents / 100)
-}
diff --git a/examples/demo-frontend/src/shared/lib/value-predicates/index.ts b/examples/demo-frontend/src/shared/lib/value-predicates/index.ts
deleted file mode 100644
index feb2426..0000000
--- a/examples/demo-frontend/src/shared/lib/value-predicates/index.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export {
- hasOwn,
- isArray,
- isArrayOf,
- isEmptyArray,
- isNonEmptyArray,
- isNumber,
- isOneOf,
- isRecord,
- isString
-} from './value-predicates'
diff --git a/examples/demo-frontend/src/shared/lib/value-predicates/value-predicates.ts b/examples/demo-frontend/src/shared/lib/value-predicates/value-predicates.ts
deleted file mode 100644
index ed25c34..0000000
--- a/examples/demo-frontend/src/shared/lib/value-predicates/value-predicates.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * Проверяет, что значение является строкой.
- */
-export const isString = (value: unknown): value is string => {
- return typeof value === 'string'
-}
-
-/**
- * Проверяет, что значение является конечным числом.
- */
-export const isNumber = (value: unknown): value is number => {
- return typeof value === 'number' && Number.isFinite(value)
-}
-
-/**
- * Проверяет, что значение является массивом без уточнения элементов.
- */
-export const isArray = (value: unknown): value is unknown[] => {
- return Array.isArray(value)
-}
-
-/**
- * Проверяет массив и каждый его элемент переданным предикатом.
- */
-export const isArrayOf = (value: unknown, isItem: (item: unknown) => item is T): value is T[] => {
- return Array.isArray(value) && value.every(isItem)
-}
-
-/**
- * Проверяет отсутствие элементов, считая null и undefined пустым списком.
- */
-export const isEmptyArray = (value: readonly unknown[] | null | undefined): boolean => {
- return !Array.isArray(value) || value.length === 0
-}
-
-/**
- * Проверяет, что список существует и содержит хотя бы один элемент.
- */
-export const isNonEmptyArray = (
- value: readonly T[] | null | undefined
-): value is readonly [T, ...T[]] => {
- return Array.isArray(value) && value.length > 0
-}
-
-/**
- * Проверяет, что значение является объектом-записью.
- */
-export const isRecord = (value: unknown): value is Record => {
- return typeof value === 'object' && value !== null && !Array.isArray(value)
-}
-
-/**
- * Проверяет наличие собственного свойства объекта.
- */
-export const hasOwn = (value: object, key: K): value is Record => {
- return Object.prototype.hasOwnProperty.call(value, key)
-}
-
-/**
- * Проверяет принадлежность значения набору литералов.
- */
-export const isOneOf = (value: unknown, values: T): value is T[number] => {
- return values.some((item) => item === value)
-}
diff --git a/examples/demo-frontend/src/shared/types/result.type.ts b/examples/demo-frontend/src/shared/types/result.type.ts
deleted file mode 100644
index 9191893..0000000
--- a/examples/demo-frontend/src/shared/types/result.type.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Успешный результат прикладного сценария.
- */
-export type SuccessResult = {
- /** Признак успешного завершения. */
- isSuccess: true
- /** Значение завершённого сценария. */
- data: T
-}
-
-/**
- * Ожидаемая ошибка прикладного сценария.
- */
-export type FailureResult = {
- /** Признак неуспешного завершения. */
- isSuccess: false
- /** Безопасная ошибка текущего владельца. */
- error: E
-}
-
-/**
- * Явный канал успешного или ожидаемо неуспешного результата.
- */
-export type Result = SuccessResult | FailureResult
diff --git a/examples/demo-frontend/src/ui/button/button.tsx b/examples/demo-frontend/src/ui/button/button.tsx
deleted file mode 100644
index f24686b..0000000
--- a/examples/demo-frontend/src/ui/button/button.tsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import cl from 'clsx'
-import type { ButtonProps } from './types/button-props.type'
-import styles from './styles/button.module.css'
-
-/**
- * Универсальное действие storefront-интерфейса.
- *
- * Используется для:
- * - submit и mutation действий
- * - компактных toolbar-контролов
- */
-export const Button = (props: ButtonProps) => {
- const {
- children,
- className,
- variant = 'primary',
- size = 'medium',
- isLoading = false,
- disabled,
- type = 'button',
- ...rootAttrs
- } = props
- const content = isLoading ? 'Working...' : children
-
- return (
-
- )
-}
diff --git a/examples/demo-frontend/src/ui/button/index.ts b/examples/demo-frontend/src/ui/button/index.ts
deleted file mode 100644
index 2981547..0000000
--- a/examples/demo-frontend/src/ui/button/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { Button } from './button'
-export type { ButtonProps } from './types/button-props.type'
diff --git a/examples/demo-frontend/src/ui/button/styles/button.module.css b/examples/demo-frontend/src/ui/button/styles/button.module.css
deleted file mode 100644
index 98d1c15..0000000
--- a/examples/demo-frontend/src/ui/button/styles/button.module.css
+++ /dev/null
@@ -1,85 +0,0 @@
-.root {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- border: 1px solid transparent;
- border-radius: 999px;
- font: inherit;
- font-size: 0.78rem;
- font-weight: 760;
- letter-spacing: 0.08em;
- line-height: 1;
- text-transform: uppercase;
- cursor: pointer;
- transition:
- transform 160ms ease,
- background 160ms ease,
- border-color 160ms ease,
- color 160ms ease;
-}
-
-.root:hover:not(:disabled) {
- transform: translateY(-2px);
-}
-
-.root:focus-visible {
- outline: 3px solid var(--color-focus);
- outline-offset: 3px;
-}
-
-.root:disabled {
- cursor: not-allowed;
- opacity: 0.48;
-}
-
-.medium {
- min-height: 44px;
- padding: 0 20px;
-}
-
-.small {
- min-height: 34px;
- padding: 0 13px;
- font-size: 0.68rem;
-}
-
-.primary {
- color: var(--color-paper);
- background: var(--color-ink);
-}
-
-.primary:hover:not(:disabled) {
- background: var(--color-blue);
-}
-
-.secondary {
- color: var(--color-ink);
- border-color: var(--color-ink);
- background: transparent;
-}
-
-.secondary:hover:not(:disabled) {
- background: var(--color-acid);
-}
-
-.ghost {
- color: var(--color-ink-muted);
- border-color: var(--color-line);
- background: color-mix(in srgb, var(--color-paper) 70%, transparent);
-}
-
-.ghost:hover:not(:disabled) {
- color: var(--color-ink);
- border-color: var(--color-ink);
-}
-
-.danger {
- color: var(--color-error);
- border-color: color-mix(in srgb, var(--color-error) 45%, transparent);
- background: transparent;
-}
-
-.danger:hover:not(:disabled) {
- color: var(--color-paper);
- background: var(--color-error);
-}
diff --git a/examples/demo-frontend/src/ui/button/types/button-props.type.ts b/examples/demo-frontend/src/ui/button/types/button-props.type.ts
deleted file mode 100644
index ab73633..0000000
--- a/examples/demo-frontend/src/ui/button/types/button-props.type.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Собственные параметры универсальной кнопки.
- */
-export type ButtonParams = {
- /** Визуальный приоритет действия. */
- variant?: 'primary' | 'secondary' | 'ghost' | 'danger'
- /** Размер интерактивной области. */
- size?: 'small' | 'medium'
- /** Выполняется ли асинхронное действие. */
- isLoading?: boolean
-}
-
-/**
- * Атрибуты корневой button-кнопки.
- */
-type RootAttrs = ComponentPropsWithoutRef<'button'>
-
-/**
- * Props универсальной кнопки.
- */
-export type ButtonProps = RootAttrs & ButtonParams
diff --git a/examples/demo-frontend/src/ui/feedback-panel/feedback-panel.tsx b/examples/demo-frontend/src/ui/feedback-panel/feedback-panel.tsx
deleted file mode 100644
index de12a93..0000000
--- a/examples/demo-frontend/src/ui/feedback-panel/feedback-panel.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import cl from 'clsx'
-import type { FeedbackPanelProps } from './types/feedback-panel-props.type'
-import styles from './styles/feedback-panel.module.css'
-
-/**
- * Выделенный outcome для loading, empty, error и success-состояний.
- *
- * Используется для:
- * - восстановления после API-ошибки
- * - объяснения пустой или недоступной страницы
- */
-export const FeedbackPanel = (props: FeedbackPanelProps) => {
- const { title, description, variant = 'info', children, className, ...rootAttrs } = props
- const role = variant === 'error' ? 'alert' : 'status'
-
- return (
-
-
-
-
{title}
-
{description}
- {children}
-
-
- )
-}
diff --git a/examples/demo-frontend/src/ui/feedback-panel/index.ts b/examples/demo-frontend/src/ui/feedback-panel/index.ts
deleted file mode 100644
index d83c691..0000000
--- a/examples/demo-frontend/src/ui/feedback-panel/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { FeedbackPanel } from './feedback-panel'
-export type { FeedbackPanelProps } from './types/feedback-panel-props.type'
diff --git a/examples/demo-frontend/src/ui/feedback-panel/styles/feedback-panel.module.css b/examples/demo-frontend/src/ui/feedback-panel/styles/feedback-panel.module.css
deleted file mode 100644
index baa530c..0000000
--- a/examples/demo-frontend/src/ui/feedback-panel/styles/feedback-panel.module.css
+++ /dev/null
@@ -1,47 +0,0 @@
-.root {
- display: grid;
- grid-template-columns: 10px minmax(0, 1fr);
- gap: 20px;
- width: 100%;
- padding: clamp(24px, 5vw, 48px);
- border: 1px solid var(--color-line);
- border-radius: 24px;
- background: var(--color-surface);
- box-shadow: var(--shadow-card);
-}
-
-.marker {
- min-height: 84px;
- border-radius: 999px;
- background: var(--color-blue);
-}
-
-.error .marker {
- background: var(--color-error);
-}
-
-.empty .marker {
- background: var(--color-orange);
-}
-
-.success .marker {
- background: var(--color-acid-dark);
-}
-
-.content {
- display: grid;
- justify-items: start;
- gap: 12px;
-}
-
-.title {
- font-family: var(--font-display);
- font-size: clamp(1.5rem, 4vw, 2.5rem);
- line-height: 0.98;
-}
-
-.description {
- max-width: 62ch;
- color: var(--color-ink-muted);
- line-height: 1.6;
-}
diff --git a/examples/demo-frontend/src/ui/feedback-panel/types/feedback-panel-props.type.ts b/examples/demo-frontend/src/ui/feedback-panel/types/feedback-panel-props.type.ts
deleted file mode 100644
index 0ce722c..0000000
--- a/examples/demo-frontend/src/ui/feedback-panel/types/feedback-panel-props.type.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Собственные параметры универсального route outcome.
- */
-export type FeedbackPanelParams = {
- /** Короткий заголовок состояния. */
- title: string
- /** Пояснение и возможное действие пользователя. */
- description: string
- /** Семантический вариант состояния. */
- variant?: 'info' | 'error' | 'empty' | 'success'
-}
-
-/**
- * Атрибуты корневой section без обязательного title-атрибута.
- */
-type RootAttrs = Omit, 'title'>
-
-/**
- * Props универсального route outcome.
- */
-export type FeedbackPanelProps = RootAttrs & FeedbackPanelParams
diff --git a/examples/demo-frontend/src/ui/form-field/form-field.tsx b/examples/demo-frontend/src/ui/form-field/form-field.tsx
deleted file mode 100644
index a841d68..0000000
--- a/examples/demo-frontend/src/ui/form-field/form-field.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import cl from 'clsx'
-import type { FormFieldProps } from './types/form-field-props.type'
-import styles from './styles/form-field.module.css'
-
-/**
- * Доступная подпись и feedback для form control.
- *
- * Используется для:
- * - auth и catalog admin форм
- */
-export const FormField = (props: FormFieldProps) => {
- const { label, htmlFor, hint, error, children, className, ...rootAttrs } = props
- const hasError = Boolean(error)
-
- return (
-
-
- {children}
- {hint && !hasError && {hint}}
- {hasError && {error}}
-
- )
-}
diff --git a/examples/demo-frontend/src/ui/form-field/index.ts b/examples/demo-frontend/src/ui/form-field/index.ts
deleted file mode 100644
index 7789a7a..0000000
--- a/examples/demo-frontend/src/ui/form-field/index.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-export { FormField } from './form-field'
-export type { FormFieldProps } from './types/form-field-props.type'
diff --git a/examples/demo-frontend/src/ui/form-field/styles/form-field.module.css b/examples/demo-frontend/src/ui/form-field/styles/form-field.module.css
deleted file mode 100644
index 2145eea..0000000
--- a/examples/demo-frontend/src/ui/form-field/styles/form-field.module.css
+++ /dev/null
@@ -1,55 +0,0 @@
-.root {
- display: grid;
- gap: 8px;
-}
-
-.label {
- color: var(--color-ink-muted);
- font-size: 0.7rem;
- font-weight: 760;
- letter-spacing: 0.1em;
- text-transform: uppercase;
-}
-
-.root :is(input, select, textarea) {
- width: 100%;
- min-height: 46px;
- padding: 11px 14px;
- color: var(--color-ink);
- border: 1px solid var(--color-line-strong);
- border-radius: 12px;
- outline: none;
- background: var(--color-paper);
- font: inherit;
- transition:
- border-color 160ms ease,
- box-shadow 160ms ease;
-}
-
-.root textarea {
- min-height: 110px;
- resize: vertical;
-}
-
-.root :is(input, select, textarea):focus {
- border-color: var(--color-blue);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-blue) 18%, transparent);
-}
-
-.hint,
-.error {
- font-size: 0.76rem;
- line-height: 1.35;
-}
-
-.hint {
- color: var(--color-ink-muted);
-}
-
-.error {
- color: var(--color-error);
-}
-
-.hasError :is(input, select, textarea) {
- border-color: var(--color-error);
-}
diff --git a/examples/demo-frontend/src/ui/form-field/types/form-field-props.type.ts b/examples/demo-frontend/src/ui/form-field/types/form-field-props.type.ts
deleted file mode 100644
index bd06fab..0000000
--- a/examples/demo-frontend/src/ui/form-field/types/form-field-props.type.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { ComponentPropsWithoutRef } from 'react'
-
-/**
- * Собственные параметры подписи поля формы.
- */
-export type FormFieldParams = {
- /** Видимая подпись control. */
- label: string
- /** Идентификатор связанного form control. */
- htmlFor: string
- /** Необязательная подсказка формата. */
- hint?: string
- /** Ошибка конкретного поля. */
- error?: string
-}
-
-/**
- * Атрибуты корневого контейнера.
- */
-type RootAttrs = ComponentPropsWithoutRef<'div'>
-
-/**
- * Props подписи и состояния form control.
- */
-export type FormFieldProps = RootAttrs & FormFieldParams
diff --git a/examples/demo-frontend/tsconfig.json b/examples/demo-frontend/tsconfig.json
deleted file mode 100644
index cf9c65d..0000000
--- a/examples/demo-frontend/tsconfig.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2017",
- "lib": ["dom", "dom.iterable", "esnext"],
- "allowJs": true,
- "skipLibCheck": true,
- "strict": true,
- "noEmit": true,
- "esModuleInterop": true,
- "module": "esnext",
- "moduleResolution": "bundler",
- "resolveJsonModule": true,
- "isolatedModules": true,
- "jsx": "react-jsx",
- "incremental": true,
- "plugins": [
- {
- "name": "next"
- }
- ],
- "paths": {
- "@/*": ["./src/*"]
- }
- },
- "include": [
- "next-env.d.ts",
- "**/*.ts",
- "**/*.tsx",
- ".next/types/**/*.ts",
- ".next/dev/types/**/*.ts",
- "**/*.mts"
- ],
- "exclude": ["node_modules"]
-}
diff --git a/examples/demo-frontend/vitest.config.mts b/examples/demo-frontend/vitest.config.mts
deleted file mode 100644
index 1ff8233..0000000
--- a/examples/demo-frontend/vitest.config.mts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { fileURLToPath } from 'node:url'
-import { defineConfig } from 'vitest/config'
-
-export default defineConfig({
- resolve: {
- alias: {
- '@': fileURLToPath(new URL('./src', import.meta.url))
- }
- },
- test: {
- include: ['src/**/*.test.ts'],
- environment: 'node'
- }
-})