commit 60de4ef27ca7adc9d8751550af0f80118c841556 Author: Yuriy Panov Date: Fri Aug 28 13:21:48 2026 +0600 Convert fitness landing to React + Tailwind v4 with amoCRM lead capture Port the single-file landing (2.6 MB of inlined CSS, JS and base64 images, kept as fitnes/legacy/index.html) to Vite + React 19 + TypeScript, with an Express API that files every form submission into amoCRM pipeline 10980758. - Extract the 14 embedded images to src/assets/images and public/ - Rebuild the design system as Tailwind v4 @theme tokens; the stock palette and breakpoints are cleared so only the EXO scale is reachable from utilities - Split the page into 15 components; all copy moves to src/data - Lead endpoint: find-or-create the contact (Russian phone spellings compared on the last 10 digits), create the lead in the pipeline's first stage, map the fields the account already has and put the rest in a note. If amoCRM is unreachable the payload is logged and kept in localStorage rather than lost. - Add a callback modal as a second entry point, tagged separately in the pipeline - Self-host Inter Variable so the layout's 760/850/900 weights render as real weights instead of snapping to bold Fidelity was checked by comparing section offsets and heights against the original at 375/480/640/900/1120/1440 px; every section and the total page height matched exactly. Loading Inter deliberately changes text metrics, so the byte-exact comparison holds against the pre-font build. Co-Authored-By: Claude Opus 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/fitnes/.claude/launch.json b/fitnes/.claude/launch.json new file mode 100644 index 0000000..3428eb2 --- /dev/null +++ b/fitnes/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "fitnes-dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev:client"], + "port": 5173 + } + ] +} diff --git a/fitnes/.env.example b/fitnes/.env.example new file mode 100644 index 0000000..1cc82d5 --- /dev/null +++ b/fitnes/.env.example @@ -0,0 +1,33 @@ +# --------------------------------------------------------------------------- +# Public (baked into the client bundle at build time — never put secrets here) +# --------------------------------------------------------------------------- +VITE_SITE_URL=https://exodevices.ru/fitness + +# Sub-path the site is served from, if any. Leave as "/" when it sits at the +# domain root; set to "/fitness/" if it is mounted under exodevices.ru/fitness. +VITE_BASE_PATH=/ + +# --------------------------------------------------------------------------- +# Server +# --------------------------------------------------------------------------- +PORT=3000 + +# --------------------------------------------------------------------------- +# amoCRM — server-side only, never exposed to the browser +# --------------------------------------------------------------------------- +# Your account subdomain: for https://exotherapy.amocrm.ru put "exotherapy" +AMO_SUBDOMAIN= + +# Long-lived access token. +# amoCRM → Настройки → Интеграции → Создать интеграцию → Внешняя интеграция → +# open it → tab "Ключи и скопы" → «Генерировать токен» (долгосрочный, ~1 год). +AMO_LONG_LIVED_TOKEN= + +# Pipeline (воронка) the leads land in. The lead is placed in its first stage. +AMO_PIPELINE_ID=10980758 + +# Optional: numeric id of the amoCRM user leads are assigned to. +AMO_RESPONSIBLE_USER_ID= + +# Optional: comma-separated tags added to every lead. +AMO_LEAD_TAGS=fitness-landing diff --git a/fitnes/.gitignore b/fitnes/.gitignore new file mode 100644 index 0000000..23fc33a --- /dev/null +++ b/fitnes/.gitignore @@ -0,0 +1,6 @@ +node_modules +dist +.env +.env.local +*.local +.DS_Store diff --git a/fitnes/README.md b/fitnes/README.md new file mode 100644 index 0000000..5a0e63e --- /dev/null +++ b/fitnes/README.md @@ -0,0 +1,151 @@ +# EXO Recovery Zone — лендинг для фитнес-клубов + +React-приложение лендинга «EXO Recovery Zone». Каждая форма создаёт сделку в +amoCRM (воронка `10980758`) и контакт, если его ещё нет. + +Стек: **Vite + React 19 + TypeScript + Tailwind CSS v4**, API — **Express 5**. + +--- + +## Быстрый старт + +```bash +npm install +cp .env.example .env # заполнить AMO_SUBDOMAIN и AMO_LONG_LIVED_TOKEN +npm run dev +``` + +`npm run dev` поднимает Vite на `http://localhost:5173` и API на `:3000`; +запросы `/api/*` проксируются с фронтенда на API. + +## Продакшен + +```bash +npm run build # сборка клиента в dist/client +npm start # Express отдаёт dist/client и обрабатывает /api/* +``` + +Один процесс Node на любом VPS. Порт — `PORT` (по умолчанию 3000). + +--- + +## amoCRM + +### 1. Получить долгосрочный токен + +1. amoCRM → **Настройки → Интеграции → Создать интеграцию → Внешняя интеграция**. +2. Название любое (например «Сайт fitness»), redirect URI — любой рабочий адрес + сайта: для долгосрочного токена он не используется. +3. Права доступа: достаточно **Сделки, Контакты** (чтение и запись). +4. Сохранить, открыть интеграцию → вкладка **«Ключи и скопы»** → + **«Генерировать токен»** (долгосрочный, действует ~1 год). +5. Скопировать токен в `.env` в `AMO_LONG_LIVED_TOKEN`, поддомен — в + `AMO_SUBDOMAIN` (для `https://exotherapy.amocrm.ru` это `exotherapy`). + +Токен читается только на сервере и никогда не попадает в браузерный бандл. +Пометьте в календаре дату истечения — токен нужно перевыпустить через год. + +### 2. Проверить подключение + +```bash +npm run amo:check +``` + +Скрипт выведет название аккаунта, первый этап воронки `10980758`, в который +попадут сделки, и то, в какие поля вашего аккаунта легли данные формы. + +### 3. Что происходит при отправке формы + +1. Поиск контакта по телефону (во всех написаниях: `+7…`, `8…`, только цифры) + и по email. Если контакт найден — используется он, недостающий телефон или + email дописывается в карточку. +2. Если контакта нет — создаётся новый с именем, телефоном и email. +3. Создаётся сделка в воронке `AMO_PIPELINE_ID`, в её **первом этапе**, + с тегами `AMO_LEAD_TAGS` + тег формы (`заявка с сайта` / `обратный звонок`). +4. Данные, для которых в аккаунте есть подходящее поле (город, формат клуба, + компания, `utm_*` и т.д.), пишутся в поля; всё остальное — в примечание к + сделке. Ничего настраивать в amoCRM заранее не нужно. + +Если amoCRM недоступна, заявка **не теряется**: она пишется в лог сервера +(`[lead] payload was: …`) и сохраняется в `localStorage` браузера, а посетитель +видит телефон для связи. + +### Переменные окружения + +| Переменная | Обязательна | Описание | +|---|---|---| +| `AMO_SUBDOMAIN` | да | Поддомен аккаунта, например `exotherapy` | +| `AMO_LONG_LIVED_TOKEN` | да | Долгосрочный токен интеграции | +| `AMO_PIPELINE_ID` | нет | Воронка для сделок (по умолчанию `10980758`) | +| `AMO_RESPONSIBLE_USER_ID` | нет | Ответственный за сделку пользователь | +| `AMO_LEAD_TAGS` | нет | Теги через запятую для всех сделок | +| `PORT` | нет | Порт API/продакшен-сервера (`3000`) | +| `VITE_SITE_URL` | нет | Канонический адрес, подставляется в `canonical` и `og:` | +| `VITE_BASE_PATH` | нет | Подпуть размещения, например `/fitness/` (по умолчанию `/`) | + +--- + +## Структура + +``` +index.html точка входа Vite (meta, Open Graph, JSON-LD) +src/ + components/ секции лендинга и UI-примитивы + data/ весь текстовый контент и данные конструктора + hooks/ useConstructor, useReveal, useScrollState, … + lib/ отправка заявки, форматирование чисел + assets/images/ 14 изображений, извлечённых из исходного HTML + index.css дизайн-токены Tailwind v4 (@theme) и базовые стили +shared/lead.ts схема заявки (zod), общая для клиента и сервера +server/src/ Express API + клиент amoCRM +scripts/amo-check.ts диагностика подключения к amoCRM +legacy/index.html исходный однофайловый лендинг (визуальный эталон) +``` + +### Дизайн-система + +Цвета, радиусы, тени, брейкпоинты и вертикальный ритм заданы токенами в +`@theme` внутри `src/index.css`. Палитра Tailwind по умолчанию отключена +(`--color-*: initial`), поэтому в разметке доступны только цвета EXO. +Брейкпоинты соответствуют исходной вёрстке: `sm` 640, `md` 900, `lg` 1120, плюс +`max-width`-варианты `narrow` (760), `phone` (520), `compact` (680), `tiny` (480). + +Вёрстка совпадает с `legacy/index.html` попиксельно: высоты всех секций и общая +высота страницы идентичны на 375 / 480 / 640 / 900 / 1120 / 1440 px. + +### Шрифты + +Inter подключён локально пакетом `@fontsource-variable/inter` — внешних запросов +(Google Fonts и т.п.) страница не делает. Это вариативный шрифт с диапазоном +100–900, поэтому нестандартные веса из макета (760, 850, 900) отрисовываются +по-настоящему, а не округляются до `bold`. + +Подсеты разделены по `unicode-range`, браузер скачивает только нужные: + +| подсет | размер | зачем | +|---|---|---| +| latin | 48 КБ | латиница | +| cyrillic | 19 КБ | кириллица | +| latin-ext | 85 КБ | **только ради знака `₽`** | + +Итого ~152 КБ. Если 85 КБ ради одного символа кажутся лишними — уберите +`latin-ext`, и `₽` будет отрисовываться системным шрифтом. + +Символы `→ ↗ ↘ ≈` не входят ни в один подсет Inter у Google Fonts / Fontsource, +поэтому для них всегда используется системный фолбэк. Это заметно только при +очень крупном кегле. + +`Manrope` и `Segoe UI` остались в стеке `--font-sans` как фолбэк, но отдельно не +загружаются: Manrope недостижим, пока Inter грузится успешно, а Segoe UI — +проприетарный шрифт Microsoft, который нельзя раздавать с сайта. + +### Калькулятор + +`оплаченные программы в день × средний чек × рабочие дни = выручка в месяц`. + +--- + +## Известные особенности + +* `og:image` отдаётся по абсолютному адресу `VITE_SITE_URL` + `/og-image.webp`; + при смене домена обновите `VITE_SITE_URL` перед сборкой. diff --git a/fitnes/index.html b/fitnes/index.html new file mode 100644 index 0000000..7c9169f --- /dev/null +++ b/fitnes/index.html @@ -0,0 +1,44 @@ + + + + + + EXO Recovery Zone для фитнес-клубов | Экзо Групп + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/fitnes/legacy/index.html b/fitnes/legacy/index.html new file mode 100644 index 0000000..e2021d3 --- /dev/null +++ b/fitnes/legacy/index.html @@ -0,0 +1,915 @@ + + + + + + +EXO Recovery Zone для фитнес-клубов | Экзо Групп + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+
+ EXO Performance / Recovery Zone +

Восстановление, которое удерживает клиента в клубе

+

Готовая зона внутри фитнес-центра: клиент проходит путь «тренировка → восстановление → контроль → следующий визит», не уходя во внешние клиники и recovery-студии.

+ +
+
3 аппаратакомпактное ядро под задачи клуба
+
5 аппаратовполный маршрут recovery и спорта
+
Окупаемостьот 6 месяцев — быстрый возврат инвестиций без риска заморозить деньги в оборудовании
+
+ Почему это актуально сейчас +
+ +
+
+
+
+
+Динамика рынка +

Фитнес рынок растет, но удерживать выручку только за счет удорожания абонемента становится все сложнее.

+

Клубу нужен принципиально новый продукт внутри сетки услуг: высокомаржинальный сервис восстановления, который увеличивает ARPU, LTV и удерживает людей в клубе.

+
+
+
+
+
263 млрд ₽+23%2024
+
316,5 млрд ₽+20%2025
+
365 млрд ₽+15% прогноз2026П
+
+
Оборот фитнес-услуг в Россиитемп: 23% → 20% → 15%
+
+
+Что меняется для собственника +
+
>50%
Рост всё больше ценовой

В I полугодии 2025 года повышение цен обеспечило более половины прироста рынка.

+
12%
Подписка меняет модель

Доля рекуррентных продаж карт достигла 12% в I квартале 2025 года, в Москве приблизилась к 20%.

+
66%
Удержание — ключевой резерв

Международный benchmark HFA: средняя годовая удерживаемость участников — около двух третей.

+
+

Источники: FitnessData, итоги 2025 и прогноз 2026; FitnessData, I полугодие 2025; HFA Benchmarking Report 2025.

+
+
+
+
+ +
+
+
+ Экономика действующей клиентской базы +

Клуб теряет деньги не на тренировке — а между посещениями

+

Более половины прироста рынка уже обеспечивается повышением цен, а средняя годовая удерживаемость составляет около 66%. Recovery Zone создаёт новую выручку без продажи ещё одной клубной карты.

+
+
+
>50%роста рынка — ценовойдальше повышать карту всё сложнее
+
34 из 100клиентов не удерживаютсярасчёт из benchmark 66%
+
≈88%продаж карт не рекуррентныерекуррентная доля — 12%
+
23→15%замедление темпа рынка2024 → прогноз 2026
+
+
+
+

Где клуб недополучает выручку

+
+
01
День без тренировки не монетизируетсяПри усталости, перегрузке или паузе клиент не приходит в клуб и не покупает дополнительный сервис.
+
02
Запрос уходит во внешние студииМассаж, восстановление и return-to-sport покупаются за пределами клуба.
+
03
ARPU растёт только вместе с ценой картыНовый доход появляется через подорожание, а не через новый продукт для действующей базы.
+
+
+
+

На чём зарабатывает клуб с Экзо

+
+
01
Разовые recovery-сессииОтдельная причина прийти в клуб после нагрузки или даже в день без тренировки.
+
02
Короткие курсы и пакетыВосстановление ног, подвижность, перезагрузка и возврат к нагрузке повышают ARPU.
+
03
Premium и рекомендации тренераRecovery усиливает membership, помогает удержанию и возвращает клиента к регулярной нагрузке.
+
+
+
+
+ Экономика строится на текущей базе, а не на дополнительном маркетинговом трафике +
+
3 аппаратакомпактное recovery-ядро
+
5 аппаратовполная платформа клуба
+
от 6 мес.заявленный ориентир окупаемости
+
4 каналасессии, пакеты, premium, PT
+
+

Итоговая конфигурация и экономика зависят от базы клуба, формата тренировок, выбранной модели работы, тарифов и загрузки зоны.

+
+
+
+ Простой расчёт +

Сколько зона восстановления может приносить клубу

+

Без процентов и сложных метрик: укажите продажи в день, средний чек и количество рабочих дней.

+
+
+
1Оплаченных программ
в день
+ × +
2Средний чек
программы
+ × +
3Рабочих дней
в месяц
+ = +
Дополнительная
выручка в месяц
+
+
+
+
+ + + Разовая сессия, пакет или короткий курс — одна продажа. +
+
+ + + Фактическая средняя сумма оплаты +
+
+ + + Сколько дней зона принимает клиентов +
+
+
+
Дополнительная выручка / месяцВведите программы в день и средний чек
+
Дополнительная выручка / годдо вычета расходов
+
+
+

Расчёт показывает сценарную выручку, а не финансовую гарантию. Итоговая модель уточняется по конфигурации аппаратов, тарифам, ФОТ, загрузке и формату работы Recovery Zone.

+
+
+
+ +
+
+
+Бизнес-эффект +

Следующий рост клуба — не ещё один тренажёр. Это восстановление

+

Зона встраивается в существующий клиентский поток и превращает паузу после нагрузки в понятный продукт клуба.

+
+
+
01 / LTV

Удержание

Появляется причина прийти даже в день без тренировки и не выпадать из привычного плана из-за перегрузки или дискомфорта.

+
02 / ARPU

Новая выручка

Процедуры, короткие курсы и recovery-пакеты монетизируют действующую базу без продажи ещё одной клубной карты.

+
03 / SALES

Роль тренера

Тренер замечает запрос, рекомендует маршрут восстановления и получает инструмент для возвращения клиента к нагрузке.

+
04 / BRAND

Новая аудитория

Premium-клиенты, спортсмены-любители, взрослые и корпоративные клиенты получают дополнительный аргумент выбрать клуб.

+
LTVLifetime Value — совокупная ценность клиента за весь период отношений с клубом.
ARPUAverage Revenue Per User — средняя выручка на одного клиента.
SALESSales — продажи и коммерческая конверсия.
BRANDBrand — бренд, его ценность и восприятие аудиторией.
+
+
+
+
+
+Конструктор EXO +

Не один комплект для всех. Собираем зону под ваш клуб

+

Выберите компактное ядро из трёх аппаратов или полную платформу из пяти. Финальная конфигурация определяется после аудита площади, потока, аудитории, формата тренировок.

+
+
+
+
Интерактивный подбор

Нажмите на аппараты — справа изменится состав и логика зоны.

+
+ + +
+
+
+
+

Состав зоны

Выберите ровно 3 аппарата

3 / 3
+
+ + + + + +
+
+ + + +
+
+
+
Пример зоны восстановления с тремя аппаратами в фитнес-клубеКомпактный форматПример визуализации. Итоговый рендер создаётся под выбранный состав и интерьер клуба.
+

Recovery Start — 3 аппарата

Компактный вход в recovery с флагманской, потоковой и автономной процедурой.

03
+
+
+ +
+
+
+
+
+
+
+
+Продукт для клиента +

Продаётся не аппарат. Продаётся понятный маршрут

+

Состав программы зависит от цели клиента, этапа тренировок и допуска специалиста. Клуб получает продукт, который проще объяснять, измерять и продавать курсом.

+
+
СИЛА И МЫШЦЫ

Готовность к тяжелым весам (Пауэрлифтинг / Бодибилдинг)

Комплексная подготовка опорно-двигательного аппарата к предельным нагрузкам. Устранение мышечной зажатости, проработка триггерных точек, фасциальный релиз и увеличение эластичности мягких тканей для безопасного приседа, жима и тяги.

Процедуры и связки: ЭкзоТекар + ЭкзоТерапия
Глубокий прогрев соединительной ткани, снятие блоков, подготовка нервной системы к взрывной работе.
БЕГ / КАРДИО

Восстановление ног и выносливости (Легкая атлетика / Сайклинг)

Мощный лимфодренаж и выведение продуктов распада (молочной кислоты) после длительного бега, функционального тренинга или «дня ног». Снятие ощущения «гудящих» мышц, устранение застойных явлений и запуск быстрой регенерации.

Процедуры и связки: ЭкзоПресс + ЭкзоВодород
Прессотерапия для вытеснения венозной крови и межтканевой жидкости + мощное антиоксидантное насыщение против клеточного стресса.
ПОДВИЖНОСТЬ И ГИБКОСТЬ

Локальный комфорт и амплитуда (Йога / Растяжка / Пилатес)

Увеличение подвижности заблокированных суставов (особенно тазобедренных и плечевых) и снятие жесткости фасций. Идеально для тех, кто хочет улучшить шпагат, разгрузить позвоночник и убрать скованность движений после сидячей работы.

Процедуры и связки: ЭкзоТекар + ЭкзоТерапия
Фасциальный массаж с глубоким прогревом для стимуляции выработки коллагена и эластичности.
ПОСЛЕ НАГРУЗКИ

Перезагрузка после активного дня (Кроссфит / Единоборства)

Быстрый перевод организма из режима максимального стресса (симпатическая нервная система) в режим глубокого отдыха (парасимпатика). Снижение общего воспалительного тонуса мышц, нормализация пульса и подготовка к комфортному ночному сну.

Процедуры и связки: ЭкзоТерапия + ЭкзоПресс
Стимуляция нервных окончаний для снятия осевой нагрузки с позвоночника + лимфодренажный массаж всего тела.
ВОЗВРАТ В СПОРТ

Безопасный возврат к тренировкам (После травм и пауз)

Мягкая и безопасная реабилитация внутри фитнес-клуба. Ускорение заживления растяжений, точечное снятие хронических воспалений в сухожилиях, восстановление мышечного тонуса и координации движений в пространстве (проприоцепции).

Процедуры и связки: Комбинация по протоколу: ЭкзоЛазер B + ЭкзоИмпульс
Локальная фотобиомодуляция для регенерации связок + глубокая электромагнитная стимуляция для пробуждения атрофированных мышц.
СТРЕСС И ПЕРЕЗАГРУЗКА

День восстановления (Фитнес без тренировки)

Сценарий посещения клуба без физических нагрузок. Полная релаксация, ликвидация умственного выгорания, синдрома хронической усталости и головных болей напряжения. Возвращение телесного комфорта и легкости за один сеанс.

Процедуры и связки: ЭкзоВодород + ЭкзоПресс
Ингаляции чистым водородом для снижения уровня стресса на клеточном уровне + мягкий релакс-массаж ног и тела.
+
Корректная коммуникация: не обещаем «сжигание жира», рост мышц от аппарата, лечение травмы без специалиста или гарантированный спортивный результат. В медицинском формате используются допуск, назначение, протокол и проверка противопоказаний.
+
+
+
+
+
+Визуальная концепция +

Recovery должен выглядеть частью фитнеса, а не «медицинским углом»

+

Используем готовые рендеры как отправную точку и создаём визуализацию под интерьер, площадь и выбранный конструктор вашего клуба.

+
+ + +
+
+ +
+
+
+Почему Экзо Групп +

Вы входите в готовую экосистему, где каждый бизнес-процесс уже автоматизирован.

+

Собственное производство, планировка пространства, меню готовых программ, обучение команды. Все это работает на окупаемость вашего клуба с первого дня.

+
+
4собственные клиники
+
70+клиник-партнёров
+
60+медицинских учреждений
+
2 млн+проведённых процедур
+
+
+
Оборудованиеподбор и поставка
+
Пространствопланировка и дизайн
+
Программыметодики и курсы
+
Обучениеоператоры и продажи
+
АналитикаKPI и повторные визиты
+
Масштабтиповой формат для сети
+
+
+ +
+
+
+
+
+
+
+ Персональная конфигурация +

Подберем индивидуальный комплект оборудования точно под формат вашего клуба.

+

Оставьте контакты — проведём первичный аудит потока и площади, предложим состав оборудования, формат размещения и визуальную концепцию.

+
+
состав аппаратов и роли каждого в зоне
+
вариант планировки и внешний вид в интерьере клуба
+
меню программ, обучение и сценарии рекомендаций
+
план запуска и контрольные показатели пилота
+
+
+
+

Получить конфигурацию

+

Выбранный в конструкторе состав автоматически добавится в заявку.

+
+
+
+
+
+
+
+
+
+ + + + +
+
+
+
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/fitnes/package-lock.json b/fitnes/package-lock.json new file mode 100644 index 0000000..ea323f7 --- /dev/null +++ b/fitnes/package-lock.json @@ -0,0 +1,3715 @@ +{ + "name": "exo-fitness-landing", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "exo-fitness-landing", + "version": "1.0.0", + "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "tsx": "^4.23.12", + "zod": "^4.4.3" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/express": "^5.0.6", + "@types/node": "^26.4.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@vitejs/plugin-react": "^6.1.1", + "concurrently": "^10.0.5", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.2.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz", + "integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } + }, + "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.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "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/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", + "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.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", + "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/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.4.0.tgz", + "integrity": "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/body-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.0.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", + "on-finished": "^2.4.1", + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/concurrently": { + "version": "10.0.5", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-10.0.5.tgz", + "integrity": "sha512-JaP/CoftUrCcAFW/g//RbgEGwlelnEae6cfBLgH6ZdO6s8jPkn6p9SB9u6pdVxYXoiSnFqseOlHfrEfF82TVOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "5.6.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "10.2.2", + "tree-kill": "1.2.2", + "yargs": "18.0.0" + }, + "bin": { + "conc": "dist/bin/index.js", + "concurrently": "dist/bin/index.js" + }, + "engines": { + "node": ">=22" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.24.5", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", + "integrity": "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "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-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "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.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "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/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/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "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/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "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/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "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==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "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 || ^0.5.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/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/vite/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/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/fitnes/package.json b/fitnes/package.json new file mode 100644 index 0000000..52a7217 --- /dev/null +++ b/fitnes/package.json @@ -0,0 +1,37 @@ +{ + "name": "exo-fitness-landing", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "concurrently -n client,server -c cyan,magenta \"vite\" \"tsx watch server/src/index.ts\"", + "dev:client": "vite", + "dev:server": "tsx watch server/src/index.ts", + "build": "tsc -b && vite build", + "preview": "vite preview", + "start": "NODE_ENV=production tsx server/src/index.ts", + "typecheck": "tsc -b --force", + "amo:check": "tsx scripts/amo-check.ts" + }, + "dependencies": { + "@fontsource-variable/inter": "^5.3.0", + "dotenv": "^17.4.2", + "express": "^5.2.1", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "tsx": "^4.23.12", + "zod": "^4.4.3" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.3.3", + "@types/express": "^5.0.6", + "@types/node": "^26.4.0", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@vitejs/plugin-react": "^6.1.1", + "concurrently": "^10.0.5", + "tailwindcss": "^4.3.3", + "typescript": "^7.0.2", + "vite": "^8.2.2" + } +} diff --git a/fitnes/public/favicon.svg b/fitnes/public/favicon.svg new file mode 100644 index 0000000..39ed792 --- /dev/null +++ b/fitnes/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fitnes/public/og-image.webp b/fitnes/public/og-image.webp new file mode 100644 index 0000000..e948af5 Binary files /dev/null and b/fitnes/public/og-image.webp differ diff --git a/fitnes/scripts/amo-check.ts b/fitnes/scripts/amo-check.ts new file mode 100644 index 0000000..4f339b8 --- /dev/null +++ b/fitnes/scripts/amo-check.ts @@ -0,0 +1,68 @@ +/** + * Verifies the amoCRM connection and prints what the integration will do with + * your account: which pipeline stage leads land in, and which of your custom + * fields the lead form's data was matched to. + * + * npm run amo:check + */ +import { AmoClient } from '../server/src/amocrm.ts' +import { readAmoConfig } from '../server/src/config.ts' +import { resolveLeadFieldMap } from '../server/src/lead-mapper.ts' + +const config = readAmoConfig() +if (!config) { + console.error('✗ AMO_SUBDOMAIN and AMO_LONG_LIVED_TOKEN must be set in .env') + process.exit(1) +} + +const amo = new AmoClient(config) + +console.info(`→ ${config.baseUrl}\n`) + +const account = await amo.getAccount() +console.info(`✓ Account: ${account?.name} (id ${account?.id})`) + +const pipeline = await amo.getPipeline(config.pipelineId) +if (!pipeline) { + console.error(`✗ Pipeline ${config.pipelineId} not found in this account.`) + process.exit(1) +} + +const stages = [...(pipeline._embedded?.statuses ?? [])].sort((a, b) => a.sort - b.sort) +console.info(`✓ Pipeline ${pipeline.id}: «${pipeline.name}»`) +console.info(` Leads will be created in the first stage: «${stages[0]?.name ?? 'unknown'}»`) +console.info(` All stages: ${stages.map((s) => s.name).join(' → ')}\n`) + +const leadFields = await amo.getLeadFields() +const map = resolveLeadFieldMap(leadFields) + +console.info(`Lead data → amoCRM field mapping (${leadFields.length} custom fields in the account):`) +const keys = [ + 'company', + 'city', + 'club_format', + 'configuration', + 'comment', + 'page', + 'referrer', + 'form', + 'utm_source', + 'utm_medium', + 'utm_campaign', + 'utm_content', + 'utm_term', +] +for (const key of keys) { + const field = map.get(key) + console.info( + field + ? ` ✓ ${key.padEnd(14)} → «${field.name}» (id ${field.id}, ${field.type})` + : ` · ${key.padEnd(14)} → note on the lead`, + ) +} + +const contactFields = await amo.getContactFields() +const hasPhone = contactFields.some((f) => f.code === 'PHONE') +const hasEmail = contactFields.some((f) => f.code === 'EMAIL') +console.info(`\nContact fields: PHONE ${hasPhone ? '✓' : '✗ missing'}, EMAIL ${hasEmail ? '✓' : '✗ missing'}`) +console.info('\nAll good — the form is ready to create leads.') diff --git a/fitnes/server/src/amocrm.ts b/fitnes/server/src/amocrm.ts new file mode 100644 index 0000000..26f9edb --- /dev/null +++ b/fitnes/server/src/amocrm.ts @@ -0,0 +1,247 @@ +import type { AmoConfig } from './config.ts' + +/* -------------------------------------------------------------------------- */ +/* amoCRM REST types (only the parts this integration touches) */ +/* -------------------------------------------------------------------------- */ + +export interface AmoEnum { + id: number + value: string + enum_code?: string | null +} + +export interface AmoCustomField { + id: number + name: string + code: string | null + type: string + enums?: AmoEnum[] | null +} + +interface AmoFieldValue { + value: string | number | boolean | null + enum_id?: number + enum_code?: string +} + +export interface AmoFieldEntry { + field_id?: number + field_code?: string + values: AmoFieldValue[] +} + +export interface AmoContact { + id: number + name?: string + custom_fields_values?: AmoFieldEntry[] | null +} + +export interface AmoLead { + id: number +} + +export class AmoError extends Error { + constructor( + message: string, + readonly status?: number, + readonly detail?: unknown, + ) { + super(message) + this.name = 'AmoError' + } +} + +/* -------------------------------------------------------------------------- */ +/* Client */ +/* -------------------------------------------------------------------------- */ + +const RETRYABLE = new Set([429, 500, 502, 503, 504]) + +export class AmoClient { + private leadFields?: Promise + private contactFields?: Promise + + constructor(private readonly config: AmoConfig) {} + + get baseUrl(): string { + return this.config.baseUrl + } + + /** + * A single amoCRM call. Returns `null` for 204 responses, which amoCRM uses + * for "found nothing" on every collection endpoint. + */ + private async request( + path: string, + init: { method?: string; body?: unknown; query?: Record } = {}, + attempt = 1, + ): Promise { + const url = new URL(path, this.config.baseUrl) + for (const [key, value] of Object.entries(init.query ?? {})) { + if (value !== undefined) url.searchParams.set(key, String(value)) + } + + let response: Response + try { + response = await fetch(url, { + method: init.method ?? 'GET', + headers: { + Authorization: `Bearer ${this.config.token}`, + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + body: init.body === undefined ? undefined : JSON.stringify(init.body), + signal: AbortSignal.timeout(15_000), + }) + } catch (cause) { + if (attempt < 3) { + await sleep(attempt * 400) + return this.request(path, init, attempt + 1) + } + throw new AmoError(`amoCRM unreachable: ${(cause as Error).message}`) + } + + if (response.status === 204) return null + + if (!response.ok) { + const detail = await response.text().catch(() => '') + if (RETRYABLE.has(response.status) && attempt < 3) { + await sleep(attempt * 700) + return this.request(path, init, attempt + 1) + } + if (response.status === 401) { + throw new AmoError('amoCRM rejected the token (401). Regenerate the long-lived token.', 401, detail) + } + throw new AmoError(`amoCRM ${init.method ?? 'GET'} ${path} failed: ${response.status}`, response.status, detail) + } + + return (await response.json()) as T + } + + /* ---------------------------------------------------------------------- */ + /* Field metadata — fetched once per process, then reused */ + /* ---------------------------------------------------------------------- */ + + private async fetchFields(entity: 'leads' | 'contacts'): Promise { + const collected: AmoCustomField[] = [] + for (let page = 1; page <= 10; page++) { + const res = await this.request<{ _embedded?: { custom_fields?: AmoCustomField[] } }>( + `/api/v4/${entity}/custom_fields`, + { query: { page, limit: 250 } }, + ) + const batch = res?._embedded?.custom_fields ?? [] + collected.push(...batch) + if (batch.length < 250) break + } + return collected + } + + getLeadFields(): Promise { + this.leadFields ??= this.fetchFields('leads') + return this.leadFields + } + + getContactFields(): Promise { + this.contactFields ??= this.fetchFields('contacts') + return this.contactFields + } + + /* ---------------------------------------------------------------------- */ + /* Contacts */ + /* ---------------------------------------------------------------------- */ + + /** Full-text search. amoCRM matches phones and emails regardless of format. */ + async findContact(queries: string[]): Promise { + for (const query of queries) { + if (!query) continue + const res = await this.request<{ _embedded?: { contacts?: AmoContact[] } }>('/api/v4/contacts', { + query: { query, limit: 1, with: 'leads' }, + }) + const contact = res?._embedded?.contacts?.[0] + if (contact) return contact + } + return null + } + + async createContact(input: { name: string; fields: AmoFieldEntry[] }): Promise { + const res = await this.request<{ _embedded?: { contacts?: AmoContact[] } }>('/api/v4/contacts', { + method: 'POST', + body: [ + { + name: input.name, + responsible_user_id: this.config.responsibleUserId, + custom_fields_values: input.fields.length ? input.fields : undefined, + }, + ], + }) + const contact = res?._embedded?.contacts?.[0] + if (!contact) throw new AmoError('amoCRM did not return the created contact') + return contact + } + + /** Adds phone/email to an existing contact without dropping what is there. */ + async appendContactFields(contactId: number, fields: AmoFieldEntry[]): Promise { + if (!fields.length) return + await this.request(`/api/v4/contacts/${contactId}`, { + method: 'PATCH', + body: { custom_fields_values: fields }, + }) + } + + /* ---------------------------------------------------------------------- */ + /* Leads */ + /* ---------------------------------------------------------------------- */ + + /** + * Creates the lead in the configured pipeline. Omitting `status_id` makes + * amoCRM drop it into that pipeline's first stage, which is what we want. + */ + async createLead(input: { name: string; contactId: number; fields: AmoFieldEntry[]; tags: string[] }): Promise { + const embedded: Record = { contacts: [{ id: input.contactId }] } + const tags = [...new Set([...this.config.tags, ...input.tags])] + if (tags.length) embedded.tags = tags.map((name) => ({ name })) + + const res = await this.request<{ _embedded?: { leads?: AmoLead[] } }>('/api/v4/leads', { + method: 'POST', + body: [ + { + name: input.name, + pipeline_id: this.config.pipelineId, + responsible_user_id: this.config.responsibleUserId, + custom_fields_values: input.fields.length ? input.fields : undefined, + _embedded: embedded, + }, + ], + }) + const lead = res?._embedded?.leads?.[0] + if (!lead) throw new AmoError('amoCRM did not return the created lead') + return lead + } + + async addLeadNote(leadId: number, text: string): Promise { + await this.request(`/api/v4/leads/${leadId}/notes`, { + method: 'POST', + body: [{ note_type: 'common', params: { text } }], + }) + } + + /* ---------------------------------------------------------------------- */ + /* Diagnostics */ + /* ---------------------------------------------------------------------- */ + + async getAccount(): Promise<{ id: number; name: string; subdomain: string } | null> { + return this.request('/api/v4/account') + } + + async getPipeline(id: number): Promise<{ + id: number + name: string + _embedded?: { statuses?: { id: number; name: string; sort: number }[] } + } | null> { + return this.request(`/api/v4/leads/pipelines/${id}`) + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) +} diff --git a/fitnes/server/src/config.ts b/fitnes/server/src/config.ts new file mode 100644 index 0000000..0bc47cc --- /dev/null +++ b/fitnes/server/src/config.ts @@ -0,0 +1,58 @@ +import { config as loadEnv } from 'dotenv' + +loadEnv() + +function optional(name: string): string | undefined { + const value = process.env[name]?.trim() + return value ? value : undefined +} + +function optionalNumber(name: string): number | undefined { + const raw = optional(name) + if (raw === undefined) return undefined + const value = Number(raw) + if (!Number.isFinite(value)) throw new Error(`${name} must be a number, got "${raw}"`) + return value +} + +/** + * amoCRM settings. Deliberately not throwing when they are missing: the site + * must still boot and serve pages without a CRM connection — only the lead + * endpoint degrades, and it says so explicitly. + */ +export interface AmoConfig { + baseUrl: string + token: string + pipelineId: number + responsibleUserId?: number + tags: string[] +} + +export function readAmoConfig(): AmoConfig | null { + const subdomain = optional('AMO_SUBDOMAIN') + const token = optional('AMO_LONG_LIVED_TOKEN') + if (!subdomain || !token) return null + + // Accepts "exotherapy", "exotherapy.amocrm.ru", or a full URL (an explicit + // scheme is kept as-is, which is what local mocks and self-hosted setups need). + const host = subdomain.replace(/\/+$/, '') + const baseUrl = /^https?:\/\//.test(host) + ? host + : `https://${host.includes('.') ? host : `${host}.${optional('AMO_DOMAIN') ?? 'amocrm.ru'}`}` + + return { + baseUrl, + token, + pipelineId: optionalNumber('AMO_PIPELINE_ID') ?? 10980758, + responsibleUserId: optionalNumber('AMO_RESPONSIBLE_USER_ID'), + tags: (optional('AMO_LEAD_TAGS') ?? '') + .split(',') + .map((t) => t.trim()) + .filter(Boolean), + } +} + +export const serverConfig = { + port: optionalNumber('PORT') ?? 3000, + isProduction: process.env.NODE_ENV === 'production', +} diff --git a/fitnes/server/src/index.ts b/fitnes/server/src/index.ts new file mode 100644 index 0000000..208e5d5 --- /dev/null +++ b/fitnes/server/src/index.ts @@ -0,0 +1,112 @@ +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import express from 'express' +import { leadSchema, type LeadResponse } from '../../shared/lead.ts' +import { AmoClient, AmoError } from './amocrm.ts' +import { readAmoConfig, serverConfig } from './config.ts' +import { LeadService } from './lead-service.ts' +import { createRateLimiter } from './rate-limit.ts' + +const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..') +const clientDir = path.join(rootDir, 'dist/client') + +const amoConfig = readAmoConfig() +const leadService = amoConfig ? new LeadService(new AmoClient(amoConfig)) : null + +if (!leadService) { + console.warn( + '[amo] AMO_SUBDOMAIN / AMO_LONG_LIVED_TOKEN are not set — the site runs, but /api/leads/* will return 503.', + ) +} + +const app = express() +app.set('trust proxy', true) +app.use(express.json({ limit: '64kb' })) + +const limiter = createRateLimiter({ limit: 8, windowMs: 10 * 60 * 1000 }) + +app.get('/api/health', (_req, res) => { + res.json({ ok: true, amo: Boolean(leadService), pipelineId: amoConfig?.pipelineId ?? null }) +}) + +app.post('/api/leads/fitness-centers', async (req, res) => { + const send = (status: number, body: LeadResponse) => res.status(status).json(body) + + const parsed = leadSchema.safeParse(req.body) + if (!parsed.success) { + const fields: Record = {} + for (const issue of parsed.error.issues) { + const key = issue.path.join('.') + if (key && !fields[key]) fields[key] = issue.message + } + return send(400, { ok: false, error: 'Проверьте заполненные поля.', fields }) + } + + const payload = parsed.data + + // Honeypot: answer exactly like a success so bots learn nothing. + if (payload.website) { + console.info('[lead] honeypot triggered from %s', req.ip) + return send(200, { ok: true, leadId: 0, contactId: 0 }) + } + + const { allowed, retryAfterSeconds } = limiter(req.ip ?? 'unknown') + if (!allowed) { + res.setHeader('Retry-After', String(retryAfterSeconds)) + return send(429, { ok: false, error: 'Слишком много заявок с этого адреса. Попробуйте позже.' }) + } + + if (!leadService) { + // Never drop a real lead silently — it must be findable in the logs. + console.error('[lead] amoCRM is not configured. Lead payload:', JSON.stringify(payload)) + return send(503, { ok: false, error: 'Форма временно недоступна. Позвоните нам: +7 939 717-80-80.' }) + } + + try { + const result = await leadService.submit(payload) + console.info( + '[lead] amoCRM lead %d (contact %d, %s)', + result.leadId, + result.contactId, + result.contactCreated ? 'new contact' : 'existing contact', + ) + return send(200, { ok: true, leadId: result.leadId, contactId: result.contactId }) + } catch (error) { + console.error('[lead] amoCRM submission failed:', error) + console.error('[lead] payload was:', JSON.stringify(payload)) + const message = + error instanceof AmoError && error.status === 401 + ? 'CRM отклонила запрос. Мы уже разбираемся — позвоните нам: +7 939 717-80-80.' + : 'Не удалось отправить заявку. Позвоните нам: +7 939 717-80-80.' + return send(502, { ok: false, error: message }) + } +}) + +if (serverConfig.isProduction) { + app.use( + express.static(clientDir, { + index: false, + setHeaders(res, filePath) { + // Vite fingerprints everything under /assets, so it can be cached hard. + if (filePath.includes(`${path.sep}assets${path.sep}`)) { + res.setHeader('Cache-Control', 'public, max-age=31536000, immutable') + } + }, + }), + ) + + // SPA fallback. Written as middleware because Express 5 no longer accepts a + // bare '*' route pattern. + app.use((req, res, next) => { + if (req.method !== 'GET' || req.path.startsWith('/api/')) return next() + res.sendFile(path.join(clientDir, 'index.html')) + }) +} + +app.listen(serverConfig.port, () => { + console.info( + '[server] listening on http://localhost:%d%s', + serverConfig.port, + serverConfig.isProduction ? ` (serving ${path.relative(rootDir, clientDir)})` : ' (API only — run vite for the UI)', + ) +}) diff --git a/fitnes/server/src/lead-mapper.ts b/fitnes/server/src/lead-mapper.ts new file mode 100644 index 0000000..62f20c7 --- /dev/null +++ b/fitnes/server/src/lead-mapper.ts @@ -0,0 +1,153 @@ +import type { LeadPayload } from '../../shared/lead.ts' +import type { AmoCustomField, AmoFieldEntry } from './amocrm.ts' + +/** Human labels used in the fallback note. */ +const LABELS: Record = { + company: 'Фитнес-клуб / сеть', + email: 'Email', + phone: 'Телефон', + city: 'Город', + club_format: 'Формат', + comment: 'Комментарий', + configuration: 'Конфигурация из конструктора', + page: 'Страница', + referrer: 'Источник перехода', + form: 'Форма', + utm_source: 'utm_source', + utm_medium: 'utm_medium', + utm_campaign: 'utm_campaign', + utm_content: 'utm_content', + utm_term: 'utm_term', +} + +/** + * Candidate amoCRM field codes / name fragments for each piece of data we + * collect. The first field in the account that matches wins; anything without + * a match falls through to the lead note instead. + */ +const CANDIDATES: Record = { + company: { codes: ['COMPANY', 'COMPANY_NAME'], names: ['компания', 'организация', 'название клуба', 'клуб', 'сеть'] }, + city: { codes: ['CITY'], names: ['город'] }, + club_format: { codes: ['CLUB_FORMAT', 'FORMAT'], names: ['формат клуба', 'формат', 'тип клуба'] }, + configuration: { + codes: ['CONFIGURATION', 'EQUIPMENT'], + names: ['конфигурация', 'комплектация', 'состав оборудования', 'оборудование'], + }, + comment: { codes: ['COMMENT', 'MESSAGE', 'DESCRIPTION'], names: ['комментарий', 'сообщение', 'описание заявки'] }, + page: { codes: ['REFERRER', 'PAGE'], names: ['страница', 'посадочная страница', 'url страницы'] }, + referrer: { codes: ['REFERER_URL'], names: ['источник перехода', 'referrer', 'реферер'] }, + form: { codes: ['FORMNAME', 'FORM_NAME'], names: ['название формы', 'форма'] }, + utm_source: { codes: ['UTM_SOURCE'], names: ['utm_source'] }, + utm_medium: { codes: ['UTM_MEDIUM'], names: ['utm_medium'] }, + utm_campaign: { codes: ['UTM_CAMPAIGN'], names: ['utm_campaign'] }, + utm_content: { codes: ['UTM_CONTENT'], names: ['utm_content'] }, + utm_term: { codes: ['UTM_TERM'], names: ['utm_term'] }, +} + +/** Field types we know how to write a plain string into. */ +const TEXTUAL = new Set(['text', 'textarea', 'url', 'tracking_data', 'numeric', 'price', 'monetary']) +const ENUMERATED = new Set(['select', 'radiobutton', 'multiselect']) + +const normalise = (value: string) => + value + .toLowerCase() + .replace(/ё/g, 'е') + .replace(/[^a-zа-я0-9]+/gi, ' ') + .trim() + +export type LeadFieldMap = Map + +/** Picks one account field per payload key. A field is never used twice. */ +export function resolveLeadFieldMap(fields: AmoCustomField[]): LeadFieldMap { + const map: LeadFieldMap = new Map() + const taken = new Set() + + for (const [key, candidate] of Object.entries(CANDIDATES)) { + const byCode = fields.find( + (f) => f.code && candidate.codes.includes(f.code.toUpperCase()) && !taken.has(f.id), + ) + const match = + byCode ?? + fields.find((f) => { + if (taken.has(f.id)) return false + const name = normalise(f.name) + return candidate.names.some((n) => name === normalise(n)) + }) ?? + fields.find((f) => { + if (taken.has(f.id)) return false + const name = normalise(f.name) + return candidate.names.some((n) => name.includes(normalise(n))) + }) + + if (!match) continue + if (!TEXTUAL.has(match.type) && !ENUMERATED.has(match.type)) continue + map.set(key, match) + taken.add(match.id) + } + + return map +} + +/** Builds one amoCRM field entry, or null when the value cannot be represented. */ +function toFieldEntry(field: AmoCustomField, value: string): AmoFieldEntry | null { + if (ENUMERATED.has(field.type)) { + const target = normalise(value) + const option = field.enums?.find((e) => normalise(e.value) === target) + // An unmatched option would silently create garbage, so let it hit the note. + return option ? { field_id: field.id, values: [{ value: option.value, enum_id: option.id }] } : null + } + + if (field.type === 'numeric' || field.type === 'price' || field.type === 'monetary') { + const numeric = Number(value.replace(/[^\d.,-]/g, '').replace(',', '.')) + return Number.isFinite(numeric) ? { field_id: field.id, values: [{ value: numeric }] } : null + } + + return { field_id: field.id, values: [{ value }] } +} + +export interface MappedLead { + fields: AmoFieldEntry[] + /** Everything that had no matching field, ready to be written as a note. */ + note: string +} + +export function mapLead(payload: LeadPayload, fieldMap: LeadFieldMap): MappedLead { + const data: Record = { + company: payload.company, + city: payload.city, + club_format: payload.club_format, + configuration: payload.configuration, + comment: payload.comment, + page: payload.page, + referrer: payload.referrer, + form: payload.form === 'callback' ? 'Быстрый обратный звонок' : 'Заявка на конфигурацию', + ...payload.utm, + } + + const fields: AmoFieldEntry[] = [] + const leftovers: string[] = [] + + for (const [key, value] of Object.entries(data)) { + if (!value) continue + const field = fieldMap.get(key) + const entry = field ? toFieldEntry(field, value) : null + if (entry) fields.push(entry) + else leftovers.push(`${LABELS[key] ?? key}: ${value}`) + } + + // Contact details are always repeated in the note so a manager can read the + // whole request without opening the linked contact card. + const header = [ + `Заявка с лендинга «EXO Recovery Zone для фитнес-клубов»`, + `${LABELS.phone}: ${payload.phone}`, + payload.email ? `${LABELS.email}: ${payload.email}` : undefined, + ].filter(Boolean) as string[] + + return { fields, note: [...header, ...leftovers].join('\n') } +} + +/** Lead title shown in the pipeline. */ +export function buildLeadName(payload: LeadPayload): string { + const who = payload.company ?? payload.name + return payload.form === 'callback' ? `Обратный звонок — ${who}` : `Recovery Zone — ${who}` +} diff --git a/fitnes/server/src/lead-service.ts b/fitnes/server/src/lead-service.ts new file mode 100644 index 0000000..2ffa5a3 --- /dev/null +++ b/fitnes/server/src/lead-service.ts @@ -0,0 +1,124 @@ +import type { LeadPayload } from '../../shared/lead.ts' +import { AmoClient, type AmoContact, type AmoFieldEntry } from './amocrm.ts' +import { buildLeadName, mapLead, resolveLeadFieldMap, type LeadFieldMap } from './lead-mapper.ts' + +const digitsOnly = (value: string) => value.replace(/\D/g, '') + +/** + * Last ten digits — the part that is stable across "+7 (999) 123-45-67", + * "8 999 1234567" and "9991234567". Used to decide whether amoCRM already + * knows a phone number. + */ +const phoneKey = (value: string) => digitsOnly(value).slice(-10) + +/** + * amoCRM's full-text search matches digit substrings, but a contact stored as + * "+7 999…" is not found by a query starting with "8 999…". Try every spelling + * a Russian visitor might type. + */ +function phoneQueries(phone: string): string[] { + const digits = digitsOnly(phone) + const queries = new Set([phone.trim(), digits]) + if (digits.length >= 10) { + const national = digits.slice(-10) + queries.add(national) + queries.add(`7${national}`) + queries.add(`8${national}`) + } + return [...queries].filter(Boolean) +} + +/** Reads the values already stored in a contact's standard multitext field. */ +function existingValues(contact: AmoContact, code: 'PHONE' | 'EMAIL'): string[] { + const entry = contact.custom_fields_values?.find((f) => f.field_code === code) + return (entry?.values ?? []).map((v) => String(v.value ?? '')).filter(Boolean) +} + +function contactFieldEntries(payload: LeadPayload): AmoFieldEntry[] { + const entries: AmoFieldEntry[] = [ + { field_code: 'PHONE', values: [{ value: payload.phone, enum_code: 'WORK' }] }, + ] + if (payload.email) { + entries.push({ field_code: 'EMAIL', values: [{ value: payload.email, enum_code: 'WORK' }] }) + } + return entries +} + +export interface CreatedLead { + leadId: number + contactId: number + contactCreated: boolean +} + +export class LeadService { + private fieldMap?: Promise + + constructor(private readonly amo: AmoClient) {} + + private getFieldMap(): Promise { + this.fieldMap ??= this.amo.getLeadFields().then(resolveLeadFieldMap) + return this.fieldMap + } + + async submit(payload: LeadPayload): Promise { + const fieldMap = await this.getFieldMap() + + // 1. Reuse the existing contact when the phone or email is already known. + const existing = await this.amo.findContact([...phoneQueries(payload.phone), payload.email ?? '']) + + let contactId: number + let contactCreated = false + + if (existing) { + contactId = existing.id + // Add whichever channel amoCRM does not have on file yet. + const missing: AmoFieldEntry[] = [] + const knownPhones = existingValues(existing, 'PHONE').map(phoneKey) + if (!knownPhones.includes(phoneKey(payload.phone))) { + missing.push({ + field_code: 'PHONE', + values: [ + ...existingValues(existing, 'PHONE').map((value) => ({ value })), + { value: payload.phone, enum_code: 'WORK' }, + ], + }) + } + if (payload.email) { + const knownEmails = existingValues(existing, 'EMAIL').map((e) => e.toLowerCase()) + if (!knownEmails.includes(payload.email.toLowerCase())) { + missing.push({ + field_code: 'EMAIL', + values: [ + ...existingValues(existing, 'EMAIL').map((value) => ({ value })), + { value: payload.email, enum_code: 'WORK' }, + ], + }) + } + } + await this.amo.appendContactFields(contactId, missing) + } else { + const created = await this.amo.createContact({ name: payload.name, fields: contactFieldEntries(payload) }) + contactId = created.id + contactCreated = true + } + + // 2. Lead in the configured pipeline, first stage. + const { fields, note } = mapLead(payload, fieldMap) + const lead = await this.amo.createLead({ + name: buildLeadName(payload), + contactId, + fields, + tags: [payload.form === 'callback' ? 'обратный звонок' : 'заявка с сайта'], + }) + + // 3. Everything the account has no field for lands in a readable note. + // A failed note must not fail the lead — the lead is the valuable part. + if (note) { + await this.amo.addLeadNote(lead.id, note).catch((error: unknown) => { + console.warn('[amo] lead %d created but the note failed:', lead.id, error) + }) + } + + return { leadId: lead.id, contactId, contactCreated } + } +} diff --git a/fitnes/server/src/rate-limit.ts b/fitnes/server/src/rate-limit.ts new file mode 100644 index 0000000..9a01b13 --- /dev/null +++ b/fitnes/server/src/rate-limit.ts @@ -0,0 +1,33 @@ +interface Bucket { + count: number + resetAt: number +} + +/** + * Minimal fixed-window limiter for the public lead endpoint. In-memory on + * purpose: a single landing page behind one Node process does not warrant a + * shared store, and losing the counters on restart is harmless. + */ +export function createRateLimiter(options: { limit: number; windowMs: number }) { + const buckets = new Map() + + return function check(key: string): { allowed: boolean; retryAfterSeconds: number } { + const now = Date.now() + + if (buckets.size > 5000) { + for (const [k, bucket] of buckets) if (bucket.resetAt <= now) buckets.delete(k) + } + + const bucket = buckets.get(key) + if (!bucket || bucket.resetAt <= now) { + buckets.set(key, { count: 1, resetAt: now + options.windowMs }) + return { allowed: true, retryAfterSeconds: 0 } + } + + bucket.count += 1 + return { + allowed: bucket.count <= options.limit, + retryAfterSeconds: Math.ceil((bucket.resetAt - now) / 1000), + } + } +} diff --git a/fitnes/shared/lead.ts b/fitnes/shared/lead.ts new file mode 100644 index 0000000..0cf3ee4 --- /dev/null +++ b/fitnes/shared/lead.ts @@ -0,0 +1,44 @@ +import { z } from 'zod' + +/** Which form on the page produced the lead. Surfaces as an amoCRM tag. */ +export const leadFormIds = ['request', 'callback'] as const +export type LeadFormId = (typeof leadFormIds)[number] + +export const utmKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'] as const + +const optionalText = (max: number) => + z + .string() + .trim() + .max(max) + .optional() + .transform((v) => (v ? v : undefined)) + +export const leadSchema = z.object({ + form: z.enum(leadFormIds), + name: z.string().trim().min(1, 'Укажите имя').max(200), + phone: z.string().trim().min(5, 'Укажите телефон').max(50), + email: z + .union([z.email().max(200), z.literal('')]) + .optional() + .transform((v) => (v ? v : undefined)), + company: optionalText(200), + city: optionalText(120), + club_format: optionalText(120), + comment: optionalText(4000), + configuration: optionalText(600), + page: optionalText(300), + referrer: optionalText(500), + // partialRecord, not record: z.record over an enum requires every key. + utm: z.partialRecord(z.enum(utmKeys), z.string().max(300)).optional(), + /** Honeypot. Bots fill it in; humans never see it. */ + website: z.string().max(200).optional(), +}) + +export type LeadPayload = z.infer +/** What the client sends — before zod's optional/empty-string normalisation. */ +export type LeadInput = z.input + +export type LeadResponse = + | { ok: true; leadId: number; contactId: number } + | { ok: false; error: string; fields?: Record } diff --git a/fitnes/src/App.tsx b/fitnes/src/App.tsx new file mode 100644 index 0000000..d19d311 --- /dev/null +++ b/fitnes/src/App.tsx @@ -0,0 +1,56 @@ +import { useCallback, useState } from 'react' +import { BenefitsSection } from './components/BenefitsSection' +import { CallbackModal } from './components/CallbackModal' +import { ConstructorSection } from './components/ConstructorSection' +import { EconomicsSection } from './components/EconomicsSection' +import { GallerySection, type GalleryImage } from './components/GallerySection' +import { Hero } from './components/Hero' +import { ImageModal } from './components/ImageModal' +import { MarketSection } from './components/MarketSection' +import { MobileCta } from './components/MobileCta' +import { ProgramsSection } from './components/ProgramsSection' +import { RequestSection } from './components/RequestSection' +import { SiteFooter } from './components/SiteFooter' +import { SiteHeader } from './components/SiteHeader' +import { WhyExoSection } from './components/WhyExoSection' +import { useConstructor } from './hooks/useConstructor' +import { useScrollState } from './hooks/useScrollState' + +export default function App() { + const { scrolled, showMobileCta } = useScrollState() + const constructor = useConstructor() + + const [lightbox, setLightbox] = useState(null) + const [callbackOpen, setCallbackOpen] = useState(false) + const [commentPrefill, setCommentPrefill] = useState('') + + const openCallback = useCallback(() => setCallbackOpen(true), []) + const closeCallback = useCallback(() => setCallbackOpen(false), []) + const closeLightbox = useCallback(() => setLightbox(null), []) + + return ( + <> + + +
+ + + + + setCommentPrefill(constructor.requestComment)} + /> + + + + +
+ + + + + + + ) +} diff --git a/fitnes/src/assets/images/config-five.webp b/fitnes/src/assets/images/config-five.webp new file mode 100644 index 0000000..63032da Binary files /dev/null and b/fitnes/src/assets/images/config-five.webp differ diff --git a/fitnes/src/assets/images/config-three.webp b/fitnes/src/assets/images/config-three.webp new file mode 100644 index 0000000..2978ced Binary files /dev/null and b/fitnes/src/assets/images/config-three.webp differ diff --git a/fitnes/src/assets/images/device-hydrogen.webp b/fitnes/src/assets/images/device-hydrogen.webp new file mode 100644 index 0000000..69943ab Binary files /dev/null and b/fitnes/src/assets/images/device-hydrogen.webp differ diff --git a/fitnes/src/assets/images/device-magnet.webp b/fitnes/src/assets/images/device-magnet.webp new file mode 100644 index 0000000..39c7ff3 Binary files /dev/null and b/fitnes/src/assets/images/device-magnet.webp differ diff --git a/fitnes/src/assets/images/device-press.webp b/fitnes/src/assets/images/device-press.webp new file mode 100644 index 0000000..73ae871 Binary files /dev/null and b/fitnes/src/assets/images/device-press.webp differ diff --git a/fitnes/src/assets/images/device-tecar.webp b/fitnes/src/assets/images/device-tecar.webp new file mode 100644 index 0000000..c64061e Binary files /dev/null and b/fitnes/src/assets/images/device-tecar.webp differ diff --git a/fitnes/src/assets/images/device-therapy.webp b/fitnes/src/assets/images/device-therapy.webp new file mode 100644 index 0000000..bc43f25 Binary files /dev/null and b/fitnes/src/assets/images/device-therapy.webp differ diff --git a/fitnes/src/assets/images/exo-logo.png b/fitnes/src/assets/images/exo-logo.png new file mode 100644 index 0000000..d1a7c79 Binary files /dev/null and b/fitnes/src/assets/images/exo-logo.png differ diff --git a/fitnes/src/assets/images/gallery-flagship.webp b/fitnes/src/assets/images/gallery-flagship.webp new file mode 100644 index 0000000..156a554 Binary files /dev/null and b/fitnes/src/assets/images/gallery-flagship.webp differ diff --git a/fitnes/src/assets/images/gallery-glass-zone.webp b/fitnes/src/assets/images/gallery-glass-zone.webp new file mode 100644 index 0000000..0d5fccc Binary files /dev/null and b/fitnes/src/assets/images/gallery-glass-zone.webp differ diff --git a/fitnes/src/assets/images/gallery-premium-room.webp b/fitnes/src/assets/images/gallery-premium-room.webp new file mode 100644 index 0000000..1d3958e Binary files /dev/null and b/fitnes/src/assets/images/gallery-premium-room.webp differ diff --git a/fitnes/src/assets/images/gallery-pt-stretch.webp b/fitnes/src/assets/images/gallery-pt-stretch.webp new file mode 100644 index 0000000..7adbecb Binary files /dev/null and b/fitnes/src/assets/images/gallery-pt-stretch.webp differ diff --git a/fitnes/src/assets/images/hero-bg.webp b/fitnes/src/assets/images/hero-bg.webp new file mode 100644 index 0000000..23ebb2b Binary files /dev/null and b/fitnes/src/assets/images/hero-bg.webp differ diff --git a/fitnes/src/components/BenefitsSection.tsx b/fitnes/src/components/BenefitsSection.tsx new file mode 100644 index 0000000..870c844 --- /dev/null +++ b/fitnes/src/components/BenefitsSection.tsx @@ -0,0 +1,63 @@ +import { benefits, glossary } from '../data/content' +import { useReveal } from '../hooks/useReveal' +import { cx } from '../lib/cx' +import { Container, Section, SectionHead } from './Layout' + +export function BenefitsSection() { + const terms = useReveal() + + return ( +
+ + + +
+ {benefits.map((benefit) => ( + + ))} +
+ +
+ {glossary.map((item) => ( +
+ {item.term} + {item.text} +
+ ))} +
+
+
+ ) +} + +function BenefitCard({ index, title, text }: (typeof benefits)[number]) { + const { ref, revealClass } = useReveal() + + return ( +
+
{index}
+

{title}

+

{text}

+
+ ) +} diff --git a/fitnes/src/components/Button.tsx b/fitnes/src/components/Button.tsx new file mode 100644 index 0000000..edb09e9 --- /dev/null +++ b/fitnes/src/components/Button.tsx @@ -0,0 +1,66 @@ +import type { AnchorHTMLAttributes, ButtonHTMLAttributes, ReactNode } from 'react' +import { cx } from '../lib/cx' + +export type ButtonVariant = 'primary' | 'ghost' | 'outline' + +const base = + 'inline-flex cursor-pointer items-center justify-center gap-[10px] rounded-full border border-transparent font-extrabold ' + + 'transition-[transform,box-shadow,background-color,color,border-color] duration-250 hover:-translate-y-[2px] ' + + '[&>svg]:size-[19px] [&>svg]:shrink-0' + +/** + * Sizing gets its own slot rather than living in `base`. Tailwind resolves + * conflicting utilities by their order in the stylesheet, not by the order + * they appear in `className`, so a `min-h-[42px]` passed next to the default + * `min-h-[54px]` would silently lose. Replacing the slot means the losing + * class is never emitted in the first place. + */ +const defaultSize = 'min-h-[54px] px-[22px]' + +const variants: Record = { + primary: + 'bg-teal text-navy-deep shadow-[0_12px_34px_rgb(0_196_180/0.25)] hover:bg-teal-bright hover:shadow-[0_16px_40px_rgb(0_196_180/0.34)]', + ghost: + 'border-white/[0.26] bg-white/[0.06] text-white backdrop-blur-[12px] hover:border-white/[0.55] hover:bg-white/[0.11]', + outline: 'border-navy/20 bg-white text-navy hover:border-teal', +} + +export function buttonClass(variant: ButtonVariant, className?: string, size: string = defaultSize) { + return cx(base, size, variants[variant], className) +} + +interface CommonProps { + variant?: ButtonVariant + /** Replaces the default min-height/padding pair. See `defaultSize` above. */ + size?: string + children: ReactNode + className?: string +} + +export function Button({ + variant = 'primary', + size, + className, + children, + ...rest +}: CommonProps & Omit, 'size'>) { + return ( + + ) +} + +export function ButtonLink({ + variant = 'primary', + size, + className, + children, + ...rest +}: CommonProps & AnchorHTMLAttributes) { + return ( + + {children} + + ) +} diff --git a/fitnes/src/components/CallbackModal.tsx b/fitnes/src/components/CallbackModal.tsx new file mode 100644 index 0000000..353760f --- /dev/null +++ b/fitnes/src/components/CallbackModal.tsx @@ -0,0 +1,147 @@ +import { useCallback, useEffect, useRef, useState, type FormEvent } from 'react' +import { contacts } from '../data/content' +import { useBodyLock } from '../hooks/useBodyLock' +import { useEscapeKey } from '../hooks/useEscapeKey' +import { submitLead } from '../lib/lead' +import { cx } from '../lib/cx' +import { Button } from './Button' +import { ConsentCheckbox, Honeypot, TextField } from './FormField' +import { ArrowRightIcon } from './Icons' + +type Status = { text: string; tone: 'idle' | 'success' | 'error' } + +/** + * Compact second entry point: name + phone only. Leads land in the same + * amoCRM pipeline, tagged so they can be told apart from the full request. + */ +export function CallbackModal({ open, onClose }: { open: boolean; onClose: () => void }) { + const formRef = useRef(null) + const firstFieldRef = useRef(null) + const [name, setName] = useState('') + const [phone, setPhone] = useState('') + const [consent, setConsent] = useState(false) + const [honeypot, setHoneypot] = useState('') + const [pending, setPending] = useState(false) + const [status, setStatus] = useState({ text: '', tone: 'idle' }) + + const close = useCallback(() => onClose(), [onClose]) + useBodyLock(open) + useEscapeKey(open, close) + + useEffect(() => { + if (open) firstFieldRef.current?.focus() + }, [open]) + + if (!open) return null + + async function onSubmit(event: FormEvent) { + event.preventDefault() + const form = formRef.current + if (!form) return + + if (!form.checkValidity()) { + form.reportValidity() + setStatus({ text: 'Проверьте обязательные поля и согласие.', tone: 'error' }) + return + } + + if (honeypot) return + + setPending(true) + setStatus({ text: '', tone: 'idle' }) + + const result = await submitLead('callback', { name, phone, website: honeypot }) + + if (result.ok) { + setName('') + setPhone('') + setConsent(false) + setStatus({ text: 'Спасибо. Мы перезвоним в ближайшее рабочее время.', tone: 'success' }) + } else { + setStatus({ text: result.error ?? 'Не удалось отправить заявку.', tone: 'error' }) + } + + setPending(false) + } + + return ( +
{ + if (event.target === event.currentTarget) close() + }} + className="fixed inset-0 z-100 flex items-center justify-center bg-[#020D16]/[0.92] p-[18px] backdrop-blur-[10px]" + > +
+ + +

+ Перезвоним вам +

+

+ Оставьте имя и телефон — специалист свяжется и ответит на вопросы по зоне восстановления. Или позвоните сами:{' '} + + {contacts.phone} + + . +

+ +
+
+ setName(event.target.value)} + /> + setPhone(event.target.value)} + /> +
+ + + + + + +
+ {status.text} +
+ +
+
+ ) +} diff --git a/fitnes/src/components/ConstructorSection.tsx b/fitnes/src/components/ConstructorSection.tsx new file mode 100644 index 0000000..9fff8e0 --- /dev/null +++ b/fitnes/src/components/ConstructorSection.tsx @@ -0,0 +1,236 @@ +import { useEffect, useRef, useState } from 'react' +import { configImages, deviceIds, devices, presetIds, presets } from '../data/devices' +import type { ConstructorState } from '../hooks/useConstructor' +import { useReveal } from '../hooks/useReveal' +import { cx } from '../lib/cx' +import { Button, ButtonLink } from './Button' +import { ArrowRightIcon, CheckIcon } from './Icons' +import { Container, Label, Section, SectionHead } from './Layout' + +/** The original used two different word forms for the two modes. */ +const MODES = [ + { value: 3, label: '3 аппарата' }, + { value: 5, label: '5 аппаратов' }, +] as const + +export function ConstructorSection({ + state, + onRequestConfiguration, +}: { + state: ConstructorState + onRequestConfiguration: () => void +}) { + const shell = useReveal() + const image = useFadingImage(state.mode === 5 ? configImages.five : configImages.three) + + return ( +
+ + + +
+
+
+ +

+ Нажмите на аппараты — справа изменится состав и логика зоны. +

+
+ +
+ {MODES.map(({ value, label }) => ( + + ))} +
+
+ +
+
+
+
+

Состав зоны

+

{state.hint}

+
+
+ {state.selected.length} / {state.mode} +
+
+ +
+ {deviceIds.map((id) => { + const device = devices[id] + const active = state.selected.includes(id) + return ( + + ) + })} +
+ + {state.mode === 3 ? ( +
+ {presetIds.map((id) => ( + + ))} +
+ ) : null} +
+ +
+
+ {image.alt} + + {state.tag} + + + Пример визуализации. Итоговый рендер создаётся под выбранный состав и интерьер клуба. + +
+ +
+
+

{state.title}

+

{state.subtitle}

+
+
{state.number}
+
+ +
+ {state.names.map((name) => ( + + {name} + + ))} +
+ +
+ {state.benefits.map((benefit) => ( +
+ + + + {benefit} +
+ ))} +
+ +
+ + Получить эту конфигурацию + + + +
+
+
+
+
+
+ ) +} + +/** + * Cross-fades the render when the mode changes, mirroring the original's + * "fade out, swap src, fade in" timing. + */ +function useFadingImage(target: { src: string; alt: string }) { + const [shown, setShown] = useState(target) + const [opacity, setOpacity] = useState(1) + const timer = useRef>(undefined) + + useEffect(() => { + if (target.src === shown.src) return + setOpacity(0) + timer.current = setTimeout(() => { + setShown(target) + setOpacity(1) + }, 180) + return () => clearTimeout(timer.current) + }, [target, shown.src]) + + return { ...shown, opacity } +} diff --git a/fitnes/src/components/EconomicsSection.tsx b/fitnes/src/components/EconomicsSection.tsx new file mode 100644 index 0000000..99e16ec --- /dev/null +++ b/fitnes/src/components/EconomicsSection.tsx @@ -0,0 +1,333 @@ +import { useState } from 'react' +import { econFormula, econGain, econLoss, econMetrics, econOps } from '../data/content' +import { useReveal } from '../hooks/useReveal' +import { formatMoney, formatNumber, parseAmount } from '../lib/format' +import { cx } from '../lib/cx' +import { Container, Section } from './Layout' + +const panelItem = 'grid grid-cols-[34px_1fr] items-start gap-[11px] rounded-[16px] p-[13px]' +const badge = 'grid size-[28px] place-items-center rounded-[10px] text-[11px]' + +export function EconomicsSection() { + const head = useReveal() + const metrics = useReveal() + const loss = useReveal() + const gain = useReveal() + const bridge = useReveal() + + return ( +
+
+ ) +} + +function RevenueCalculator() { + const { ref, revealClass } = useReveal() + const [programs, setPrograms] = useState('') + const [check, setCheck] = useState('') + const [days, setDays] = useState('30') + + const programsValue = parseAmount(programs) + const checkValue = parseAmount(check) + const daysValue = parseAmount(days) + const ready = programsValue > 0 && checkValue > 0 && daysValue > 0 + const volume = programsValue * daysValue + const month = volume * checkValue + + return ( +
+
+ Простой расчёт +

Сколько зона восстановления может приносить клубу

+

+ Без процентов и сложных метрик: укажите продажи в день, средний чек и количество рабочих дней. +

+
+ +
+ {econFormula.map((step, index) => ( + + ))} +
+ +
+
+ + + +
+ +
+ + +
+
+ +

+ Расчёт показывает сценарную выручку, а не финансовую гарантию. Итоговая модель уточняется по конфигурации + аппаратов, тарифам, ФОТ, загрузке и формату работы Recovery Zone. +

+
+ ) +} + +function FormulaFragment({ + step, + index, + isResult, +}: { + step: (typeof econFormula)[number] + index: number + isResult: boolean +}) { + return ( + <> + {index > 0 ? ( + + {isResult ? '=' : '×'} + + ) : null} +
+ + {step.badge} + + + {step.lines[0]} +
+ {step.lines[1]} +
+
+ + ) +} + +function CalcField({ + id, + label, + hint, + value, + onChange, + placeholder, + min, + max, + step, +}: { + id: string + label: string + hint: string + value: string + onChange: (value: string) => void + placeholder?: string + min?: number + max?: number + step?: number +}) { + return ( +
+ + onChange(event.target.value)} + className="h-[48px] w-full min-w-0 rounded-[13px] border border-white/[0.17] bg-white/[0.075] px-[13px] text-[15px] font-extrabold text-white tabular-nums outline-none placeholder:text-white/[0.28] focus:border-teal focus:shadow-[0_0_0_3px_rgb(0_196_180/0.13)]" + /> + {hint} +
+ ) +} + +function CalcResult({ label, value, note }: { label: string; value: string; note: string }) { + return ( +
+ {label} + + {value} + + {note} +
+ ) +} diff --git a/fitnes/src/components/FormField.tsx b/fitnes/src/components/FormField.tsx new file mode 100644 index 0000000..b4c9944 --- /dev/null +++ b/fitnes/src/components/FormField.tsx @@ -0,0 +1,102 @@ +import type { ComponentPropsWithRef, ReactNode } from 'react' +import { cx } from '../lib/cx' + +export const controlClass = + 'w-full rounded-[15px] border border-navy/15 bg-field px-[15px] py-[14px] text-ink outline-none ' + + 'transition-[border-color,box-shadow,background-color] duration-200 ' + + 'focus:border-teal focus:bg-white focus:shadow-[0_0_0_4px_rgb(0_196_180/0.10)]' + +function Field({ id, label, full, children }: { id: string; label: string; full?: boolean; children: ReactNode }) { + return ( +
+ + {children} +
+ ) +} + +export function TextField({ + id, + label, + full, + ...rest +}: { id: string; label: string; full?: boolean } & ComponentPropsWithRef<'input'>) { + return ( + + + + ) +} + +export function SelectField({ + id, + label, + options, + full, + ...rest +}: { id: string; label: string; options: string[]; full?: boolean } & ComponentPropsWithRef<'select'>) { + return ( + + + + ) +} + +export function TextareaField({ + id, + label, + full, + ...rest +}: { id: string; label: string; full?: boolean } & ComponentPropsWithRef<'textarea'>) { + return ( + +