From c090bfcf472731e1fdba470a2332be3db6e89865 Mon Sep 17 00:00:00 2001
From: Sergey Zotov
Date: Mon, 13 Apr 2026 01:20:24 +0500
Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20Aspekter=20VA=20?=
=?UTF-8?q?email=20builder?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Full project: Svelte 5 frontend, Vite 7 backend API,
Pug email templates (email-gen), Docker deployment.
Co-Authored-By: Claude Opus 4.6 (1M context)
---
.env.prod.example | 2 +
.gitignore | 21 +
DOCKER-DEPLOY.md | 117 +
deploy/email-gen-api/Dockerfile | 14 +
deploy/email-gen-api/entrypoint.sh | 14 +
deploy/email-gen-api/server.js | 197 +
deploy/nginx/default.conf | 25 +
deploy/nginx/favicon.jpg | Bin 0 -> 1157 bytes
deploy/nginx/landing.html | 907 +++
deploy/scripts/update-email-gen.sh | 32 +
docker-compose.dev.yml | 7 +
docker-compose.prod.yml | 24 +
docker-compose.yml | 29 +
docs/01-user-guide.md | 238 +
docs/02-developer-guide.md | 574 ++
docs/applyNowrap.md | 168 +
email-gen | 1 +
email-gen-overrides/README.md | 16 +
.../reaspekt-master/blocks/_factory.pug | 323 +
.../reaspekt-master/blocks/buttons.pug | 77 +
.../reaspekt-master/blocks/other-ext.pug | 29 +
.../reaspekt-master/blocks/texts-ext.pug | 222 +
.../reaspekt-master/blocks/texts.pug | 83 +
z51-pug-builder/.dockerignore | 3 +
z51-pug-builder/.gitignore | 24 +
z51-pug-builder/.vscode/extensions.json | 3 +
z51-pug-builder/Dockerfile | 13 +
z51-pug-builder/README.md | 243 +
z51-pug-builder/index.html | 16 +
z51-pug-builder/jsconfig.json | 33 +
z51-pug-builder/package-lock.json | 2051 +++++
z51-pug-builder/package.json | 21 +
z51-pug-builder/public/Block.pug | 156 +
z51-pug-builder/public/favicon.jpg | Bin 0 -> 1157 bytes
z51-pug-builder/public/login-bg/2.jpg | Bin 0 -> 130040 bytes
z51-pug-builder/public/login-bg/3.jpg | Bin 0 -> 172158 bytes
z51-pug-builder/public/login-bg/4.jpg | Bin 0 -> 55633 bytes
z51-pug-builder/public/login-bg/5.jpg | Bin 0 -> 12205969 bytes
z51-pug-builder/public/vite.svg | 1 +
z51-pug-builder/src/App.svelte | 6975 +++++++++++++++++
z51-pug-builder/src/app.css | 3494 +++++++++
z51-pug-builder/src/assets/svelte.svg | 1 +
z51-pug-builder/src/icons/back.svg | 2 +
z51-pug-builder/src/icons/clear.svg | 2 +
z51-pug-builder/src/icons/files.svg | 2 +
z51-pug-builder/src/icons/newmail.svg | 5 +
z51-pug-builder/src/icons/plus.svg | 24 +
z51-pug-builder/src/icons/preset.svg | 6 +
z51-pug-builder/src/icons/pug.svg | 6 +
z51-pug-builder/src/icons/save.svg | 6 +
z51-pug-builder/src/icons/settings.svg | 5 +
z51-pug-builder/src/icons/spacer.svg | 2 +
z51-pug-builder/src/icons/stack.svg | 5 +
z51-pug-builder/src/lib/Counter.svelte | 10 +
z51-pug-builder/src/lib/api.js | 212 +
z51-pug-builder/src/lib/parsing.js | 687 ++
z51-pug-builder/src/lib/spellcheck.js | 57 +
z51-pug-builder/src/lib/utils.js | 105 +
z51-pug-builder/src/main.js | 9 +
z51-pug-builder/svelte.config.js | 8 +
z51-pug-builder/vite.config.js | 1600 ++++
61 files changed, 18907 insertions(+)
create mode 100644 .env.prod.example
create mode 100644 .gitignore
create mode 100644 DOCKER-DEPLOY.md
create mode 100644 deploy/email-gen-api/Dockerfile
create mode 100644 deploy/email-gen-api/entrypoint.sh
create mode 100644 deploy/email-gen-api/server.js
create mode 100644 deploy/nginx/default.conf
create mode 100644 deploy/nginx/favicon.jpg
create mode 100644 deploy/nginx/landing.html
create mode 100755 deploy/scripts/update-email-gen.sh
create mode 100644 docker-compose.dev.yml
create mode 100644 docker-compose.prod.yml
create mode 100644 docker-compose.yml
create mode 100644 docs/01-user-guide.md
create mode 100644 docs/02-developer-guide.md
create mode 100644 docs/applyNowrap.md
create mode 160000 email-gen
create mode 100644 email-gen-overrides/README.md
create mode 100644 email-gen-overrides/reaspekt-master/blocks/_factory.pug
create mode 100644 email-gen-overrides/reaspekt-master/blocks/buttons.pug
create mode 100644 email-gen-overrides/reaspekt-master/blocks/other-ext.pug
create mode 100644 email-gen-overrides/reaspekt-master/blocks/texts-ext.pug
create mode 100644 email-gen-overrides/reaspekt-master/blocks/texts.pug
create mode 100644 z51-pug-builder/.dockerignore
create mode 100644 z51-pug-builder/.gitignore
create mode 100644 z51-pug-builder/.vscode/extensions.json
create mode 100644 z51-pug-builder/Dockerfile
create mode 100644 z51-pug-builder/README.md
create mode 100644 z51-pug-builder/index.html
create mode 100644 z51-pug-builder/jsconfig.json
create mode 100644 z51-pug-builder/package-lock.json
create mode 100644 z51-pug-builder/package.json
create mode 100644 z51-pug-builder/public/Block.pug
create mode 100644 z51-pug-builder/public/favicon.jpg
create mode 100644 z51-pug-builder/public/login-bg/2.jpg
create mode 100644 z51-pug-builder/public/login-bg/3.jpg
create mode 100644 z51-pug-builder/public/login-bg/4.jpg
create mode 100644 z51-pug-builder/public/login-bg/5.jpg
create mode 100644 z51-pug-builder/public/vite.svg
create mode 100644 z51-pug-builder/src/App.svelte
create mode 100644 z51-pug-builder/src/app.css
create mode 100644 z51-pug-builder/src/assets/svelte.svg
create mode 100644 z51-pug-builder/src/icons/back.svg
create mode 100644 z51-pug-builder/src/icons/clear.svg
create mode 100644 z51-pug-builder/src/icons/files.svg
create mode 100644 z51-pug-builder/src/icons/newmail.svg
create mode 100644 z51-pug-builder/src/icons/plus.svg
create mode 100644 z51-pug-builder/src/icons/preset.svg
create mode 100644 z51-pug-builder/src/icons/pug.svg
create mode 100644 z51-pug-builder/src/icons/save.svg
create mode 100644 z51-pug-builder/src/icons/settings.svg
create mode 100644 z51-pug-builder/src/icons/spacer.svg
create mode 100644 z51-pug-builder/src/icons/stack.svg
create mode 100644 z51-pug-builder/src/lib/Counter.svelte
create mode 100644 z51-pug-builder/src/lib/api.js
create mode 100644 z51-pug-builder/src/lib/parsing.js
create mode 100644 z51-pug-builder/src/lib/spellcheck.js
create mode 100644 z51-pug-builder/src/lib/utils.js
create mode 100644 z51-pug-builder/src/main.js
create mode 100644 z51-pug-builder/svelte.config.js
create mode 100644 z51-pug-builder/vite.config.js
diff --git a/.env.prod.example b/.env.prod.example
new file mode 100644
index 0000000..c801c99
--- /dev/null
+++ b/.env.prod.example
@@ -0,0 +1,2 @@
+# Host port for nginx in production compose
+HTTP_PORT=80
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..016532a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,21 @@
+node_modules/
+dist/
+.DS_Store
+*.tar.gz
+*.zip
+z51-pug-builder/data/
+z51-pug-builder/data-dev*/
+.claude/
+.vite/
+.env
+.env.*
+!.env.*.example
+aspekter_ref/
+__MACOSX/
+Untitled-1
+Block.pug
+reaspekt.html
+test.html
+blockAT.pug
+blockCB.pug
+blockRU.pug
diff --git a/DOCKER-DEPLOY.md b/DOCKER-DEPLOY.md
new file mode 100644
index 0000000..ed71653
--- /dev/null
+++ b/DOCKER-DEPLOY.md
@@ -0,0 +1,117 @@
+# Docker deploy: z51-pug-builder + email-gen
+
+## Что есть
+
+- `docker-compose.yml` — локальный/дев запуск (builder на `localhost:5173`)
+- `docker-compose.prod.yml` — прод запуск через nginx
+
+Сервисы:
+- `builder` — Svelte-конструктор + локальный API (`/api/*`)
+- `email-gen-api` — мост к `email-gen` (рендер PUG -> HTML)
+- `nginx` (только в prod compose) — входная точка на 80 порту
+
+---
+
+## 1) Локальный запуск (dev)
+
+```bash
+cd /Users/sergeyzotov/Documents/GENERATOR_Z51
+docker compose up -d --build
+docker compose ps
+```
+
+Открыть: `http://localhost:5173`
+
+Остановить:
+```bash
+docker compose down
+```
+
+---
+
+## 2) Прод запуск (nginx + docker)
+
+1. Подготовь env:
+```bash
+cd /Users/sergeyzotov/Documents/GENERATOR_Z51
+cp .env.prod.example .env
+```
+
+2. Запусти:
+```bash
+docker compose -f docker-compose.prod.yml up -d --build
+```
+
+3. Проверка:
+```bash
+docker compose -f docker-compose.prod.yml ps
+```
+
+Открыть: `http://` (или домен, если DNS уже настроен).
+
+Остановить:
+```bash
+docker compose -f docker-compose.prod.yml down
+```
+
+---
+
+## 3) Как работает генерация HTML
+
+1. В конструкторе собирается PUG.
+2. Нажимаешь `Превью -> Обновить`.
+3. Builder вызывает `POST /api/project/:name/render-email`.
+4. Запрос уходит в `email-gen-api`.
+5. `email-gen-api`:
+ - пишет PUG в `email-gen/emails//letters/let.pug`
+ - запускает `gulp pug --project `
+ - читает `email-gen/public/index.html`
+ - возвращает HTML обратно в конструктор.
+
+Важно: в `Настройки -> Текущий проект` заполняй поле `Папка проекта в email-gen` (например `numizmat`).
+
+---
+
+## 4) Обновление email-gen без ручной пересборки всего
+
+Скрипт:
+```bash
+./deploy/scripts/update-email-gen.sh
+```
+
+Или с веткой:
+```bash
+./deploy/scripts/update-email-gen.sh main
+```
+
+Что делает скрипт:
+- `git fetch`/`git pull --ff-only` в `email-gen`
+- пересобирает и перезапускает только `email-gen-api` контейнер
+
+Это удобно, если `email-gen` обновляется через git и перезаписывается.
+
+---
+
+## 5) Логи
+
+Prod:
+```bash
+docker compose -f docker-compose.prod.yml logs -f nginx
+docker compose -f docker-compose.prod.yml logs -f builder
+docker compose -f docker-compose.prod.yml logs -f email-gen-api
+```
+
+Dev:
+```bash
+docker compose logs -f builder
+docker compose logs -f email-gen-api
+```
+
+---
+
+## 6) Данные
+
+- Данные конструктора: `z51-pug-builder/data`
+- Репозиторий генератора: `email-gen` (bind mount в контейнер)
+
+Оба каталога остаются на хосте и не теряются при пересоздании контейнеров.
diff --git a/deploy/email-gen-api/Dockerfile b/deploy/email-gen-api/Dockerfile
new file mode 100644
index 0000000..f6fc594
--- /dev/null
+++ b/deploy/email-gen-api/Dockerfile
@@ -0,0 +1,14 @@
+FROM node:20-alpine
+
+WORKDIR /app
+COPY deploy/email-gen-api/server.js /app/server.js
+COPY deploy/email-gen-api/entrypoint.sh /app/entrypoint.sh
+RUN chmod +x /app/entrypoint.sh
+
+# Copy email-gen templates into image
+COPY email-gen /workspace/email-gen
+RUN if [ -f /workspace/email-gen/package.json ]; then cd /workspace/email-gen && npm install; fi
+
+EXPOSE 8787
+
+CMD ["/app/entrypoint.sh"]
diff --git a/deploy/email-gen-api/entrypoint.sh b/deploy/email-gen-api/entrypoint.sh
new file mode 100644
index 0000000..92ebc9c
--- /dev/null
+++ b/deploy/email-gen-api/entrypoint.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -e
+
+EMAIL_GEN_ROOT="${EMAIL_GEN_ROOT:-/workspace/email-gen}"
+
+# Install deps inside container if needed (host node_modules may be incompatible)
+if [ -f "$EMAIL_GEN_ROOT/package.json" ]; then
+ echo "Installing email-gen dependencies..."
+ cd "$EMAIL_GEN_ROOT"
+ npm install 2>&1
+ echo "Dependencies ready."
+fi
+
+exec node /app/server.js
diff --git a/deploy/email-gen-api/server.js b/deploy/email-gen-api/server.js
new file mode 100644
index 0000000..9e5e36c
--- /dev/null
+++ b/deploy/email-gen-api/server.js
@@ -0,0 +1,197 @@
+const http = require('http')
+const fs = require('fs')
+const path = require('path')
+const { spawnSync } = require('child_process')
+
+const PORT = Number(process.env.PORT || 8787)
+const EMAIL_GEN_ROOT = path.resolve(process.env.EMAIL_GEN_ROOT || '/workspace/email-gen')
+
+function send(res, status, payload) {
+ res.statusCode = status
+ res.setHeader('Content-Type', 'application/json')
+ res.end(JSON.stringify(payload))
+}
+
+const MAX_BODY_SIZE = 5 * 1024 * 1024
+
+function readBody(req) {
+ return new Promise((resolve, reject) => {
+ let size = 0
+ let data = ''
+ req.on('data', (chunk) => {
+ size += chunk.length
+ if (size <= MAX_BODY_SIZE) data += chunk
+ })
+ req.on('end', () => {
+ if (size > MAX_BODY_SIZE) return reject(new Error('payload_too_large'))
+ try {
+ resolve(data ? JSON.parse(data) : {})
+ } catch {
+ reject(new Error('invalid_json'))
+ }
+ })
+ })
+}
+
+function sanitizeProjectSlug(value) {
+ return String(value || '').trim().replace(/[^a-zA-Z0-9_-]/g, '')
+}
+
+function rewriteHtmlPug(projectDir, preheader = '', gender = 'female', genderPaths = {}) {
+ const htmlPugPath = path.resolve(projectDir, 'html.pug')
+ const safePreheader = preheader.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
+ function sanitizePartPath(p, fallback) {
+ if (!p || typeof p !== 'string') return fallback
+ const clean = p.replace(/\\/g, '/').replace(/\/+/g, '/')
+ if (/\.\./.test(clean) || /[\0\r\n]/.test(clean)) return fallback
+ if (!clean.startsWith('./parts/') && !clean.startsWith('parts/')) return fallback
+ return clean
+ }
+ const headerPath = gender === 'male'
+ ? sanitizePartPath(genderPaths.headerMale, './parts/header/header-man')
+ : sanitizePartPath(genderPaths.headerFemale, './parts/header/header-woman')
+ const footerPath = gender === 'male'
+ ? sanitizePartPath(genderPaths.footerMale, './parts/footer/footer-man')
+ : sanitizePartPath(genderPaths.footerFemale, './parts/footer/footer-woman')
+ const body = [
+ 'extends layout/layout.pug',
+ '',
+ 'block header',
+ ` include ${headerPath}`,
+ 'block preheader',
+ ` +preheader("${safePreheader}")`,
+ 'block content',
+ ' include ./letters/let.pug',
+ 'block footer',
+ ` include ${footerPath}`,
+ '',
+ ].join('\n')
+ fs.writeFileSync(htmlPugPath, body, 'utf-8')
+}
+
+function ensureNpmDeps() {
+ const marker = path.resolve(EMAIL_GEN_ROOT, 'node_modules')
+ if (fs.existsSync(marker)) return
+ const install = spawnSync('npm', ['install'], {
+ cwd: EMAIL_GEN_ROOT,
+ encoding: 'utf-8',
+ shell: false,
+ })
+ if (install.status !== 0) {
+ throw new Error((install.stderr || install.stdout || 'npm install failed').trim())
+ }
+}
+
+function renderWithNode(projectSlug, renderTemplate = 'html') {
+ const script = `
+const path = require('path');
+const fs = require('fs');
+const Email = require('email-templates');
+
+async function run() {
+ // For node -e, first extra arg starts at argv[1]
+ const project = process.argv[1];
+ const renderTemplate = process.argv[2];
+ const root = process.argv[3];
+ const email = new Email();
+ const html = await email.render({
+ path: project + '/' + renderTemplate,
+ juiceResources: {
+ preserveImportant: true,
+ applyStyleTags: true,
+ removeStyleTags: true,
+ preserveMediaQueries: true,
+ webResources: {
+ relativeTo: path.resolve(root, 'emails', project)
+ }
+ },
+ }, { pretty: true });
+
+ const outDir = path.resolve(root, 'public');
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
+ const outPath = path.resolve(outDir, 'index.html');
+ const normalized = String(html).replace('#MAILRU_PREHEADER_TAG#', ' ');
+ fs.writeFileSync(outPath, normalized, 'utf-8');
+}
+
+run().catch((error) => {
+ console.error(error && error.stack ? error.stack : String(error));
+ process.exit(1);
+});
+`
+
+ const run = spawnSync(process.execPath, ['-e', script, projectSlug, renderTemplate, EMAIL_GEN_ROOT], {
+ cwd: EMAIL_GEN_ROOT,
+ encoding: 'utf-8',
+ shell: false,
+ timeout: 120000,
+ })
+
+ if (run.error || run.status !== 0) {
+ throw new Error((run.stderr || run.stdout || run.error?.message || 'render failed').trim())
+ }
+}
+
+const server = http.createServer(async (req, res) => {
+ if (req.method === 'GET' && req.url === '/health') {
+ return send(res, 200, { ok: true })
+ }
+
+ if (req.method === 'POST' && req.url === '/render') {
+ let body
+ try { body = await readBody(req) } catch (e) { return send(res, 400, { error: e.message }) }
+ const projectSlug = sanitizeProjectSlug(body.projectSlug)
+ const pug = String(body.pug || '')
+ const preheader = String(body.preheader || '')
+ const gender = String(body.gender || 'female')
+ const genderPaths = body.genderPaths || {}
+
+ if (!projectSlug) return send(res, 400, { error: 'missing_project_slug', details: 'Project slug required' })
+ if (!pug.trim()) return send(res, 400, { error: 'missing_pug', details: 'PUG required' })
+
+ if (!fs.existsSync(EMAIL_GEN_ROOT)) {
+ return send(res, 500, { error: 'email_gen_not_found', details: 'email-gen root missing' })
+ }
+
+ const emailsRoot = path.resolve(EMAIL_GEN_ROOT, 'emails')
+ const projectDir = path.resolve(emailsRoot, projectSlug)
+ if (!projectDir.startsWith(emailsRoot)) {
+ return send(res, 400, { error: 'invalid_project_slug', details: 'Invalid project slug' })
+ }
+ if (!fs.existsSync(projectDir)) {
+ return send(res, 404, { error: 'email_project_not_found', details: `Project ${projectSlug} not found` })
+ }
+
+ try {
+ ensureNpmDeps()
+ const lettersDir = path.resolve(projectDir, 'letters')
+ if (!fs.existsSync(lettersDir)) fs.mkdirSync(lettersDir, { recursive: true })
+ fs.writeFileSync(path.resolve(lettersDir, 'let.pug'), pug, 'utf-8')
+ rewriteHtmlPug(projectDir, preheader, gender, genderPaths)
+ renderWithNode(projectSlug, 'html')
+
+ const htmlPath = path.resolve(EMAIL_GEN_ROOT, 'public', 'index.html')
+ if (!fs.existsSync(htmlPath)) {
+ return send(res, 500, { error: 'preview_not_found', details: 'public/index.html not found' })
+ }
+
+ const html = fs.readFileSync(htmlPath, 'utf-8')
+ return send(res, 200, {
+ html,
+ generatedAt: new Date().toISOString(),
+ })
+ } catch (error) {
+ return send(res, 500, {
+ error: 'render_failed',
+ details: error?.message || 'Unknown render error',
+ })
+ }
+ }
+
+ return send(res, 404, { error: 'not_found' })
+})
+
+server.listen(PORT, () => {
+ console.log(`email-gen-api listening on :${PORT}`)
+ console.log(`email-gen root: ${EMAIL_GEN_ROOT}`)
+})
diff --git a/deploy/nginx/default.conf b/deploy/nginx/default.conf
new file mode 100644
index 0000000..2de4456
--- /dev/null
+++ b/deploy/nginx/default.conf
@@ -0,0 +1,25 @@
+map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+}
+
+server {
+ listen 80;
+ server_name _;
+
+ client_max_body_size 20m;
+
+ location / {
+ proxy_pass http://builder:5173;
+ proxy_http_version 1.1;
+
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # Vite HMR/WebSocket
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ }
+}
diff --git a/deploy/nginx/favicon.jpg b/deploy/nginx/favicon.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ab74f63863a2b80440724e91bc3255532790bf55
GIT binary patch
literal 1157
zcmex=Jq?U}9uuW@2GxWo2Ojs;&jfGq4D<
z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d
zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP
zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2x9$;rq#OV6afSDRI-wmO
zy>xrmA9<^Obk@C{%d@J!wk~Teu7AYOoLwh;p>(ft(DhuCA8C6xN951C{>F$;VtxNS
zap|av%+x=xN)@H{d{tYxda;I^uIo-!sflwIU5U=L)fbw&<=Qjq8zrQtp`2I%q
zKMvbRbMp^Po2T)2fqh>8E&D_A%==V+)W`4atMz|WfABv8Uwp;-2hTh0)YVpf`+Rmm
zMf(w}Wm`XVKi0jrZC>cyxktX2Sxt!N&)U9PHtfWQNoOi13C?a4*E@7x(5o;kB>Sw>
z9*s|Ti`@)+YpnX-s$AbUq1f7ecYWiF`5)r$|4=bMe*cHoe}<#=H%=d}XMg-7`orym
z@jNB(d&KP}>ofReq@&l?`&{1RUi;ytjr@c4d==GhZXc(2J=-po-t+Z#t=zL(`9t=(
z`@(m9)N8NQo_%LWzRls3Erp-gzg=}@%?mAwtn$D-uJv11T6%vk*!%M6Nlm}g9-cW*
z)+e9YpSHhoTm6>ff5h4!p7>Gu;BL|BZ-T(sQTccF+WyV{2lWNwUvB^QkN4y9xAJ!V
z_J{8ao41w9dR_CdiLQQd+dDSd?PJ{1<@e8peWuN%f{G}8BI3|{_c-wtskD6on
ztt;JUb~KB9tms%a*Q>%WHB9EE^}&!msVtYCwN@Uj(N)
+
+
+
+
+ Aspekter — Визуальный конструктор email-рассылок
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Профессиональный email-конструктор
+
Письма, которыепродают. Без кода.
+
+ Единый инструмент для маркетолога: план → конструктор → проверка →
+ экспорт. Всё в одном интерфейсе, без переключений между сервисами.
+
+
+
+
+
+
+
+
+
+
BANNER
+
+
+
Копировать HTML
+
↓ Скачать
+
+
+
+
+
+
+
+
+
+
+
Конструктор
+
Собери письмоиз готовых блоков
+
Drag & drop, поиск блоков, collapse, история — всё для быстрой сборки без кода.
+
+
+
+
⚡
+
Блоки и их управление
+
Добавляй блоки через поиск, меняй порядок перетаскиванием или стрелками. Сворачивай ненужные блоки — рабочая область остаётся чистой. Переиспользуй любимые блоки через Quick Blocks на панели.
+
Drag & Drop Поиск блоков Collapse Quick Blocks
+
+
+
📦
+
Пресеты сборок
+
Сохрани текущую структуру письма как пресет — и применяй к следующим кампаниям одним кликом. Поиск, сортировка, удаление. Пресеты хранятся на сервере и доступны всей команде.
+
+
📧
Баннер + 4 товара + кнопка
12.03
+
📧
Sale: баннер + 6 товаров × 2
05.03
+
+
+
+
+
🕐
+
История изменений
+
Каждое сохранение создаёт снимок. Откати письмо к любой точке в истории — без потери текущей версии. Просматривай все снимки с датой и временем.
+
+
14:32
Добавлен блок «4 товара»
↩ Откат
+
14:18
Изменён текст баннера
↩ Откат
+
13:55
Создано письмо
↩ Откат
+
+
+
+
💾
+
Автосохранение и Ctrl+S
+
Изменения сохраняются автоматически через 600мс после остановки. Индикатор-точка в интерфейсе показывает статус: сохраняется / сохранено / ошибка. Принудительное сохранение — Ctrl+S.
+
Автосохранение Ctrl+S Ctrl+Z — Undo Ctrl+G — Рендер
+
+
+
+
+
+
+
+
+
Редактор текста
+
Текст,который выглядит правильно
+
Встроенные инструменты форматирования — типограф, жирный, переносы — прямо в интерфейсе конструктора.
+
+
+
+
+ Типограф — автоматически исправляет кавычки, тире, пробелы по правилам русской типографики (SOAP API)
+ Жирный — оборачивает выделенный текст в span с font-weight: 700
+ Авто-перенос — умно разбивает текст по ширине блока, предотвращает висячие слова
+ Перенос строки и буллет — быстрая вставка <br> и • кнопками
+ Вставка ссылки с настраиваемым шаблоном UTM-меток
+ Shift+Enter — ручной перенос строки прямо в поле редактирования
+
+
+
+
+
+
⚡ Quick Edit — редактирование в превью
+
+
Кликни на любой элемент в превью — появится плавающий попап для быстрого редактирования прямо на месте. Без поиска поля в списке блоков.
+
+
+
+
+
+
+
+
+
+
+
+
Предпросмотр
+
Три режима.Один клик.
+
Превью, HTML-код и Pug-исходник — в правой панели. Рендер кэшируется по хэшу контента — повторный просмотр мгновенный.
+
+
+
+
Режим 1
+
Живое превью
+
Отрендеренное письмо в iframe. Масштаб 40–100% — меняй ползунком, значение сохраняется в профиле. Рендер запускается автоматически или по Ctrl+G.
+
+
+
+
Режим 2
+
HTML-код
+
Готовый HTML для вставки в ESP. Копируй в буфер одной кнопкой — с нормализацией разметки. Или скачивай как файл.
+
+ Копировать HTML
+ Скачать .html
+ Нормализация
+
+
+
+
Режим 3
+
Pug-исходник
+
Собранный Pug-код письма — для версионирования и архива. Копируй или скачивай. Можно импортировать обратно в другой проект.
+
+ Копировать PUG
+ Скачать .pug
+ Импорт блоков
+
+
+
+
+
+
+
+
+
+
+
+
Сегментация
+
Одно письмо —две версии
+
Женская и мужская подборка в одном файле. Переключение — одной кнопкой, рендер — мгновенно.
+
+ Кнопки Ж / М — рендер с соответствующим хедером и футером
+ Кнопка ⇅ — переставляет блоки относительно блока-разделителя
+ Каждый блок получает сегмент: Общий / Женский / Мужской
+ Пути хедера/футера настраиваются в настройках проекта
+ Версии Ж и М кэшируются отдельно — скорость не страдает
+
+
+
♀ Женская версия
+
⇅
+
♂ Мужская версия
+
+
+
+
+
📦 Пул ID из плана
+
+
Вставь список ID товаров — система автоматически распределит их по блокам (3–4 ID на блок). Женские ID — первыми, мужские — следом. Счётчик показывает использованные/всего.
+
+
Пул ID 8 / 20 использовано
+
+
Блок 1 (Жен)
112233, 445566, 778899, 001122
+
Блок 2 (Муж)
334455, 667788, 990011, 223344
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Контроль качества
+
Ни один«нет в наличии» не пройдёт
+
Система парсит XML-фид VipAvenue и автоматически находит проблемные товары прямо в превью письма.
+
+
+
+
🔎
+
Автопроверка наличия
+
Кликни «Проверить» — система сверяет каждый артикул в письме с актуальным фидом. Товары без остатка подсвечиваются красным прямо в превью. Никакой ручной проверки перед отправкой.
+
+
Платье Jacquemus Rouge
НЕТ В НАЛИЧИИ
+
Блузка Self-Portrait
В НАЛИЧИИ
+
Жакет Max Mara
НЕТ В НАЛИЧИИ
+
+
+
+
✨
+
Умный подбор замен
+
Для каждого отсутствующего товара — список замен. Алгоритм учитывает тип изделия, бренд, гендер и ценовой диапазон. Товары дороже ×3 — исключаются автоматически. Hover на фото замены — показывает крупное изображение, название и цену.
+
Тип товара Бренд Гендер Цена ± Hover-превью
+
+
Платье Rotate → замена: A.W.A.K.E Mode
+12%
+
Платье Rotate → замена: LoveShackFancy
-8%
+
+
+
+
🔗
+
Проверка всех ссылок
+
Система извлекает все href и src из письма и проверяет их доступность. Результат — список с цветовыми индикаторами. Сломанные ссылки и 404 заметны сразу.
+
+
https://vipavenue.ru/sale/spring
200 OK
+
https://img.vipavenue.ru/banner.jpg
200 OK
+
https://vipavenue.ru/old-page
404
+
+
+
+
✏️
+
Проверка орфографии
+
Яндекс.Спеллер проверяет русский и английский текст прямо в превью письма. Ошибки подсвечиваются, счётчик показывает их количество. Проверяются только первые 9 500 символов.
+
«весеная» коллекция
→ весенняя
+
Ограниченое предложение
→ ограниченное
+
+
+
+
+
+
+
+
+
+
+
Планирование
+
Редакционныйкалендарь внутри
+
Все предстоящие рассылки — в одной вкладке. Переход из плана в конструктор нужного письма — одним кликом.
+
+ Загрузка рассылок из внешнего источника (Yonote)
+ Фильтры: просрочено / сегодня / завтра / неделя / позже
+ Тема, прехедер и ID писем подтягиваются в конструктор автоматически
+ Кнопка «Отправлено» — обновляет статус в источнике
+ Пуш-уведомления о новых рассылках в браузере
+ Гендерные ID (Ж / М) в одной строке плана
+
+
+
+
+
📅 План рассылок — Март 2026
+
+
+
14.03 Весенняя коллекция ГОТОВО
+
18.03 Sale до –40% В РАБОТЕ
+
22.03 Новые поступления ПЛАН
+
25.03 Тренды сезона ПЛАН
+
28.03 Бренд-фокус: Max Mara ПЛАН
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Медиабиблиотека
+
FTP-галереяпрямо в браузере
+
Загружай изображения для писем без FTP-клиентов. Пакетная загрузка, просмотр, копирование URL — не выходя из конструктора.
+
+ Пакетная загрузка — несколько файлов одним выбором
+ Папки организованы по дате письма — автоматически
+ Клик по миниатюре — копирует URL в буфер
+ Прогресс загрузки: N из M / процент
+ Удаление файлов с подтверждением
+ Поддержка FTP и SFTP
+
+
+
+
+
🖼 FTP Галерея — 03-14
+
+
+
🖼
+
🖼
+
🖼
+
🖼
+
🖼
+
🖼
+
🖼
+
🖼
+
+
+ Загрузить файлы (3/5 загружается...)
+
+
+
+
+
+
+
+
+
+
+
Специальные блоки
+
Сертификаты и расширенные блоки
+
Специализированный редактор сертификатов — отдельный тип контента с перетаскиванием, типографом и форматированием.
+
+
+
+
🏆
+
Редактор сертификатов
+
Отдельный режим для блоков-сертификатов. Добавляй строки, меняй их порядок перетаскиванием. Жирный, перенос, буллеты, Типограф — всё как в основном редакторе. Готовый сертификат сохраняется в письмо.
+
+
⠿
Дорогой клиент, дарим вам
+
+
⠿
действителен до 31 декабря 2026
+
+
+
+
⚙️
+
Опции товарных блоков
+
Для каждого блока с товарами — набор переключаемых опций (скидка, новинка, хит и т.д.). Включай и выключай опцию для конкретного блока или сразу для всех. Визуальный индикатор состояния: вкл / частично / выкл.
+
Скидка Новинка Хит Эксклюзив + Свои опции
+
+
+
+
+
+
+
+
+
Процесс
+
От плана доготового HTML
+
Три шага без переключений между инструментами.
+
+
+
01
+
Открой план
+
Перейди в редакционный календарь. Найди нужную рассылку — дата, тема и прехедер уже заполнены. Нажми «Собрать» — письмо откроется в конструкторе с нужными данными.
+
+
+
02
+
Собери письмо
+
Добавляй блоки, редактируй тексты, загружай баннеры через FTP-галерею. Вставь ID из пула — распределятся сами. Переключи гендерную версию — убедись, что обе выглядят идеально.
+
+
+
03
+
Проверь и отправь
+
Проверь наличие товаров, орфографию и ссылки. Всё чисто — копируй HTML и вставляй в платформу. Обнови статус «Отправлено» прямо из интерфейса.
+
+
+
+
+
+
+
+
+
Платформа
+
Настраиваетсяпод проект
+
Детальные настройки для каждого проекта, ролевой доступ и статистика трудозатрат.
+
+
+
+
Настройки
+
Гибкая конфигурация
+
Логотип, акцентный цвет, отступы. Для каждого блока — шаблон, видимость полей, подписи. Редактор Pug-частей (хедер/футер) прямо из интерфейса. Шаблон ссылок с UTM, авто-нумерация изображений.
+
+
+
Авторизация
+
Безопасность
+
scrypt-хэши паролей. Сессии 7 дней, хранятся на сервере (переживают перезапуск). Тема, zoom, активная страница — сохраняются в профиле. Настройки доступны сразу на любом устройстве.
+
+
+
Команда
+
Управление пользователями
+
Создавай пользователей, назначай роли (admin / user) и проекты. Каждый видит только свои проекты. Смена пароля и настроек профиля — из интерфейса.
+
+
+
+
Аналитика
+
Статистика времени
+
Трекер фиксирует время работы над каждым письмом. Статусы: черновик, в работе, отправлено. Агрегированная статистика по месяцам — чтобы планировать нагрузку команды.
+
+
+
+
+
+
+
+
+
+
Готовы делать письма быстрее?
+
Войдите в систему и начните прямо сейчас.
+
Войти в Aspekter →
+
+
+
+
+
+
+
+
diff --git a/deploy/scripts/update-email-gen.sh b/deploy/scripts/update-email-gen.sh
new file mode 100755
index 0000000..1b1eca2
--- /dev/null
+++ b/deploy/scripts/update-email-gen.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
+EMAIL_GEN_DIR="$ROOT_DIR/email-gen"
+
+if [[ ! -d "$EMAIL_GEN_DIR/.git" ]]; then
+ echo "[error] email-gen git repo not found: $EMAIL_GEN_DIR"
+ exit 1
+fi
+
+BRANCH="${1:-}"
+
+cd "$EMAIL_GEN_DIR"
+
+echo "[info] fetching email-gen..."
+git fetch --all --prune
+
+if [[ -n "$BRANCH" ]]; then
+ echo "[info] checking out branch: $BRANCH"
+ git checkout "$BRANCH"
+fi
+
+CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
+echo "[info] pulling branch: $CURRENT_BRANCH"
+git pull --ff-only origin "$CURRENT_BRANCH"
+
+echo "[info] rebuilding email-gen-api container..."
+cd "$ROOT_DIR"
+docker compose -f docker-compose.prod.yml up -d --build email-gen-api
+
+echo "[ok] email-gen updated and email-gen-api restarted"
diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml
new file mode 100644
index 0000000..8873e11
--- /dev/null
+++ b/docker-compose.dev.yml
@@ -0,0 +1,7 @@
+services:
+ builder:
+ volumes:
+ - ./z51-pug-builder/data-dev:/app/data
+ - ./z51-pug-builder/src:/app/src
+ - ./z51-pug-builder/vite.config.js:/app/vite.config.js
+ - ./z51-pug-builder/public/login-bg:/app/public/login-bg
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
new file mode 100644
index 0000000..564b623
--- /dev/null
+++ b/docker-compose.prod.yml
@@ -0,0 +1,24 @@
+services:
+ builder:
+ image: vaaspekter-builder
+ container_name: va-builder
+ environment:
+ - EMAIL_GEN_API_URL=http://email-gen-api:8787
+ - NODE_OPTIONS=--max-old-space-size=3072
+ ports:
+ - "127.0.0.1:6001:5173"
+ volumes:
+ - ./data:/app/data
+ - ./email-gen:/email-gen
+ restart: unless-stopped
+ depends_on:
+ - email-gen-api
+
+ email-gen-api:
+ image: vaaspekter-email-gen-api
+ container_name: va-email-gen-api
+ environment:
+ - EMAIL_GEN_ROOT=/workspace/email-gen
+ volumes:
+ - ./email-gen:/workspace/email-gen
+ restart: unless-stopped
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..395c465
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,29 @@
+services:
+ builder:
+ build:
+ context: .
+ dockerfile: z51-pug-builder/Dockerfile
+ environment:
+ - EMAIL_GEN_API_URL=http://email-gen-api:8787
+ ports:
+ - "127.0.0.1:6001:5173"
+ volumes:
+ - ./z51-pug-builder/data:/app/data
+ - ./z51-pug-builder/src:/app/src
+ - ./z51-pug-builder/vite.config.js:/app/vite.config.js
+ - ./email-gen:/email-gen
+ depends_on:
+ - email-gen-api
+
+ email-gen-api:
+ build:
+ context: .
+ dockerfile: deploy/email-gen-api/Dockerfile
+ environment:
+ - EMAIL_GEN_ROOT=/workspace/email-gen
+ volumes:
+ - ./email-gen:/workspace/email-gen
+ - email-gen-node-modules:/workspace/email-gen/node_modules
+
+volumes:
+ email-gen-node-modules:
diff --git a/docs/01-user-guide.md b/docs/01-user-guide.md
new file mode 100644
index 0000000..e24ef2d
--- /dev/null
+++ b/docs/01-user-guide.md
@@ -0,0 +1,238 @@
+# Aspekter VA — Руководство пользователя
+
+## Что это такое
+
+Aspekter VA — визуальный конструктор email-рассылок для интернет-магазина VipAvenue. Позволяет собирать письма из готовых блоков, редактировать тексты, подставлять товары из фида, проверять орфографию и выгружать готовый HTML для отправки через Mindbox.
+
+Адрес: **https://aspekter.ru**
+
+---
+
+## Вход в систему
+
+1. Открыть https://aspekter.ru
+2. Ввести логин и пароль
+3. Нажать «Войти»
+
+При первом запуске создаётся учётная запись `admin` с временным паролем (выводится в консоль контейнера). После входа рекомендуется сменить пароль через панель администратора.
+
+Сессия живёт 7 дней. После этого потребуется повторный вход.
+
+---
+
+## Основной интерфейс
+
+### Страницы (навигация слева)
+
+| Страница | Назначение |
+|---|---|
+| **Конструктор** | Сборка письма из блоков |
+| **План** | Календарь рассылок из Yonote |
+| **Статистика** | Время работы над письмами |
+| **Настройки** | Параметры проекта |
+
+### Конструктор — левая панель
+
+- **Список блоков** — перетаскивание, добавление, удаление, изменение порядка
+- **Поля блока** — при выборе блока показываются редактируемые поля (тексты, ссылки, картинки, ID товаров)
+- **Пресеты** — сохранённые наборы блоков для быстрого старта
+
+### Конструктор — правая панель (превью)
+
+- **Превью письма** — iframe с отрендеренным HTML
+- **Масштаб** — ползунок 40–100%
+- **Кнопки Жен / Муж** — переключение гендерной версии превью
+- **Кнопка ⇅** — переставляет блоки для мужской/женской версии (flip)
+- **Копировать HTML** — копирует итоговый HTML в буфер обмена
+
+---
+
+## Работа с блоками
+
+### Добавление блока
+1. Нажать «+ Добавить блок» в левой панели
+2. Выбрать блок из списка (появится dropdown с иконками)
+3. Блок добавляется в конец списка
+
+### Редактирование полей
+Каждый блок раскрывается по клику. Внутри — поля:
+- **Текст** — многострочное поле, поддерживает пробелы и переносы
+- **Ссылка** — URL (кнопки, картинки, ссылки меню)
+- **ID товаров** — через запятую, подтягиваются из фида
+- **Картинка** — URL изображения
+
+### Перетаскивание
+Блоки можно перетаскивать за иконку ⠿ слева от названия.
+
+### Удаление
+Кнопка 🗑 справа от блока.
+
+### Секции внутри блока
+Некоторые блоки (например «Текст») состоят из секций. Секции можно:
+- Менять местами перетаскиванием
+- Удалять (кроме первой)
+- Добавлять новые
+
+---
+
+## Гендерная сегментация
+
+Письма VipAvenue отправляются в двух версиях: женской и мужской.
+
+### Как это работает
+- Каждое письмо собирается с набором блоков
+- **Кнопка «Жен» / «Муж»** — переключает превью на соответствующую версию (меняет header/footer и порядок блоков)
+- **Кнопка ⇅ (flip)** — физически переставляет блоки: блоки до разделителя и после меняются местами
+- **Разделитель (⊕)** — блок-разделитель, служит осью для flip. Устанавливается кнопкой ⊕ только на блоке типа «dividerVA»
+
+### Хедеры и футеры
+Настраиваются в разделе «Настройки» → «Гендерные пути»:
+- Хедер женский / мужской
+- Футер женский / мужской
+
+Выбираются из списка файлов в `email-gen/emails/vipavenue/parts/`.
+
+---
+
+## Товары из фида
+
+### Настройка фида
+В настройках проекта указывается URL XML-фида Mindbox. Фид кэшируется на 3 часа.
+
+### Подстановка товаров
+1. В блоке с товарами ввести ID через запятую
+2. Система найдёт товары в фиде и покажет превью (название, цена, картинка)
+3. Если товар не найден или недоступен — появится предупреждение
+
+### Замена товара
+Если товар недоступен, система предложит замену. Алгоритм подбора:
+- Тот же тип товара (+20 баллов)
+- Та же категория (+15)
+- Тот же бренд (+25)
+- Тот же гендер (+10)
+- Близкая цена (+0..10, исключение если >3x разница)
+- Тот же цвет (+5)
+
+### Пул ID
+В левой панели можно вести «Пул ID» — список товаров для быстрой вставки.
+
+---
+
+## Пресеты
+
+Пресет — сохранённый набор блоков с их содержимым.
+
+- **Сохранить** — кнопка «Сохранить как пресет», вводится название
+- **Загрузить** — выбрать из списка, блоки заменятся
+- **Удалить** — кнопка удаления в списке пресетов
+
+---
+
+## Письма (карточки рассылок)
+
+### Создание
+Каждая рассылка — карточка с полями:
+- Тема письма
+- Дата
+- Тег (категория)
+- Набор блоков
+
+### Список писем
+В левой панели — список карточек. Можно:
+- Создать новое письмо
+- Открыть существующее
+- Удалить
+- Просмотреть историю изменений
+
+### История
+Каждое сохранение создаёт снимок. Можно откатиться к предыдущей версии.
+
+---
+
+## Заметки
+
+В боковой панели есть раздел «Заметки» — текстовые заметки привязанные к проекту.
+- Создание, редактирование, удаление
+- Данные хранятся на сервере
+
+---
+
+## План рассылок
+
+Страница «План» подтягивает данные из Yonote (API). Показывает:
+- Календарь с отмеченными датами рассылок
+- Список с темами, статусами, датами
+- Уведомления о новых/удалённых рассылках (polling каждые 3 минуты)
+
+---
+
+## Проверка ссылок
+
+Кнопка «Проверить ссылки» в превью:
+- Находит все URL в сгенерированном HTML
+- Проверяет каждый (HEAD-запрос)
+- Показывает статус: ✅ работает, ❌ битая, ⚠️ редирект
+
+---
+
+## Проверка орфографии
+
+Использует Яндекс.Спеллер. Подсвечивает ошибки в превью красным волнистым подчёркиванием с подсказкой при наведении.
+
+---
+
+## Типографика
+
+Кнопка «Типограф» отправляет тексты блоков через сервис Артемия Лебедева (typograf.artlebedev.ru). Применяет:
+- Неразрывные пробелы после предлогов
+- Правильные кавычки «ёлочки»
+- Тире вместо дефиса
+- И другие правила русской типографики
+
+---
+
+## FTP / Галерея изображений
+
+### Настройка
+В настройках проекта → FTP:
+- Хост, порт, логин, пароль
+- Протокол (FTP / SFTP)
+- Удалённый путь
+- Публичный URL
+
+### Использование
+- Кнопка «Галерея» открывает модальное окно
+- Загрузка: выбрать файлы (множественный выбор), загрузка автоматическая
+- Копирование URL: клик по миниатюре
+- Удаление: кнопка на миниатюре
+
+---
+
+## Копирование HTML
+
+1. Нажать «Копировать HTML»
+2. HTML копируется в буфер обмена
+3. Вставить в Mindbox
+
+**Важно:** в сгенерированном HTML предлоги и короткие слова (≤3 символов) склеены со следующим словом через `` — это предотвращает висячие предлоги в почтовых клиентах.
+
+---
+
+## Тема оформления
+
+Переключатель светлая/тёмная тема — в верхней панели. Настройка сохраняется в профиле пользователя на сервере.
+
+---
+
+## Администрирование
+
+Доступно только пользователям с ролью `admin`.
+
+### Управление пользователями
+- Создание новых пользователей (логин, пароль ≥8 символов, имя, роль)
+- Редактирование (смена пароля, имени, роли)
+- Удаление (нельзя удалить самого себя)
+
+### Роли
+- **admin** — полный доступ + управление пользователями
+- **user** — работа с конструктором
diff --git a/docs/02-developer-guide.md b/docs/02-developer-guide.md
new file mode 100644
index 0000000..02cdfee
--- /dev/null
+++ b/docs/02-developer-guide.md
@@ -0,0 +1,574 @@
+# Aspekter VA — Руководство разработчика
+
+## 1. Обзор архитектуры
+
+Aspekter VA — монолитное web-приложение для сборки email-рассылок VipAvenue.
+
+```
+┌─────────────────────────────────────────────────┐
+│ Браузер │
+│ App.svelte (UI) ←→ api.js (HTTP client) │
+└────────────────────┬────────────────────────────┘
+ │ HTTP
+┌────────────────────▼────────────────────────────┐
+│ builder (порт 6001) │
+│ Vite dev server + API middleware │
+│ vite.config.js — весь backend (~1500 строк) │
+└────────────────────┬────────────────────────────┘
+ │ HTTP (внутренний)
+┌────────────────────▼────────────────────────────┐
+│ email-gen-api (порт 8787) │
+│ server.js — Pug → HTML рендер │
+│ email-gen/ — шаблоны, стили, миксины │
+└─────────────────────────────────────────────────┘
+```
+
+### Стек
+- **Frontend:** Svelte 5 + Vite 7
+- **Backend:** Vite middleware plugin (Node.js, встроен в dev-server)
+- **Email рендер:** Pug шаблоны + email-templates (npm)
+- **Хранение данных:** файловая система (JSON)
+- **Инфраструктура:** Docker Compose, nginx reverse proxy
+
+### Почему так устроено
+Backend встроен как Vite middleware plugin, а не как отдельный сервер. Это значит:
+- Один процесс, один порт
+- HMR для фронтенда работает из коробки
+- Нет CORS-проблем
+- Но: vite.config.js не перезагружается через HMR — нужен `docker restart builder`
+
+---
+
+## 2. Структура файлов
+
+```
+VA.ASPEKTER/
+├── docker-compose.yml # Оркестрация контейнеров
+├── deploy/
+│ ├── nginx/ # Nginx конфиг + лендинг
+│ │ ├── nginx.conf
+│ │ └── landing.html
+│ └── email-gen-api/
+│ ├── Dockerfile
+│ └── server.js # Standalone Pug render service
+├── email-gen/ # Git-репозиторий коллег (шаблоны)
+│ └── emails/vipavenue/
+│ ├── layout/layout.pug # Базовый layout (width=600)
+│ ├── blocks/block.pug # Все блоки конструктора
+│ ├── includes/mixins.pug # Pug миксины (товары, кнопки, preheader)
+│ ├── css/style.css # Стили (инлайнятся при рендере)
+│ └── parts/ # Хедеры, футеры (по гендеру)
+│ ├── header/
+│ └── footer/
+└── z51-pug-builder/ # Основное приложение
+ ├── Dockerfile
+ ├── package.json
+ ├── vite.config.js # ВЕСЬ BACKEND API (~1500 строк)
+ ├── index.html # Входная точка Vite
+ ├── public/ # Статика (favicon)
+ ├── src/
+ │ ├── App.svelte # ВЕСЬ FRONTEND UI (~6400 строк)
+ │ ├── app.css # Все стили
+ │ ├── main.js # Точка входа Svelte
+ │ └── lib/
+ │ ├── api.js # API-клиент (все эндпоинты)
+ │ ├── parsing.js # Парсинг Pug-блоков, миксинов, аргументов
+ │ ├── spellcheck.js # Проверка орфографии (Yandex Speller)
+ │ └── utils.js # Утилиты (normalizeNewlines, escapeRegExp, etc)
+ └── data/ # Данные (том Docker)
+ ├── config.json # Глобальный конфиг (Yonote токен, URL)
+ ├── feed-cache.json # Кэш товарного фида
+ ├── render-cache.json # Кэш рендера (Pug→HTML)
+ ├── _system/
+ │ ├── users.json # Пользователи (логин, хэш пароля, роль)
+ │ └── sessions.json # Активные сессии
+ └── vipavenue/ # Данные проекта
+ ├── settings.json # Настройки (блоки, FTP, гендер, фид)
+ ├── block.pug # Текущий пуг-файл конструктора
+ ├── presets.json # Пресеты
+ ├── notes.json # Заметки (индекс)
+ ├── notes/ # Файлы заметок
+ ├── stats.json # Статистика времени
+ ├── drafts/ # Черновики по пользователям
+ └── letters/ # Карточки писем по пользователям
+ └── {userId}/
+ ├── letters.json # Индекс писем
+ ├── {id}.json # Данные письма
+ └── {id}.history.json # История снимков
+```
+
+---
+
+## 3. Docker — контейнеры и тома
+
+### Контейнеры
+
+| Контейнер | Образ | Порт | Назначение |
+|---|---|---|---|
+| `builder` | z51-pug-builder/Dockerfile | 6001→5173 | Vite dev server + API |
+| `email-gen-api` | deploy/email-gen-api/Dockerfile | (внутренний) 8787 | Pug→HTML рендер |
+| nginx | (отдельно) | 80/443 | Reverse proxy |
+
+### Тома builder
+
+| Хост | Контейнер | Режим | HMR? |
+|---|---|---|---|
+| `./z51-pug-builder/src` | `/app/src` | rw | ✅ Мгновенно |
+| `./z51-pug-builder/vite.config.js` | `/app/vite.config.js` | rw | ❌ Нужен restart |
+| `./z51-pug-builder/data` | `/app/data` | rw | — |
+| `./email-gen` | `/email-gen` | rw | ❌ Нужен build |
+
+### Тома email-gen-api
+
+| Хост | Контейнер |
+|---|---|
+| `./email-gen` | `/workspace/email-gen` (rw) |
+| Named volume | `/workspace/email-gen/node_modules` |
+
+### Правила пересборки (КРИТИЧЕСКИ ВАЖНО)
+
+**`docker compose build && docker compose up -d` нужен при:**
+- Изменения в `email-gen/` (pug шаблоны, миксины, стили)
+- Изменения в `deploy/email-gen-api/server.js`
+- Изменения в `z51-pug-builder/Dockerfile`
+- Изменения в `z51-pug-builder/package.json` (новые зависимости)
+
+**`docker compose restart builder` нужен при:**
+- Изменения в `z51-pug-builder/vite.config.js`
+
+**Ничего не нужно (HMR подхватит):**
+- Изменения в `z51-pug-builder/src/` (App.svelte, app.css, lib/*.js)
+
+> **НЕ использовать** `docker cp` или `docker exec` для правки исходников на проде. Только редактировать на хосте, тома примонтированы.
+
+---
+
+## 4. Backend API — все эндпоинты
+
+Весь API реализован в `vite.config.js` как Vite middleware plugin.
+
+### Авторизация (без аутентификации)
+
+| Метод | URL | Описание |
+|---|---|---|
+| POST | `/api/auth/login` | Вход: `{login, password}` → cookie `va_token` |
+| POST | `/api/auth/logout` | Выход: удаляет cookie |
+| GET | `/api/auth/me` | Текущий пользователь (или 401) |
+| PUT | `/api/auth/preferences` | Сохранить тему/activePage/zoom |
+
+### Администрирование (role: admin)
+
+| Метод | URL | Описание |
+|---|---|---|
+| GET | `/api/admin/users` | Список пользователей |
+| POST | `/api/admin/users` | Создать: `{login, password, name, role}` |
+| PUT | `/api/admin/users/:id` | Обновить пользователя |
+| DELETE | `/api/admin/users/:id` | Удалить (нельзя удалить себя) |
+
+### Проект (все требуют аутентификации)
+
+| Метод | URL | Описание |
+|---|---|---|
+| GET | `/api/projects` | Список проектов (всегда `["vipavenue"]`) |
+| GET | `/api/project/:name` | Данные проекта (блок, настройки, черновик, пресеты, письма, заметки) |
+| PUT | `/api/project/:name/block` | Сохранить block.pug |
+| PUT | `/api/project/:name/block-custom` | Сохранить кастомный блок |
+| PUT | `/api/project/:name/settings` | Сохранить настройки |
+| PUT | `/api/project/:name/draft` | Сохранить черновик |
+| PUT | `/api/project/:name/presets` | Сохранить пресеты |
+
+### Письма
+
+| Метод | URL | Описание |
+|---|---|---|
+| GET | `/api/project/:name/letter/:id` | Получить письмо |
+| PUT | `/api/project/:name/letter` | Сохранить: `{id, ...data}` |
+| DELETE | `/api/project/:name/letter/:id` | Удалить |
+| GET | `/api/project/:name/letter/:id/history` | История снимков |
+| PUT | `/api/project/:name/letter/:id/history` | Добавить снимок |
+
+### Заметки
+
+| Метод | URL | Описание |
+|---|---|---|
+| GET | `/api/project/:name/notes` | Индекс заметок |
+| PUT | `/api/project/:name/notes` | Сохранить индекс |
+| GET | `/api/project/:name/note/:id` | Получить заметку |
+| PUT | `/api/project/:name/note` | Сохранить: `{id, ...data}` |
+| DELETE | `/api/project/:name/note/:id` | Удалить |
+
+### Рендер email
+
+| Метод | URL | Описание |
+|---|---|---|
+| POST | `/api/project/:name/render-email` | Рендер Pug→HTML. Body: `{projectSlug, pug, preheader, gender}` |
+
+**Цепочка рендера:**
+1. Frontend отправляет Pug-код + preheader + gender
+2. Backend формирует hash, проверяет кэш
+3. Если кэш-промах → отправляет в email-gen-api
+4. email-gen-api: записывает `letters/let.pug`, генерирует `html.pug`, запускает `email-templates` → `public/index.html`
+5. Backend читает HTML, обрабатывает Mindbox-теги (подставляет товары), применяет nowrap
+6. Возвращает `{html, previewHtml, unavailableProducts}`
+
+### Товарный фид
+
+| Метод | URL | Описание |
+|---|---|---|
+| POST | `/api/project/:name/feed-refresh` | Очистить кэш фида, перезагрузить |
+| POST | `/api/project/:name/feed-lookup` | Найти товары по ID: `{ids: [...]}` |
+| POST | `/api/project/:name/feed-suggest` | Подобрать замену: `{productId, excludeIds, search}` |
+
+Фид кэшируется на 3 часа. XML парсится regex-ом (не DOM), поддерживается windows-1251.
+
+### FTP/SFTP
+
+| Метод | URL | Описание |
+|---|---|---|
+| POST | `/api/project/:name/ftp/test` | Проверить подключение |
+| POST | `/api/project/:name/ftp/upload` | Загрузить файл: `{imageData, fileName, folder}` |
+| POST | `/api/project/:name/ftp/list` | Список файлов: `{folder}` |
+| POST | `/api/project/:name/ftp/delete` | Удалить файл: `{folder, fileName}` |
+
+### Прочее
+
+| Метод | URL | Описание |
+|---|---|---|
+| GET | `/api/config` | Глобальный конфиг (Yonote, upload URL) |
+| PUT | `/api/config` | Сохранить конфиг |
+| GET | `/api/yonote/status` | Статус подключения Yonote |
+| GET | `/api/yonote/databases` | Список баз Yonote |
+| GET | `/api/yonote/database/:id/properties` | Свойства базы |
+| GET | `/api/yonote/database/:id/rows` | Строки базы |
+| POST | `/api/yonote/row/update` | Обновить строку: `{rowId, values}` |
+| POST | `/api/upload-image` | Загрузить изображение: `{imageData, fileName}` |
+| POST | `/api/check-links` | Проверить ссылки: `{urls: [...]}` |
+| GET | `/api/parts-files` | Список pug-файлов из parts/ |
+| GET | `/api/parts-file-read?path=...` | Прочитать parts-файл |
+| POST | `/api/parts-file-write` | Записать parts-файл |
+| GET | `/api/project/:name/stats` | Статистика проекта |
+| POST | `/api/project/:name/stats` | Сохранить запись статистики |
+| GET | `/api/stats` | Вся статистика (все проекты) |
+
+---
+
+## 5. Frontend — архитектура
+
+### Общая структура
+
+Весь UI — один файл `App.svelte` (~6400 строк). Это монолит. Причина — проект начинался как прототип и вырос.
+
+### Ключевые модули (lib/)
+
+**api.js** — HTTP-клиент. Все функции `api*()` вызывают `apiRequest()` который делает `fetch()` с `Content-Type: application/json` и обрабатывает ошибки.
+
+**parsing.js** — парсинг Pug-кода:
+- `parseBlocks(text)` — разбивает Pug на блоки по `//` комментариям
+- `buildBaseSchema(content, blockName, mixinRules)` — строит схему полей блока (какие поля редактируемые)
+- `parseSections(content)` — разбивает блок на секции для drag-n-drop
+- `findMixinArgRange(line, argIndex)` — находит позицию аргумента миксина в строке
+- `extractMixinArgValue()` / `replaceMixinArgQuoted()` — чтение/запись аргументов
+
+**spellcheck.js** — `checkSpelling(text)` отправляет в Yandex Speller, `injectSpellMarks(html, errors)` подсвечивает ошибки в превью.
+
+**utils.js** — `normalizeNewlines()`, `escapeRegExp()`, `unquoteValue()`, `downloadFile()`.
+
+### State management
+
+Svelte 5 reactive `let` переменные. Ключевые группы:
+
+| Группа | Переменные | Описание |
+|---|---|---|
+| UI | `activePage`, `theme`, `sidebarTab`, `settingsBlockIndex` | Состояние интерфейса |
+| Данные | `assembledBlocks`, `presets`, `planRows`, `allStats` | Основные данные |
+| Рендер | `renderedHtml`, `previewHtml`, `autoRenderTimer` | Результат рендера |
+| Гендер | `currentGenderVersion`, `segmentFlipped` | Гендерная сегментация |
+| Фид | `feedProducts`, `unavailableProducts` | Товары |
+| Письма | `currentLetterId`, `letterName` | Текущее письмо |
+| Настройки | `settings` | settings.json проекта |
+
+### Ключевые функции
+
+| Функция | Назначение |
+|---|---|
+| `loadProject()` | Загрузка проекта при старте |
+| `rebuildOutput()` | Пересборка Pug из блоков → рендер |
+| `renderEmail()` | Отправка Pug на сервер для рендера |
+| `assembleGenderVersion(target)` | Сборка гендерной версии |
+| `flipSegmentOrder()` | Переключение порядка блоков (⇅) |
+| `addBlock(name)` / `removeBlock(id)` | Управление блоками |
+| `updateBlockContent(id, content)` | Обновление содержимого блока |
+| `saveDraft()` | Автосохранение черновика |
+| `copyHtml()` | Копирование HTML в буфер |
+| `handlePreviewElementClick(data)` | Клик по элементу в превью → фокус на поле |
+
+### Реактивные вычисления ($:)
+
+```javascript
+$: settingsBlockName = allBlocks[settingsBlockIndex]?.name
+$: settingsTemplate = getEffectiveTemplate(settingsBlockName)
+$: baseSchema = buildBaseSchema(settingsTemplate, settingsBlockName, ...)
+$: allBlocks = [...blockTemplates, ...customBlocksList]
+$: productOptionsList = (settings.productOptions || []).map(...)
+```
+
+---
+
+## 6. Email rendering pipeline
+
+### Полная цепочка
+
+```
+1. App.svelte: assembleGenderVersion() + rebuildOutput()
+ ↓ Собирает Pug из блоков
+2. App.svelte: apiRenderEmail(name, {projectSlug, pug, preheader, gender})
+ ↓ HTTP POST
+3. vite.config.js: /api/project/:name/render-email
+ ↓ pugHash() → проверка кэша
+ ↓ Если промах:
+4. vite.config.js → HTTP POST email-gen-api:8787/render
+ ↓ {projectSlug, pug, preheader, gender, genderPaths}
+5. email-gen-api/server.js:
+ a) Записывает letters/let.pug (содержимое из конструктора)
+ b) rewriteHtmlPug() — генерирует html.pug с правильным header/footer
+ c) renderWithNode() — вызывает email-templates (Pug → HTML)
+ d) Читает public/index.html → возвращает HTML
+6. vite.config.js:
+ a) Кэширует HTML
+ b) processMindboxTags() — подставляет товары из фида
+ c) applyNowrap() — склеивает предлоги с nowrap-спанами
+ d) Возвращает {html, previewHtml, unavailableProducts}
+7. App.svelte: показывает превью в iframe
+```
+
+### Кэш рендера
+- Ключ: `md5(slug + pug + gender + genderPaths)`
+- Максимум 30 записей (LRU)
+- Персистится в `data/render-cache.json`
+- Инвалидируется при смене Pug-кода, гендера или путей header/footer
+
+### Mindbox-обработка
+- Замена `@{for...}@{end for}` блоков
+- Извлечение ID товаров из `GetByValue('12345')`
+- Подстановка данных из фида (цена, название, картинка, URL)
+- Отслеживание недоступных товаров
+
+### Nowrap-обработка
+- Только для текстовых блоков (span с классом `h3`)
+- Оборачивает `предлог + слово` в ``
+- Использует placeholder-подход чтобы избежать двойной обработки
+
+---
+
+## 7. Гендерная сегментация — техническая реализация
+
+### Данные
+```json
+// settings.json → genderPaths
+{
+ "headerFemale": "./parts/header/header-woman",
+ "headerMale": "./parts/header/header-man",
+ "footerFemale": "./parts/footer/footer-woman",
+ "footerMale": "./parts/footer/footer-man"
+}
+```
+
+### Цепочка передачи gender
+
+1. **App.svelte** → `apiRenderEmail(..., { gender: 'female'|'male' })`
+2. **vite.config.js** → читает `genderPaths` из settings.json, форвардит в email-gen-api
+3. **email-gen-api/server.js** → `rewriteHtmlPug(projectDir, preheader, gender, genderPaths)` — записывает `html.pug` с нужным include header/footer
+4. Pug рендерит HTML с правильным header/footer
+
+### Кэш
+`pugHash(slug, pug, gender, genderPaths)` — gender и genderPaths включены в ключ, female/male кэшируются отдельно.
+
+### Flip (⇅)
+- `flipSegmentOrder()` в App.svelte
+- Находит блок-разделитель с `swapCenter = true`
+- Блоки до разделителя и после меняются местами
+- `segmentFlipped` (bool) отслеживает состояние toggle
+
+---
+
+## 8. Авторизация и безопасность
+
+### Аутентификация
+- **Хэширование:** scrypt (salt 16 bytes, key length 64)
+- **Сессии:** случайный токен 32 bytes hex, хранится в Map + файл sessions.json
+- **Cookie:** `va_token`, HttpOnly, SameSite=Strict, Secure, Max-Age=7d
+- **Brute-force:** 5 попыток на IP за 15 минут
+
+### Middleware стек (порядок важен!)
+1. Security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy)
+2. Auth endpoints (/api/auth/*) — без проверки сессии
+3. CSRF проверка (origin/referer vs host)
+4. Auth middleware — проверка cookie → req.user
+5. Admin middleware (/api/admin/*) — проверка role=admin
+6. Uploads middleware (/uploads/*) — статика с проверкой path traversal
+7. API middleware — основные эндпоинты
+
+### Защита от атак
+- **Path traversal:** `sanitizeFileId()` для всех файловых ID, `getProjectDir()` проверяет startsWith
+- **SSRF:** фильтрация приватных IP (127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16)
+- **CSRF:** проверка origin/referer на state-changing запросах
+- **XSS:** не применимо (внутренний инструмент, HTML генерируется из шаблонов)
+
+---
+
+## 9. Настройки проекта (settings.json)
+
+```json
+{
+ "globalSpacing": 40,
+ "blocks": {
+ "Текст": { "spacing": 20, "template": "..." },
+ "Кнопка": { "spacing": 10 }
+ },
+ "genderPaths": {
+ "headerFemale": "./parts/header/header-woman",
+ "headerMale": "./parts/header/header-man",
+ "footerFemale": "./parts/footer/footer-woman",
+ "footerMale": "./parts/footer/footer-man"
+ },
+ "feedUrl": "https://...",
+ "ftpConfig": {
+ "host": "...",
+ "port": 21,
+ "user": "...",
+ "password": "***",
+ "protocol": "ftp",
+ "remotePath": "/images",
+ "publicUrl": "https://..."
+ },
+ "mixinRules": [
+ { "mixin": "buttonRounded", "argIndex": 0, "type": "mixin-text", "label": "Текст кнопки" }
+ ],
+ "productOptions": [
+ { "code": "showPrice:true", "label": "Показывать цену", "defaultEnabled": true }
+ ],
+ "customBlocks": [
+ { "id": "...", "name": "Мой блок", "content": "..." }
+ ]
+}
+```
+
+---
+
+## 10. email-gen — шаблоны (репозиторий коллег)
+
+### Структура
+```
+email-gen/emails/vipavenue/
+├── layout/layout.pug # Основной layout, table width=600
+├── blocks/block.pug # Все блоки (//Текст, //Кнопка, //Товары, etc)
+├── includes/mixins.pug # Pug-миксины (+products, +buttonRounded, +preheader, etc)
+├── css/style.css # CSS (инлайнится при рендере)
+└── parts/
+ ├── header/ # Хедеры (header-woman.pug, header-man.pug, header-dark.pug)
+ └── footer/ # Футеры (аналогично)
+```
+
+### Как добавить новый блок
+1. Добавить секцию в `blocks/block.pug`:
+ ```pug
+ //Мой новый блок
+ tr
+ td.paddingWrapper
+ +defaultTable("100%")
+ tr
+ td(align="center")
+ span.font.h3.blackText Текст блока
+ ```
+2. Пересобрать контейнеры: `docker compose build && docker compose up -d`
+3. Блок появится в конструкторе
+
+### Preheader
+- Миксин `+preheader(text)` в mixins.pug
+- Формат: `{текст} ⠀×130`
+- `#MAILRU_PREHEADER_TAG#` в pug заменяется на `` в server.js
+
+---
+
+## 11. Деплой
+
+### Сервер
+- IP: определяется из конфига
+- Проект: `/opt/va/` (или аналогичный путь)
+- Nginx: reverse proxy → localhost:6001
+
+### Обновление кода
+
+**Frontend (App.svelte, app.css, lib/*.js):**
+```bash
+# Файлы примонтированы через том — просто редактируй на хосте
+# HMR подхватит автоматически
+```
+
+**Backend (vite.config.js):**
+```bash
+# Файл примонтирован, но Vite не перечитывает конфиг
+docker compose restart builder
+```
+
+**Шаблоны email (email-gen/):**
+```bash
+docker compose build && docker compose up -d
+```
+
+**email-gen-api/server.js:**
+```bash
+docker compose build email-gen-api && docker compose up -d email-gen-api
+```
+
+### Первый запуск
+```bash
+git clone
+cd VA.ASPEKTER
+docker compose build
+docker compose up -d
+# Смотреть логи для временного пароля admin:
+docker compose logs builder | grep "Временный пароль"
+```
+
+---
+
+## 12. Известные особенности и ограничения
+
+### Архитектурные
+- **Монолит:** App.svelte ~6400 строк, vite.config.js ~1500 строк. Декомпозиция не проводилась.
+- **Файловое хранилище:** нет БД, всё в JSON. Race condition при параллельных записях теоретически возможен (writeFileSync без блокировки).
+- **Single-project:** код спроектирован под vipavenue. Мультипроектность убрана, но следы остались (PROJECT_NAME constant).
+
+### Рендер
+- **Кэш:** 30 записей LRU. При изменении CSS в email-gen кэш не инвалидируется (нужно очистить вручную или пересобрать).
+- **Mindbox-теги:** парсятся regex-ом, не DOM. Если формат тегов изменится — сломается.
+- **XML-фид:** парсится regex-ом. CDATA, вложенные теги с одинаковым именем — потенциально проблемны.
+
+### Фронтенд
+- **Svelte 5:** используется Svelte 5, но не все runes-паттерны применены. `ensureSchema()` мутирует block.schema напрямую.
+- **MutationObserver:** на document.body с subtree=true. Debounce 200ms добавлен, но это всё равно потенциальная нагрузка.
+
+### TODO (отмечено в коде)
+- Google Sheets → Yonote API миграция (поиск по `TODO: YONOTE`)
+- Пользовательские настройки (тема, zoom) частично перенесены на сервер, localStorage ещё используется как fallback
+- Pull email-gen из git — функция запланирована, но не реализована
+
+---
+
+## 13. Conventions
+
+### Именование
+- localStorage ключи: `va-*` (va-active-page, va-theme, va-plan-cache, va-previewZoom)
+- Cookie: `va_token`
+- CSS классы: `va-spell` (spellcheck), `vaclick` (preview detection)
+- Docker: `vaaspekter-builder-1`, `vaaspekter-email-gen-api-1`
+
+### Форматирование кода
+- 2 пробела отступ
+- Одинарные кавычки в JS
+- Без точек с запятой (semicolons) — inconsistent, в некоторых местах есть
+
+### Git
+- `email-gen/` — это отдельный git-репозиторий коллег, вложен как поддиректория (НЕ submodule)
+- Основной репозиторий: `VA.ASPEKTER/`
diff --git a/docs/applyNowrap.md b/docs/applyNowrap.md
new file mode 100644
index 0000000..a2a5a77
--- /dev/null
+++ b/docs/applyNowrap.md
@@ -0,0 +1,168 @@
+# applyNowrap — защита от висячих предлогов в email
+
+## Назначение
+
+Функция `applyNowrap(html)` предотвращает «висячие» предлоги и короткие слова (≤3 букв) в email-рассылках. Оборачивает короткое слово + следующее слово в ``, чтобы они не разрывались на разные строки.
+
+**Почему нужна:** Mail.ru и некоторые другие почтовые клиенты игнорируют ` ` при переносе строк. Единственный надёжный способ — `white-space:nowrap` на ``.
+
+## Полный код
+
+```javascript
+function applyNowrap(html) {
+ function wrapShort(text) {
+ return text.replace(
+ /(?]*class="[^"]*\bh3\b[^"]*"[^>]*>)([\s\S]*?)(<\/span><\/td>)/gi,
+ (_match, open, content, close) => {
+ // Шаг 2: Обработать текст МЕЖДУ тегами: >текст<
+ const processed = content.replace(
+ />([^<]+) '>' + wrapShort(t) + '<'
+ )
+ // Шаг 3: Обработать текст В НАЧАЛЕ (до первого тега)
+ const firstText = processed.replace(
+ /^([^<]+)/,
+ (m) => wrapShort(m)
+ )
+ return open + firstText + close
+ }
+ )
+ // Шаг 4: Заменить placeholder'ы на реальные span-теги
+ return result
+ .replace(/\u200Bspan\u200Bnwr\u200B/g, '')
+ .replace(/\u200B\/span\u200Bnwr\u200B/g, ' ')
+}
+```
+
+## Алгоритм пошагово
+
+### Шаг 1 — Выбор целевых блоков
+
+```
+/(]*class="[^"]*\bh3\b[^"]*"[^>]*>)([\s\S]*?)(<\/span><\/td>)/gi
+```
+
+Ищет `` с классом `h3` (текстовые блоки писем), закрытый ` `.
+
+Три группы захвата:
+- `open` — открывающий тег ``
+- `content` — всё содержимое между открывающим и закрывающим тегами
+- `close` — закрывающий ` `
+
+> **Адаптация для другого проекта:** заменить `h3` на нужный CSS-класс текстовых блоков. Заменить ` ` на ваш закрывающий паттерн.
+
+### Шаг 2 — Обработка текста между вложенными тегами
+
+```
+/>([^<]+)` и `<` (текст между HTML-тегами внутри блока). Например в `Что это дает ?` поймает `Что это дает`.
+
+### Шаг 3 — Обработка текста в начале блока
+
+```
+/^([^<]+)/
+```
+
+Ловит текст от начала content до первого HTML-тега. Нужен отдельно, потому что regex из шага 2 ищет `>текст<`, а в начале блока нет `>` перед текстом.
+
+### Шаг 4 — Placeholder → реальные теги
+
+```javascript
+.replace(/\u200Bspan\u200Bnwr\u200B/g, '')
+.replace(/\u200B\/span\u200Bnwr\u200B/g, ' ')
+```
+
+Заменяет placeholder-маркеры на настоящие HTML-теги.
+
+**Зачем placeholder'ы?** Если бы мы сразу вставляли ``, то regex из шага 2 (`>([^<]+)<`) мог бы снова поймать текст внутри уже вставленного span'а и обработать его повторно. Маркеры `\u200B` (zero-width space) невидимы и не матчатся как `<` или `>`, поэтому двойной обработки не происходит.
+
+## Ключевой regex — wrapShort
+
+```
+/(?` |
+| `$1` | Короткое слово (предлог) |
+| `\u00A0` | Non-breaking space между словами (двойная защита) |
+| `$2` | Следующее слово |
+| `\u200B/span\u200Bnwr\u200B` | Placeholder для ` ` |
+
+## Результат на примере
+
+**Вход:**
+```html
+С первыми весенними днями дизайнеры и стилисты
+```
+
+**Выход:**
+```html
+
+ С первыми весенними днями
+ дизайнеры и стилисты
+
+```
+
+## Исправленные баги
+
+### Баг 1: ` ` не матчился (2026-03-20)
+
+**Симптом:** Nowrap-спаны не появляются в HTML, хотя функция вызывается.
+
+**Причина:** Типограф (Артемий Лебедев / любой другой) вставляет ` ` между предлогами и словами. Оригинальный regex искал только `\s+` и не находил ` `.
+
+**Фикс:** Заменить `\s+` на `(?:\s| )+`.
+
+### Баг 2: `\b` не работает с кириллицей (2026-03-21)
+
+**Симптом:** Regex вообще ничего не матчит для кириллических предлогов (С, и, в, на, от, из, но, до, по, за, ко).
+
+**Причина:** В JavaScript `\b` (word boundary) работает **ТОЛЬКО с ASCII-символами** (`[a-zA-Z0-9_]`). Кириллица не считается «word character». Поэтому `\b` перед кириллическими буквами НИКОГДА не срабатывает.
+
+**Фикс:** Заменить `\b` на `(? ⚠️ **Правило:** Никогда не использовать `\b` для кириллицы в JavaScript. Всегда использовать lookbehind/lookahead.
+
+## Адаптация для другого проекта
+
+1. **Целевой CSS-класс:** Заменить `h3` в regex шага 1 на класс текстовых блоков вашего шаблона
+2. **Закрывающий паттерн:** ` ` — подстроить под структуру вашего HTML (может быть `
`, `` и т.д.)
+3. **Типограф:** Если используется типограф, regex уже обрабатывает ` `. Если нет — `(?:\s| )+` всё равно будет работать корректно (просто ` `-ветка не сработает)
+4. **Вызов:** Применять к финальному HTML **после** типографа, но **до** отдачи клиенту
+5. **Тестирование:** Обязательно проверять в Mail.ru — это самый строгий клиент по обработке пробелов
+
+## Где вызывается
+
+В бэкенде (vite.config.js), в endpoint рендера email — после получения HTML из email-gen-api и обработки Mindbox-тегов:
+
+```javascript
+const nowrapHtml = applyNowrap(rawHtml)
+const nowrapPreview = applyNowrap(mindbox.html)
+```
+
+Результаты отдаются клиенту:
+- `nowrapHtml` — финальный HTML для экспорта/отправки
+- `nowrapPreview` — HTML с подставленными товарами для превью
diff --git a/email-gen b/email-gen
new file mode 160000
index 0000000..ada0a37
--- /dev/null
+++ b/email-gen
@@ -0,0 +1 @@
+Subproject commit ada0a3716dc454846ab244296f39718d66c6ff38
diff --git a/email-gen-overrides/README.md b/email-gen-overrides/README.md
new file mode 100644
index 0000000..ef4a836
--- /dev/null
+++ b/email-gen-overrides/README.md
@@ -0,0 +1,16 @@
+# email-gen overrides
+
+This folder stores local overrides mounted into `email-gen-api` container.
+
+Scope now:
+- `reaspekt-master` only
+- mounted files:
+ - `blocks/_factory.pug`
+ - `blocks/buttons.pug`
+ - `blocks/texts.pug`
+ - `blocks/texts-ext.pug`
+ - `blocks/other-ext.pug`
+
+Why:
+- keep customizations outside `email-gen` repo
+- `git pull` in `email-gen` does not remove these local overrides
diff --git a/email-gen-overrides/reaspekt-master/blocks/_factory.pug b/email-gen-overrides/reaspekt-master/blocks/_factory.pug
new file mode 100644
index 0000000..0706812
--- /dev/null
+++ b/email-gen-overrides/reaspekt-master/blocks/_factory.pug
@@ -0,0 +1,323 @@
+// Reaspekt master shared factory mixins
+
+mixin ctaButtonSection(opts = {})
+ - const width = opts.width || 560
+ - const text = opts.text || '#ТЕКСТ#'
+ - const href = opts.href || '#ССЫЛКА#'
+ - const buttonBg = opts.buttonBg || '#ffffff'
+ - const buttonText = opts.buttonText || '#130F33'
+ - const iconSrc = opts.iconSrc || 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-white.png'
+
+ if width === 560
+ tr
+ td.padding-wrapper
+ +buttonRounded(text, href, 560, 60, buttonBg, 16, buttonText, 0, '', iconSrc, 20, 17, 1, 'right').textVerdana
+ else if width === 270
+ tr
+ td.padding-wrapper
+ +defaultTable('560')
+ tr
+ td(width='270')
+ +buttonRounded(text, href, 270, 60, buttonBg, 16, buttonText, 0, '', iconSrc, 20, 17, 1, 'right').textVerdana
+ +tdFixed(20)
+ td(width='270')
+ +buttonRounded(text, href, 270, 60, buttonBg, 16, buttonText, 0, '', iconSrc, 20, 17, 1, 'right').textVerdana
+ else if width === 173
+ tr
+ td.padding-wrapper
+ +defaultTable('560')
+ tr
+ td(width='173')
+ +buttonRounded(text, href, 173, 60, buttonBg, 16, buttonText, 0, '', iconSrc, 20, 17, 1, 'right').textVerdana
+ +tdFixed(21)
+ td(width='173')
+ +buttonRounded(text, href, 173, 60, buttonBg, 16, buttonText, 0, '', iconSrc, 20, 17, 1, 'right').textVerdana
+ +tdFixed(20)
+ td(width='173')
+ +buttonRounded(text, href, 173, 60, buttonBg, 16, buttonText, 0, '', iconSrc, 20, 17, 1, 'right').textVerdana
+
+mixin ctaLinkSection(opts = {})
+ - const width = opts.width || 560
+ - const href = opts.href || ''
+ - const linkText = opts.linkText || (width === 173 ? 'Любой текст' : 'Как не терять наши письма?')
+ - const colorClass = opts.colorClass || 'color__blue'
+ - const linkClass = opts.linkClass || 'text__link-blue'
+ - const linkColorClass = opts.linkColorClass || textClass
+ - const tableWidth = opts.tableWidth || '560'
+
+ if width === 560
+ tr
+ td(align='center').padding-wrapper
+ a(href=href target='_blank').textVerdana(class=`${colorClass} text__link ${linkClass}`)= linkText
+ else if width === 270
+ tr
+ td.padding-wrapper
+ +defaultTable(tableWidth)
+ tr
+ td(width='270' align='center')
+ a(href=href target='_blank').textVerdana(class=`${colorClass} text__link ${linkClass}`)= linkText
+ +tdFixed(20)
+ td(width='270' align='center')
+ a(href=href target='_blank').textVerdana(class=`${colorClass} text__link ${linkClass}`)= linkText
+ else if width === 173
+ tr
+ td.padding-wrapper
+ +defaultTable(tableWidth)
+ tr
+ td(width='173' align='center')
+ a(href=href target='_blank').textVerdana(class=`${colorClass} text__link ${linkClass}`)= linkText
+ +tdFixed(21)
+ td(width='173' align='center')
+ a(href=href target='_blank').textVerdana(class=`${colorClass} text__link ${linkClass}`)= linkText
+ +tdFixed(20)
+ td(width='173' align='center')
+ a(href=href target='_blank').textVerdana(class=`${colorClass} text__link ${linkClass}`)= linkText
+
+mixin contentCardInner(opts = {})
+ - const width = opts.width || 560
+ - const titleClass = opts.titleClass || 'color__blue'
+ - const textClass = opts.textClass || 'color__blue'
+ - const titleSizeClass = opts.titleSizeClass || 'header__h1'
+ - const title = opts.title || 'Контекстная реклама для увеличения продаж'
+ - const text = opts.text || 'Наша команда стала активнее участвовать в образовательных мероприятиях, на которых мы делимся тонкостями своей работы и рассказываем о практическом опыте. Решили поделиться с вами записями прошедших вебинаров. Вот сводка тем:'
+ - const buttonBg = opts.buttonBg || '#ffffff'
+ - const buttonText = opts.buttonText || '#130F33'
+ - const withImage = !!opts.withImage
+ - const imageSrc = opts.imageSrc || 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/banner-50-percent.jpg'
+
+ +defaultTable(`${width}`)
+ if withImage
+ tr
+ td
+ +backgroundImageBlock(imageSrc, width, 180, '#ffffff', 'left', 'top')
+ +spacerLine(20)
+ tr
+ td
+ span.textVerdana(class=`${titleSizeClass} ${titleClass}`)= title
+ +spacerLine(20)
+ if opts.firstColumnExtraGap
+ +spacerLine(20)
+ tr
+ td
+ span.textVerdana.text__normal(class=textClass)= text
+ +spacerLine(20)
+ tr
+ td
+ +buttonRounded('#ТЕКСТ#', '#ССЫЛКА#', width, 60, buttonBg, 16, buttonText, 0, '').textVerdana
+
+mixin textSection560(opts = {})
+ tr
+ td.padding-wrapper(class=opts.bgClass || 'background__white')
+ +defaultTable('560')
+ +spacerLine(40)
+ tr
+ td
+ span.textVerdana.header__h1(class=opts.titleClass || 'color__blue') Мы продолжаем делать вебинары для вас, а чтобы следить за актуальными темами, подписывайтесь на наше сообщество ВКонтакте
+ +spacerLine(20)
+ tr
+ td
+ span.textVerdana.text__normal(class=opts.textClass || 'color__blue') Наша команда стала активнее участвовать в образовательных мероприятиях, на которых мы делимся тонкостями своей работы и рассказываем о практическом опыте. Решили поделиться с вами записями прошедших вебинаров. Вот сводка тем:
+ +spacerLine(20)
+ tr
+ td
+ +buttonRounded('#ТЕКСТ#', '#ССЫЛКА#', 560, 60, opts.buttonBg || '#ffffff', 16, opts.buttonText || '#130F33', 0, '').textVerdana
+ +spacerLine(40)
+
+mixin textSection270(opts = {})
+ tr
+ td.padding-wrapper(class=opts.bgClass || 'background__white')
+ +defaultTable('560')
+ +spacerLine(40, 3)
+ tr
+ td(valign='top')
+ +contentCardInner({
+ width: 270,
+ withImage: true,
+ titleClass: opts.titleClass,
+ textClass: opts.textClass,
+ buttonBg: opts.buttonBg,
+ buttonText: opts.buttonText
+ })
+ +tdFixed(20)
+ td(valign='top')
+ +contentCardInner({
+ width: 270,
+ withImage: true,
+ titleClass: opts.titleClass,
+ textClass: opts.textClass,
+ buttonBg: opts.buttonBg,
+ buttonText: opts.buttonText
+ })
+ +spacerLine(40, 3)
+
+mixin textSection173(opts = {})
+ tr
+ td.padding-wrapper(class=opts.bgClass || 'background__white')
+ +defaultTable('560')
+ +spacerLine(40, 5)
+
+mixin textImageSection560(opts = {})
+ - const title = opts.title || 'Мы продолжаем делать вебинары для вас, а чтобы следить за актуальными темами, подписывайтесь на наше сообщество ВКонтакте'
+ - const text = opts.text || 'Наша команда стала активнее участвовать в образовательных мероприятиях, на которых мы делимся тонкостями своей работы и рассказываем о практическом опыте. Решили поделиться с вами записями прошедших вебинаров. Вот сводка тем:'
+ - const imageSrc = opts.imageSrc || 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/image.jpg'
+ - const bgClass = opts.bgClass || 'background__white'
+ - const titleClass = opts.titleClass || 'color__blue'
+ - const textClass = opts.textClass || 'color__blue'
+ - const buttonBg = opts.buttonBg || '#ffffff'
+ - const buttonText = opts.buttonText || '#130F33'
+ - const showTitle = opts.showTitle !== false
+ - const showText = opts.showText !== false
+ - const showButton = !!opts.showButton
+ - const textBeforeImage = !!opts.textBeforeImage
+ - const center = !!opts.center
+ - const linkMode = !!opts.linkMode
+ - const linkHref = opts.linkHref || ''
+ - const linkText = opts.linkText || 'Как не терять наши письма?'
+ - const linkClass = opts.linkClass || 'text__link-blue'
+
+ tr
+ td.padding-wrapper(class=bgClass)
+ +defaultTable('560')
+ +spacerLine(40)
+ if showTitle && !textBeforeImage
+ tr
+ td(class=center ? 'text__center' : '')
+ span.textVerdana.header__h1(class=titleClass)= title
+ +spacerLine(20)
+ if showText && textBeforeImage
+ tr
+ td(class=center ? 'text__center' : '')
+ span.textVerdana.text__normal(class=textClass)= text
+ +spacerLine(20)
+ tr
+ td
+ +backgroundImageBlock(imageSrc, 560, 266, '#ffffff', 'left', 'top')
+ if showText && !textBeforeImage
+ +spacerLine(20)
+ tr
+ td(class=center ? 'text__center' : '')
+ span.textVerdana.text__normal(class=textClass)= text
+ if showButton
+ +spacerLine(20)
+ tr
+ td
+ if linkMode
+ a(href=linkHref target='_blank' style='width: 100%;').textVerdana(class=`${linkColorClass} text__link ${linkClass}`)= linkText
+ else
+ +buttonRounded('#ТЕКСТ#', '#ССЫЛКА#', 560, 60, buttonBg, 16, buttonText, 0, '').textVerdana
+ +spacerLine(40)
+
+mixin contentCardImage270(opts = {})
+ - const title = opts.title || 'Контекстная реклама для увеличения продаж'
+ - const text = opts.text || 'Наша команда стала активнее участвовать в образовательных мероприятиях, на которых мы делимся тонкостями своей работы и рассказываем о практическом опыте. Решили поделиться с вами записями прошедших вебинаров. Вот сводка тем:'
+ - const imageSrc = opts.imageSrc || 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/image.jpg'
+ - const titleClass = opts.titleClass || 'color__blue'
+ - const textClass = opts.textClass || 'color__blue'
+ - const buttonBg = opts.buttonBg || '#ffffff'
+ - const buttonText = opts.buttonText || '#130F33'
+ - const showButton = !!opts.showButton
+ - const linkMode = !!opts.linkMode
+ - const linkHref = opts.linkHref || ''
+ - const linkText = opts.linkText || 'Как не терять наши письма?'
+ - const linkClass = opts.linkClass || 'text__link-blue'
+ - const linkColorClass = opts.linkColorClass || textClass
+
+ +defaultTable('270')
+ tr
+ td
+ +backgroundImageBlock(imageSrc, 270, 270, '#ffffff', 'left', 'top')
+ +spacerLine(20)
+ tr
+ td
+ span.textVerdana.header__h2(class=titleClass)= title
+ +spacerLine(20)
+ tr
+ td
+ span.textVerdana.text__normal(class=textClass)= text
+ if showButton
+ +spacerLine(20)
+ tr
+ td
+ if linkMode
+ a(href=linkHref target='_blank' style='width: 100%;').textVerdana(class=`${linkColorClass} text__link ${linkClass}`)= linkText
+ else
+ +buttonRounded('#ТЕКСТ#', '#ССЫЛКА#', 270, 60, buttonBg, 16, buttonText, 0, '').textVerdana
+
+mixin textImageSection270(opts = {})
+ - const bgClass = opts.bgClass || 'background__white'
+
+ tr
+ td.padding-wrapper(class=bgClass)
+ +defaultTable('560')
+ +spacerLine(40, 3)
+ tr
+ td(valign='top')
+ +contentCardImage270(opts)
+ +tdFixed(20)
+ td(valign='top')
+ +contentCardImage270(opts)
+ +spacerLine(40, 3)
+ tr
+ td(valign='top')
+ +contentCardInner({
+ width: 173,
+ withImage: false,
+ titleSizeClass: 'header__h2',
+ titleClass: opts.titleClass,
+ textClass: opts.textClass,
+ buttonBg: opts.buttonBg,
+ buttonText: opts.buttonText,
+ firstColumnExtraGap: !!opts.firstColumnExtraGap
+ })
+ +tdFixed(20)
+ td(valign='top')
+ +contentCardInner({
+ width: 173,
+ withImage: false,
+ titleSizeClass: 'header__h2',
+ titleClass: opts.titleClass,
+ textClass: opts.textClass,
+ buttonBg: opts.buttonBg,
+ buttonText: opts.buttonText
+ })
+ +tdFixed(21)
+ td(valign='top')
+ +contentCardInner({
+ width: 173,
+ withImage: false,
+ titleSizeClass: 'header__h2',
+ titleClass: opts.titleClass,
+ textClass: opts.textClass,
+ buttonBg: opts.buttonBg,
+ buttonText: opts.buttonText
+ })
+ +spacerLine(40, 5)
+
+mixin sideImageTextSection(opts = {})
+ - const bgClass = opts.bgClass || 'background__blue'
+ - const textClass = opts.textClass || 'color__white'
+ - const imageSrc = opts.imageSrc || 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/icons-box-blue.png'
+ - const imageBg = opts.imageBg || '#130F33'
+ - const text1 = opts.text1 || 'Искусственный интеллект может ускорить работу SEO-специалистов и оптимизировать затраты. Заменяет ли chatGPT копирайтера? Всем ли поможет такой подход? Об этом и не только узнайте по ссылке.'
+ - const text2 = opts.text2 || 'Наша команда стала активнее участвовать в образовательных мероприятиях, на которых мы делимся тонкостями своей работы....'
+ - const showSecondText = opts.showSecondText !== false
+
+ tr
+ td(class=bgClass)
+ +defaultTable('100%')
+ tr
+ td.paddingWrapper
+ +defaultTable('100%')
+ +spacerLine(40)
+ tr
+ td
+ span.textVerdana.text__normal(class=textClass)!= text1
+ if showSecondText
+ tr
+ td
+ span.textVerdana.text__normal(class=textClass)!= text2
+ +spacerLine(40)
+ td(valign='bottom')
+ +defaultTable('')
+ +trtd
+ +backgroundImageBlock(imageSrc, 145, 270, imageBg, 'center', 'top', 'contain')
diff --git a/email-gen-overrides/reaspekt-master/blocks/buttons.pug b/email-gen-overrides/reaspekt-master/blocks/buttons.pug
new file mode 100644
index 0000000..e839a9a
--- /dev/null
+++ b/email-gen-overrides/reaspekt-master/blocks/buttons.pug
@@ -0,0 +1,77 @@
+include ./_factory
+
++spacerLine(20)
+//Кнопка Синяя 100% ширины
++ctaButtonSection({
+ width: 560,
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ iconSrc: 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-white.png'
+})
++spacerLine(20)
+//Кнопка Синяя 50% ширины
++ctaButtonSection({
+ width: 270,
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ iconSrc: 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-white.png'
+})
++spacerLine(20)
+//Кнопка Синяя 33% ширины
++ctaButtonSection({
+ width: 173,
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ iconSrc: 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-white.png'
+})
++spacerLine(20)
+
++spacerLine(20)
+//Кнопка Зеленая 100% ширины
++ctaButtonSection({
+ width: 560,
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8',
+ iconSrc: 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-blue.png'
+})
++spacerLine(20)
+//Кнопка Зеленая 50% ширины
++ctaButtonSection({
+ width: 270,
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8',
+ iconSrc: 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-blue.png'
+})
++spacerLine(20)
+//Кнопка Зеленая 33% ширины
++ctaButtonSection({
+ width: 173,
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8',
+ iconSrc: 'https://574922.selcdn.ru/email.static/reaspekt/2024_newsletters/2024_09_29/icon-watch-blue.png'
+})
+
++spacerLine(20)
+//Сcылка синяя 100% ширины
++ctaLinkSection({ width: 560, colorClass: 'color__blue', linkClass: 'text__link-blue' })
++spacerLine(20)
+//Сcылка синяя 50% ширины
++ctaLinkSection({ width: 270, colorClass: 'color__blue', linkClass: 'text__link-blue' })
++spacerLine(20)
+//Сcылка синяя 33% ширины
++ctaLinkSection({ width: 173, colorClass: 'color__blue', linkClass: 'text__link-blue' })
++spacerLine(20)
+
+tr
+ td.background__blue
+ +defaultTable('560')
+ +spacerLine(20)
+ //Сcылка белая 100% ширины
+ +ctaLinkSection({ width: 560, colorClass: 'color__white', linkClass: 'text__link-white' })
+ +spacerLine(20)
+ //Сcылка белая 50% ширины
+ +ctaLinkSection({ width: 270, colorClass: 'color__white', linkClass: 'text__link-white', tableWidth: '100%' })
+ +spacerLine(20)
+ //Сcылка белая 33% ширины
+ +ctaLinkSection({ width: 173, colorClass: 'color__white', linkClass: 'text__link-white', tableWidth: '100%' })
+ +spacerLine(20)
diff --git a/email-gen-overrides/reaspekt-master/blocks/other-ext.pug b/email-gen-overrides/reaspekt-master/blocks/other-ext.pug
new file mode 100644
index 0000000..2ab5fc7
--- /dev/null
+++ b/email-gen-overrides/reaspekt-master/blocks/other-ext.pug
@@ -0,0 +1,29 @@
+include ./_factory
+
+// Extended
+
+// 1
+
+// Текст с изображением справа Синий Фон
++sideImageTextSection({
+ bgClass: 'background__blue',
+ textClass: 'color__white',
+ imageSrc: 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/icons-box-blue.png',
+ imageBg: '#130F33'
+})
+
+// Текст с изображением справа Белый Фон
++sideImageTextSection({
+ bgClass: 'background__white',
+ textClass: 'color__blue',
+ imageSrc: 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/icons-box-white.png',
+ imageBg: '#ffffff'
+})
+
+// Текст с изображением справа Зеленый Фон
++sideImageTextSection({
+ bgClass: 'background__green',
+ textClass: 'color__blue',
+ imageSrc: 'https://574922.selcdn.ru/email.static/reaspekt/master-tamplate/banners/icons-box-green.png',
+ imageBg: '#AAC8C8'
+})
diff --git a/email-gen-overrides/reaspekt-master/blocks/texts-ext.pug b/email-gen-overrides/reaspekt-master/blocks/texts-ext.pug
new file mode 100644
index 0000000..3ccb0cb
--- /dev/null
+++ b/email-gen-overrides/reaspekt-master/blocks/texts-ext.pug
@@ -0,0 +1,222 @@
+include ./_factory
+
+//Перенести в texts.pug
+
+//Текст 100% Ширины + Картинка Синий фон
++textImageSection560({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8',
+ showTitle: true,
+ showText: true,
+ showButton: true
+})
+
+//Текст 100% Ширины + Картинка Белый фон
++textImageSection560({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ showTitle: true,
+ showText: true,
+ showButton: true
+})
+
+//Текст 100% Ширины + Картинка Зеленый фон
++textImageSection560({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ showTitle: true,
+ showText: true,
+ showButton: true
+})
+
+//Extended
+
+//1
+
+//Текст 100% Ширины + Картинка Синий фон
++textImageSection560({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ showTitle: false,
+ showText: true,
+ textBeforeImage: true,
+ showButton: false
+})
+
+//Текст 100% Ширины + Картинка Белый фон
++textImageSection560({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ showTitle: false,
+ showText: true,
+ textBeforeImage: true,
+ showButton: false
+})
+
+//Текст 100% Ширины + Картинка Зеленый фон
++textImageSection560({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ showTitle: false,
+ showText: true,
+ textBeforeImage: true,
+ showButton: false
+})
+
+//2
+
+//Текст 100% Ширины + Картинка Синий фон
++textImageSection560({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8',
+ showTitle: false,
+ showText: true,
+ textBeforeImage: true,
+ showButton: true
+})
+
+//Текст 100% Ширины + Картинка Белый фон
++textImageSection560({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ showTitle: false,
+ showText: true,
+ textBeforeImage: true,
+ showButton: true
+})
+
+//Текст 100% Ширины + Картинка Зеленый фон
++textImageSection560({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ showTitle: false,
+ showText: true,
+ textBeforeImage: true,
+ showButton: true
+})
+
+//3
+
+//Текст 100% Ширины + Картинка Синий фон
++textImageSection560({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ title: 'Контекстная реклама для увеличения продаж',
+ center: true,
+ showTitle: true,
+ showText: true,
+ showButton: false
+})
+
+//Текст 100% Ширины + Картинка Белый фон
++textImageSection560({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ title: 'Контекстная реклама для увеличения продаж',
+ center: true,
+ showTitle: true,
+ showText: true,
+ showButton: false
+})
+
+//Текст 100% Ширины + Картинка Зеленый фон
++textImageSection560({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ title: 'Контекстная реклама для увеличения продаж',
+ center: true,
+ showTitle: true,
+ showText: true,
+ showButton: false
+})
+
+//4
+
+//Текст 50% Ширины + Картинка Синий фон
++textImageSection270({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8',
+ showButton: true
+})
+
+//Текст 50% Ширины + Картинка Белый фон
++textImageSection270({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ showButton: true
+})
+
+//Текст 50% Ширины + Картинка Зеленый фон
++textImageSection270({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ showButton: true
+})
+
+//5
+
+//Текст 50% Ширины + Картинка Синий фон
++textImageSection270({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ showButton: true,
+ linkMode: true,
+ linkColorClass: 'color__green',
+ linkClass: 'text__link-green'
+})
+
+//Текст 50% Ширины + Картинка Белый фон
++textImageSection270({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ showButton: true,
+ linkMode: true,
+ linkColorClass: 'color__green',
+ linkClass: 'text__link-green'
+})
+
+//Текст 50% Ширины + Картинка Зеленый фон
++textImageSection270({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ showButton: true,
+ linkMode: true,
+ linkColorClass: 'color__blue',
+ linkClass: 'text__link-blue'
+})
diff --git a/email-gen-overrides/reaspekt-master/blocks/texts.pug b/email-gen-overrides/reaspekt-master/blocks/texts.pug
new file mode 100644
index 0000000..2f4ac05
--- /dev/null
+++ b/email-gen-overrides/reaspekt-master/blocks/texts.pug
@@ -0,0 +1,83 @@
+include ./_factory
+
+//Текст 100% Ширины Синий фон
++textSection560({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8'
+})
+
+//Текст 100% Ширины Белый фон
++textSection560({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33'
+})
+
+//Текст 100% Ширины Зеленый фон
++textSection560({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33'
+})
+
+//Текст 50% Ширины Синий фон
++textSection270({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8'
+})
+
+//Текст 50% Ширины Белый фон
++textSection270({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33'
+})
+
+//Текст 50% Ширины Зеленый фон
++textSection270({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33'
+})
+
+//Текст 33% Ширины Синий фон
++textSection173({
+ bgClass: 'background__blue',
+ titleClass: 'color__white',
+ textClass: 'color__white',
+ buttonBg: '#130F33',
+ buttonText: '#AAC8C8'
+})
+
+//Текст 33% Ширины Белый фон
++textSection173({
+ bgClass: 'background__white',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33',
+ firstColumnExtraGap: true
+})
+
+//Текст 33% Ширины Зеленый фон
++textSection173({
+ bgClass: 'background__green',
+ titleClass: 'color__blue',
+ textClass: 'color__blue',
+ buttonBg: '#ffffff',
+ buttonText: '#130F33'
+})
diff --git a/z51-pug-builder/.dockerignore b/z51-pug-builder/.dockerignore
new file mode 100644
index 0000000..0ca39c0
--- /dev/null
+++ b/z51-pug-builder/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+dist
+.DS_Store
diff --git a/z51-pug-builder/.gitignore b/z51-pug-builder/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/z51-pug-builder/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/z51-pug-builder/.vscode/extensions.json b/z51-pug-builder/.vscode/extensions.json
new file mode 100644
index 0000000..bdef820
--- /dev/null
+++ b/z51-pug-builder/.vscode/extensions.json
@@ -0,0 +1,3 @@
+{
+ "recommendations": ["svelte.svelte-vscode"]
+}
diff --git a/z51-pug-builder/Dockerfile b/z51-pug-builder/Dockerfile
new file mode 100644
index 0000000..2f6c2e5
--- /dev/null
+++ b/z51-pug-builder/Dockerfile
@@ -0,0 +1,13 @@
+FROM node:20-alpine
+
+WORKDIR /app
+
+COPY z51-pug-builder/package*.json ./
+RUN npm install
+
+COPY z51-pug-builder/ ./
+COPY email-gen/ /email-gen/
+
+EXPOSE 5173
+
+CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"]
diff --git a/z51-pug-builder/README.md b/z51-pug-builder/README.md
new file mode 100644
index 0000000..51185dd
--- /dev/null
+++ b/z51-pug-builder/README.md
@@ -0,0 +1,243 @@
+# Z51 Pug Builder — руководство по интерфейсу
+
+Этот файл описывает **интерфейс** и **логику использования** конструктора писем. Здесь нет технических деталей реализации — только то, как устроена работа в UI, где что находится и зачем это нужно.
+
+---
+
+## 1. Общая идея
+
+Конструктор собирает письмо из блоков, взятых из `block.pug`. Каждый блок — это кусок Pug-кода, размеченный комментарием `//Название`. В интерфейсе вы выбираете нужные блоки, редактируете только важные поля (текст, ссылки, изображения, ID и т.п.), а на выходе получаете собранный Pug-файл.
+
+Главная выгода: быстро собирать письма без ручного копирования и без риска повредить структуру шаблона.
+
+---
+
+## 2. Навигация по интерфейсу
+
+Верхняя панель (шапка) содержит:
+- **Иконка дискеты** — открывает поле для названия пресета и сохранение пресета.
+- **Иконка папки** — переход на страницу пресетов.
+- **Сборка** — основная страница сборки письма.
+- **Настройки** — управление проектом и шаблонами.
+- **Переключатель темы** — светлая/тёмная.
+- **Назад / Сброс** — только на странице сборки.
+
+В левом сайдбаре:
+- **Название проекта** (крупно, окрашено акцентом) или логотип.
+- **Выбор проекта**.
+- **Добавить блок** (выпадающий список).
+- **Быстрые кнопки блоков**.
+- Счётчик общего количества блоков.
+
+---
+
+## 3. Страница «Сборка»
+
+Это основной экран для работы с письмом.
+
+### 3.1. Добавление блоков
+- Через выпадающий список «Добавить блок».
+- Через быстрые кнопки (настраиваются в настройках проекта).
+- Через кнопку «+» внизу каждого блока — добавляет новый блок **после текущего**.
+
+Новые блоки открываются сразу (развернуты).
+
+---
+
+### 3.2. Карточка блока
+Каждый блок — это карточка с:
+- названием блока;
+- кнопками **вверх / вниз / удалить**;
+- полями редактирования;
+- чекбоксом «Отступ после блока» + поле значения (если включено);
+- кнопками «PUG» и «+» (добавление блока после).
+
+В любой момент можно раскрыть/свернуть блок по клику на его заголовок.
+
+---
+
+### 3.3. Поля редактирования
+Интерфейс не показывает весь код, а только полезные поля.
+
+Типы полей:
+- **Текст** — содержимое внутри строк Pug.
+- **Ссылки** — `href`.
+- **Картинки** — `src`.
+- **Кнопки** — если в блоке есть `+buttonRounded(...)`, показываются отдельные поля для текста и ссылки.
+- **Товары** — если есть `+productsX`, показывается поле «ID товаров».
+
+Поля автоматически определяются из шаблона блока, но их можно включать/выключать и переименовывать в настройках.
+
+---
+
+### 3.4. Форматирование текста
+Для текстовых полей есть панель действий:
+- **T** — типограф.
+- **Ж** — жирный текст.
+- **•** — маркер.
+- **** / ** ** — списки.
+- **A + URL** — ссылка.
+- **Глаз** — предпросмотр HTML.
+
+---
+
+### 3.5. Абзацы в блоке «Текст»
+Блок «Текст» поддерживает несколько абзацев:
+- Кнопка **«Абзац»** добавляет новый абзац.
+- У каждого дополнительного абзаца есть кнопка удаления.
+- Между абзацами автоматически вставляется `+spacerLine(20)`.
+
+---
+
+### 3.6. Списки (нумерованные и маркированные)
+Для блоков списков:
+- Редактируются только **тексты пунктов**.
+- Картинки маркеров/цифр скрыты и не редактируются.
+- Кнопка **«Пункт»** добавляет новый пункт.
+- Кнопка **×** удаляет пункт.
+
+При добавлении блока со списком создаётся только **первый пункт**, остальные добавляются вручную.
+
+---
+
+### 3.7. Отступы
+- **Глобальный отступ** задаётся в настройках.
+- У каждого блока можно включить/выключить отступ и задать своё значение.
+- Для некоторых блоков отступ не добавляется автоматически (Разделитель, Отступ 20/40).
+
+---
+
+### 3.8. Итоговый Pug
+Правый блок — это итоговый Pug-код:
+- он обновляется при любом изменении;
+- можно **копировать** или **скачивать**;
+- при редактировании блок подсвечивается и прокручивается в Pug-окне.
+
+---
+
+## 4. Страница «Настройки»
+
+Настройки делятся на вкладки:
+
+### 4.1. Общие
+**Источник блоков**
+- Загрузка нового `block.pug`.
+- Перезагрузка текущего источника.
+
+**Поля блока**
+- Выбор любого блока.
+- Включение/выключение полей.
+- Переименование подписи поля.
+
+---
+
+### 4.2. Глобальные блоки
+- Список всех блоков проекта.
+- Для каждого блока можно редактировать Pug-шаблон напрямую.
+- Настроить отступ по умолчанию для конкретного блока.
+
+---
+
+### 4.3. Текущий проект
+Настройки внешнего вида проекта:
+- Название проекта (в левом верхнем углу).
+- Цвет акцентов (через палитру или HEX).
+- Логотип проекта (загрузка изображения).
+
+Логотип хранится в настройках проекта.
+
+---
+
+### 4.4. Быстрые блоки
+- Выбор блоков, которые появятся слева как быстрые кнопки.
+- Можно добавлять/удалять элементы.
+
+---
+
+### 4.5. Новый проект
+- Создание нового проекта.
+- Каждый проект имеет собственный `block.pug`, настройки, пресеты и сборки.
+
+---
+
+## 5. Пресеты
+
+Пресет — это сохранённая сборка письма.
+
+### Как работают пресеты
+- Сохраняются через иконку дискеты в шапке.
+- Открываются через страницу «Пресеты».
+- При сохранении, если имя уже существует — система спрашивает, **обновить или создать новый**.
+- При открытии пресета его имя автоматически подставляется в поле сохранения.
+
+### На странице пресетов
+- Отображаются название и дата/время сохранения.
+- Есть поиск по названию.
+- Есть сортировка: сначала новые / сначала старые.
+
+---
+
+## 6. Подсказки и наведение
+
+Все кнопки и поля имеют всплывающие подсказки (`title`), чтобы быстро понимать назначение элемента.
+
+---
+
+## 7. Выгоды использования конструктора
+
+- **Быстрее сборки** — письмо собирается за минуты.
+- **Нет ошибок в структуре Pug** — редактируются только нужные фрагменты.
+- **Один интерфейс для всех проектов**.
+- **Удобное сохранение и повторное использование** через пресеты.
+- **Гибкая настройка под разные команды и бренды**.
+
+---
+
+## 8. Типовые сценарии
+
+### 8.1. Быстрая сборка письма с нуля
+1) Перейдите в «Сборка».
+2) Добавьте блоки через быстрые кнопки или выпадающий список.
+3) Заполните поля (текст, ссылки, ID товаров).
+4) Проверьте итоговый Pug справа.
+5) Скопируйте или скачайте итоговый файл.
+
+### 8.2. Сборка письма по шаблону (пресету)
+1) Откройте страницу «Пресеты».
+2) Найдите нужный пресет через поиск.
+3) Нажмите «Открыть».
+4) Отредактируйте детали и сохраните обратно.
+
+### 8.3. Обновление существующего пресета
+1) Откройте пресет.
+2) Внесите изменения.
+3) Нажмите иконку дискеты — имя пресета подставится автоматически.
+4) Подтвердите обновление в диалоге.
+
+### 8.4. Настройка быстрых блоков
+1) Перейдите в «Настройки» → «Быстрые блоки».
+2) Выберите нужные блоки и нажмите «Добавить».
+3) Удалите ненужные кнопки через «×».
+4) Эти кнопки появятся слева в «Сборке».
+
+### 8.5. Добавление абзацев в текстовом блоке
+1) Откройте блок «Текст».
+2) Нажмите «Абзац».
+3) Заполните новый пункт текста.
+4) При необходимости удалите абзац кнопкой «×».
+
+### 8.6. Работа со списками
+1) Добавьте блок «Маркированный список» или «Нумерованный список».
+2) Заполните первый пункт.
+3) Добавьте новые пункты кнопкой «Пункт».
+4) Удалите лишние пункты кнопкой «×».
+
+### 8.7. Создание нового проекта
+1) Откройте «Настройки» → «Новый проект».
+2) Введите имя проекта, нажмите «Добавить».
+3) Загрузите `block.pug` для нового проекта.
+4) Настройте быстрые блоки, цвета и поля.
+
+---
+
+Если нужно — могу добавить в интерфейс отдельную страницу «Справка» с этим текстом.
diff --git a/z51-pug-builder/index.html b/z51-pug-builder/index.html
new file mode 100644
index 0000000..21eaad3
--- /dev/null
+++ b/z51-pug-builder/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+ ASPEKTER
+
+
+
+
+
+
diff --git a/z51-pug-builder/jsconfig.json b/z51-pug-builder/jsconfig.json
new file mode 100644
index 0000000..c7a0b10
--- /dev/null
+++ b/z51-pug-builder/jsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "moduleResolution": "bundler",
+ "target": "ESNext",
+ "module": "ESNext",
+ /**
+ * svelte-preprocess cannot figure out whether you have
+ * a value or a type, so tell TypeScript to enforce using
+ * `import type` instead of `import` for Types.
+ */
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "resolveJsonModule": true,
+ /**
+ * To have warnings / errors of the Svelte compiler at the
+ * correct position, enable source maps by default.
+ */
+ "sourceMap": true,
+ "esModuleInterop": true,
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+ /**
+ * Typecheck JS in `.svelte` and `.js` files by default.
+ * Disable this if you'd like to use dynamic types.
+ */
+ "checkJs": true
+ },
+ /**
+ * Use global.d.ts instead of compilerOptions.types
+ * to avoid limiting type declarations.
+ */
+ "include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
+}
diff --git a/z51-pug-builder/package-lock.json b/z51-pug-builder/package-lock.json
new file mode 100644
index 0000000..30dd1a2
--- /dev/null
+++ b/z51-pug-builder/package-lock.json
@@ -0,0 +1,2051 @@
+{
+ "name": "va-aspekter",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "va-aspekter",
+ "version": "0.0.0",
+ "dependencies": {
+ "@yonote/js-sdk": "^0.1.1",
+ "basic-ftp": "^5.0.5",
+ "ssh2-sftp-client": "^11.0.0"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
+ "svelte": "^5.43.8",
+ "vite": "^7.2.4"
+ }
+ },
+ "node_modules/@borewit/text-codec": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.1.tgz",
+ "integrity": "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
+ "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz",
+ "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz",
+ "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz",
+ "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz",
+ "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz",
+ "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz",
+ "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz",
+ "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz",
+ "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz",
+ "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz",
+ "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz",
+ "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz",
+ "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz",
+ "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz",
+ "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz",
+ "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz",
+ "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz",
+ "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz",
+ "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz",
+ "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz",
+ "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz",
+ "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz",
+ "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
+ "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
+ "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
+ "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
+ "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
+ "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
+ "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
+ "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
+ "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
+ "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
+ "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
+ "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
+ "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
+ "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
+ "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
+ "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
+ "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
+ "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
+ "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
+ "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
+ "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
+ "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
+ "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
+ "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@sveltejs/acorn-typescript": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.8.tgz",
+ "integrity": "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^8.9.0"
+ }
+ },
+ "node_modules/@sveltejs/vite-plugin-svelte": {
+ "version": "6.2.4",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
+ "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
+ "deepmerge": "^4.3.1",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.0",
+ "vitefu": "^1.1.1"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24"
+ },
+ "peerDependencies": {
+ "svelte": "^5.0.0",
+ "vite": "^6.3.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@sveltejs/vite-plugin-svelte-inspector": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
+ "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "obug": "^2.1.0"
+ },
+ "engines": {
+ "node": "^20.19 || ^22.12 || >=24"
+ },
+ "peerDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0",
+ "svelte": "^5.0.0",
+ "vite": "^6.3.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@tokenizer/inflate": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
+ "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "fflate": "^0.8.2",
+ "token-types": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@yonote/js-sdk": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@yonote/js-sdk/-/js-sdk-0.1.1.tgz",
+ "integrity": "sha512-yq/3bCAM3lVBlJVQ8yB8IsR6CO0wH5JMPWT+zTQYmFvPOB6epDFl2rBxB9g5kf74h9BHOh1c/eSpPu/MRTSJEg==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "1.12.0",
+ "file-type": "21.0.0",
+ "uuid": "11.1.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
+ "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/asn1": {
+ "version": "0.2.6",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
+ "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": "~2.1.0"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.12.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
+ "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
+ "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/basic-ftp": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz",
+ "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/bcrypt-pbkdf": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tweetnacl": "^0.14.3"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "license": "MIT"
+ },
+ "node_modules/buildcheck": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
+ "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
+ "optional": true,
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "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/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concat-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
+ "engines": [
+ "node >= 6.0"
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^3.0.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "node_modules/cpu-features": {
+ "version": "0.0.10",
+ "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
+ "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "dependencies": {
+ "buildcheck": "~0.0.6",
+ "nan": "^2.19.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "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/deepmerge": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
+ "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/devalue": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
+ "integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==",
+ "license": "MIT"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.27.2",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz",
+ "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.27.2",
+ "@esbuild/android-arm": "0.27.2",
+ "@esbuild/android-arm64": "0.27.2",
+ "@esbuild/android-x64": "0.27.2",
+ "@esbuild/darwin-arm64": "0.27.2",
+ "@esbuild/darwin-x64": "0.27.2",
+ "@esbuild/freebsd-arm64": "0.27.2",
+ "@esbuild/freebsd-x64": "0.27.2",
+ "@esbuild/linux-arm": "0.27.2",
+ "@esbuild/linux-arm64": "0.27.2",
+ "@esbuild/linux-ia32": "0.27.2",
+ "@esbuild/linux-loong64": "0.27.2",
+ "@esbuild/linux-mips64el": "0.27.2",
+ "@esbuild/linux-ppc64": "0.27.2",
+ "@esbuild/linux-riscv64": "0.27.2",
+ "@esbuild/linux-s390x": "0.27.2",
+ "@esbuild/linux-x64": "0.27.2",
+ "@esbuild/netbsd-arm64": "0.27.2",
+ "@esbuild/netbsd-x64": "0.27.2",
+ "@esbuild/openbsd-arm64": "0.27.2",
+ "@esbuild/openbsd-x64": "0.27.2",
+ "@esbuild/openharmony-arm64": "0.27.2",
+ "@esbuild/sunos-x64": "0.27.2",
+ "@esbuild/win32-arm64": "0.27.2",
+ "@esbuild/win32-ia32": "0.27.2",
+ "@esbuild/win32-x64": "0.27.2"
+ }
+ },
+ "node_modules/esm-env": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
+ "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/esrap": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz",
+ "integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.4.15"
+ }
+ },
+ "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/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-type": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
+ "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/inflate": "^0.2.7",
+ "strtok3": "^10.2.2",
+ "token-types": "^6.0.0",
+ "uint8array-extras": "^1.4.0"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/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-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/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/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "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/is-reference": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
+ "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.6"
+ }
+ },
+ "node_modules/locate-character": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
+ "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "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/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "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/nan": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
+ "integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "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/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "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.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "license": "MIT",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.57.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
+ "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.57.1",
+ "@rollup/rollup-android-arm64": "4.57.1",
+ "@rollup/rollup-darwin-arm64": "4.57.1",
+ "@rollup/rollup-darwin-x64": "4.57.1",
+ "@rollup/rollup-freebsd-arm64": "4.57.1",
+ "@rollup/rollup-freebsd-x64": "4.57.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.57.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.57.1",
+ "@rollup/rollup-linux-arm64-musl": "4.57.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.57.1",
+ "@rollup/rollup-linux-loong64-musl": "4.57.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.57.1",
+ "@rollup/rollup-linux-ppc64-musl": "4.57.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.57.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.57.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-gnu": "4.57.1",
+ "@rollup/rollup-linux-x64-musl": "4.57.1",
+ "@rollup/rollup-openbsd-x64": "4.57.1",
+ "@rollup/rollup-openharmony-arm64": "4.57.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.57.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.57.1",
+ "@rollup/rollup-win32-x64-gnu": "4.57.1",
+ "@rollup/rollup-win32-x64-msvc": "4.57.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/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/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/ssh2": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
+ "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "asn1": "^0.2.6",
+ "bcrypt-pbkdf": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=10.16.0"
+ },
+ "optionalDependencies": {
+ "cpu-features": "~0.0.10",
+ "nan": "^2.23.0"
+ }
+ },
+ "node_modules/ssh2-sftp-client": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/ssh2-sftp-client/-/ssh2-sftp-client-11.0.0.tgz",
+ "integrity": "sha512-lOjgNYtioYquhtgyHwPryFNhllkuENjvCKkUXo18w/Q4UpEffCnEUBfiOTlwFdKIhG1rhrOGnA6DeKPSF2CP6w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "concat-stream": "^2.0.0",
+ "promise-retry": "^2.0.1",
+ "ssh2": "^1.15.0"
+ },
+ "engines": {
+ "node": ">=18.20.4"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://square.link/u/4g7sPflL"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strtok3": {
+ "version": "10.3.4",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz",
+ "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/svelte": {
+ "version": "5.49.1",
+ "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.1.tgz",
+ "integrity": "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.4",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@sveltejs/acorn-typescript": "^1.0.5",
+ "@types/estree": "^1.0.5",
+ "acorn": "^8.12.1",
+ "aria-query": "^5.3.1",
+ "axobject-query": "^4.1.0",
+ "clsx": "^2.1.1",
+ "devalue": "^5.6.2",
+ "esm-env": "^1.2.1",
+ "esrap": "^2.2.2",
+ "is-reference": "^3.0.3",
+ "locate-character": "^3.0.0",
+ "magic-string": "^0.30.11",
+ "zimmerframe": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/token-types": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
+ "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
+ "license": "MIT",
+ "dependencies": {
+ "@borewit/text-codec": "^0.2.1",
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
+ "license": "Unlicense"
+ },
+ "node_modules/typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
+ "license": "MIT"
+ },
+ "node_modules/uint8array-extras": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
+ "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
+ "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/esm/bin/uuid"
+ }
+ },
+ "node_modules/vite": {
+ "version": "7.3.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
+ "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "esbuild": "^0.27.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "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",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.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
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "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/vitefu": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz",
+ "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==",
+ "dev": true,
+ "license": "MIT",
+ "workspaces": [
+ "tests/deps/*",
+ "tests/projects/*",
+ "tests/projects/workspace/packages/*"
+ ],
+ "peerDependencies": {
+ "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0"
+ },
+ "peerDependenciesMeta": {
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/zimmerframe": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz",
+ "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==",
+ "dev": true,
+ "license": "MIT"
+ }
+ }
+}
diff --git a/z51-pug-builder/package.json b/z51-pug-builder/package.json
new file mode 100644
index 0000000..ab84554
--- /dev/null
+++ b/z51-pug-builder/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "va-aspekter",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@yonote/js-sdk": "^0.1.1",
+ "basic-ftp": "^5.0.5",
+ "ssh2-sftp-client": "^11.0.0"
+ },
+ "devDependencies": {
+ "@sveltejs/vite-plugin-svelte": "^6.2.1",
+ "svelte": "^5.43.8",
+ "vite": "^7.2.4"
+ }
+}
diff --git a/z51-pug-builder/public/Block.pug b/z51-pug-builder/public/Block.pug
new file mode 100644
index 0000000..87257d0
--- /dev/null
+++ b/z51-pug-builder/public/Block.pug
@@ -0,0 +1,156 @@
+//Заголовок зеленый
+tr
+ td.paddingWrapperBig
+ +defaultTable("100%")
+ tr
+ td(align="center")
+ span.text.smallHeader.bold.greenText Конитива, герой!
+
+
+//Заголовок серый
+tr
+ td.paddingWrapperBig
+ +defaultTable("100%")
+ tr
+ td(align="center")
+ span.text.smallHeader.bold.groceryText Конитива, герой!
+
+
+//Текст
+tr
+ td.paddingWrapperBig
+ +defaultTable("100%")
+ tr
+ td
+ span.text.groceryText Пока одни вспоминают старую «Якудзу-3» с ее бесконечными блоками и симулятором няньки, студия Ryu Ga Gotoku Studio готовит полноценную революцию!
+
+//Доп. текст
+ +spacerLine(20)
+ tr
+ td
+ span.text.groceryText Минимальные 1080p / 30 FPS (с FSR) Проц: Intel i3-8100 / AMD Ryzen 3 2300X Видяха: NVIDIA GTX 1650 / AMD RX 6400 Оперативочка: 8 ГБ
+
+
+
+
+
+//Отступ 20
++spacerLine(20)
+
+
+//Отступ 40
++spacerLine(40)
+
+//3 товара в ряд
++products3inRow({
+ '144839': {
+ imageSrc: '',
+ name: 'BAD BUNNY',
+ category: 'Осторожно, этот кролик плохой)',
+ },
+ '142672': {
+ imageSrc: '',
+ name: 'MONARCH',
+ category: 'Ролекс среди кресел',
+ },
+ '140228': {
+ imageSrc: '',
+ name: 'Kitty Meow',
+ category: 'Кошечка делает мур-р-р!',
+ },
+})
+
+
+//Разделитель
++dividerZ(525, 2)
++spacerLine(40)
+
+//Банер
+tr
+ td(align="center")
+ a(href="https://z51.ru" target="_blank")
+ img(src="https://z51.ru/upload/email/newsletter-2026/20-01-2026/1.jpg" alt="pic" style="display: block" width="600")
+
+
+
+//Кнопка
+tr
+ td(align="center").paddingWrapper
+ +buttonRounded("Смотреть топовые ПК", "https://z51.ru/catalog/gaming-pc/", 525, 42, "#c9e905", 18, "#000000", 4, "#c9e905").bold.text
+
+//Две кнопки
+tr
+ td.paddingWrapper
+ +defaultTable("100%")
+ tr
+ td(width="250")
+ +defaultTable("250")
+ tr
+ td(align="center")
+ +buttonRounded("Игровые кресла", "https://z51.ru/catalog/kresla/", 240, 42, "#c9e905", 18, "#000000", 4, "#c9e905").bold.text
+ +tdFixed(36)
+ td(width="250")
+ +defaultTable("250")
+ tr
+ td(align="center")
+ +buttonRounded("Эргономичные кресла", "https://z51.ru/catalog/ergonomic-office-chairs/", 240, 42, "#c9e905", 18, "#000000", 4, "#c9e905", 4).bold.text
+
+
+//Блок преимуществ
+tr
+ td.paddingWrapperBig
+ +defaultTable("100%")
+ tr
+ td(align="center")
+ span.text.smallHeader.bold.greenText Почему выбирают товары у Баззи?
+
++spacerLine(40)
+
+tr
+ td.paddingWrapper
+ +defaultTable("100%")
+ tr
+ td(width="250" valign="top")
+ +defaultTable("250")
+ //Unordered List
+ tr
+ td
+ +defaultTable("100%")
+ tr
+ +tdFixed(12, "center", "top").markerPadding
+ img(src="https://z51.ru/upload/email/master-template/markers/marker.png" alt="pic" width="12")
+ +tdFixed(10)
+ td
+ span.groceryText Официальный магазин В наличии всё самое вкусное от ZONE 51 — кресла, столы, периферия и аксессуары
+ +spacerLine(20)
+ tr
+ +tdFixed(12, "center", "top").markerPadding
+ img(src="https://z51.ru/upload/email/master-template/markers/marker.png" alt="pic" width="12")
+ +tdFixed(10)
+ td
+ span.groceryText Первоклассные и надежные продукты Из качественных, инопланетных и вроде как безопасных материалов для себя, родных и друзей. Не понравилось? Можешь вернуть в течение 28 дней с даты приобретения
+ +tdFixed(36)
+ td(width="250" valign="top")
+ +defaultTable("250")
+ tr
+ td
+ +defaultTable("100%")
+ tr
+ +tdFixed(12, "center", "top").markerPadding
+ img(src="https://z51.ru/upload/email/master-template/markers/marker.png" alt="pic" width="12")
+ +tdFixed(10)
+ td
+ span.groceryText Новинки и эксклюзивы Я постоянно потею над новыми товарами, которые можно приобрести только здесь
+ +spacerLine(20)
+ tr
+ +tdFixed(12, "center", "top").markerPadding
+ img(src="https://z51.ru/upload/email/master-template/markers/marker.png" alt="pic" width="12")
+ +tdFixed(10)
+ td
+ span.groceryText Клиенто-ориентированность Даю до 3 лет гарантии на свой товар +1 год за покупку в фирменном магазине ZONE 51 (онлайн и офлайн), а человеки у трубки помогут быстро обкашлять любые вопросы
++spacerLine(40)
+
+tr
+ td(align="center").paddingWrapper
+ +buttonRounded("Залетай к нам!", "https://z51.ru/", 300, 42, "#c9e905", 18, "#000000", 4, "#c9e905").bold.text
+//Конец блока преимуществ
\ No newline at end of file
diff --git a/z51-pug-builder/public/favicon.jpg b/z51-pug-builder/public/favicon.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..ab74f63863a2b80440724e91bc3255532790bf55
GIT binary patch
literal 1157
zcmex=Jq?U}9uuW@2GxWo2Ojs;&jfGq4D<
z3Mm>ovIz$!vMUve7&T5@$f4}C@t|nX#SbdRNkvVZTw>x9l2WQ_>Kd9_CZ=ZQ7M51d
zF0O9w9-dyoA)#U65s^{JDXD4c8JStdC8cHM6_r)ZEv;?s9i3g1CQq3GGAU*RJ2VdF$b$$4{OP
zfBE|D`;VW$K>lK6V1@@7#A9gw0tNyj6AKG73p>bPj7;S~%q+;ls%Xe2x9$;rq#OV6afSDRI-wmO
zy>xrmA9<^Obk@C{%d@J!wk~Teu7AYOoLwh;p>(ft(DhuCA8C6xN951C{>F$;VtxNS
zap|av%+x=xN)@H{d{tYxda;I^uIo-!sflwIU5U=L)fbw&<=Qjq8zrQtp`2I%q
zKMvbRbMp^Po2T)2fqh>8E&D_A%==V+)W`4atMz|WfABv8Uwp;-2hTh0)YVpf`+Rmm
zMf(w}Wm`XVKi0jrZC>cyxktX2Sxt!N&)U9PHtfWQNoOi13C?a4*E@7x(5o;kB>Sw>
z9*s|Ti`@)+YpnX-s$AbUq1f7ecYWiF`5)r$|4=bMe*cHoe}<#=H%=d}XMg-7`orym
z@jNB(d&KP}>ofReq@&l?`&{1RUi;ytjr@c4d==GhZXc(2J=-po-t+Z#t=zL(`9t=(
z`@(m9)N8NQo_%LWzRls3Erp-gzg=}@%?mAwtn$D-uJv11T6%vk*!%M6Nlm}g9-cW*
z)+e9YpSHhoTm6>ff5h4!p7>Gu;BL|BZ-T(sQTccF+WyV{2lWNwUvB^QkN4y9xAJ!V
z_J{8ao41w9dR_CdiLQQd+dDSd?PJ{1<@e8peWuN%f{G}8BI3|{_c-wtskD6on
ztt;JUb~KB9tms%a*Q>%WHB9EE^}&!msVtYCwN@Uj(N)0`oCQOHB?t$7l4C<1DN`60RC+Ov;c(o1cU_m
zgoFfyL_~zdq|~IOBqXHtlvL!@O!UmmO!SP5tQ`DYtZaPjjEvlmxcLMGL0}LImx#EC
zkT|~(Na%k-aEOSANQp`5NJ;60SQ%M`{-5pNTL29)&OP39JRBMTE)5PI4bHzY06PGH
zL-0Se|GyF7;^7k#;Q)yL^CGALICyxtc!YTW!6zUk0FdGQ=LHbZ(DD#UXb{oynmPv1
zOUCkPW>>W{NWq7R8G%lLi_%(gdsj^SGO}htIn^E4&cV4OO9FE7+B%(r<`yB#|HWm)
z0pR_&=>NU!{{sJ)pZ}b)|H1p8;o#%o;uHLT;Bath@OWq?@HI>c=y)9^=>zy;v#SUh
zq%_;%!;5=YjMu<_n*ee=od5QPM*~m;Y&_5f4er*Ny&uTim&($vI5rAS7;tSN&$%`>
z{9u3<7U-det#hK^{e^Pxp!YqV76-5x?|%<7dzAGLfL~eqaPj*DU0WDQ`f>SB1JJzy
zyqFS?PMAs^x+x(1Sw);3KCcgI8=I)aYK6c0e?vFRcX@ehS_c@9t8p?zIKtc&xRC5z00jLYoYFZ%t(c=FRl;d>#TK|&fxf1%DNT-HdY6EScG{Tf&G`pj%d>wmN!o;N>znAm-BJ;%iGrCXJK7k#)7?HrWp;<
zS)*(U=VQ||PWcIAAwqe7SsFkRHo6Dw{j7$_OqxAFPy=&LDf