Initial commit: WA Dev Tools service
This commit is contained in:
commit
54d220e0f4
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
uploads/
|
||||||
|
downloads/
|
||||||
|
*.log
|
||||||
|
.idea/
|
||||||
|
*.bak
|
||||||
|
.env
|
||||||
115
EDITOR-DOC.md
Normal file
115
EDITOR-DOC.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
# Mini Photoshop — Browser Image Editor
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Client-side image editor tool at `/editor` on images.wadevelop.ru.
|
||||||
|
All processing via Canvas API — images never leave the browser.
|
||||||
|
|
||||||
|
## File
|
||||||
|
- `/public/editor.html` — standalone page (~600 lines)
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
- **Sidebar** (56px) — standard site navigation
|
||||||
|
- **Toolbar** (44px top) — Open, Export, drawing tools, transforms, undo/redo, color, zoom
|
||||||
|
- **Tool Options Bar** (36px, contextual) — appears when tool selected
|
||||||
|
- **Canvas Viewport** (center) — checkerboard bg for transparency, 3 stacked canvases
|
||||||
|
- **Status Bar** (28px bottom) — dimensions, zoom%, cursor coords, history position
|
||||||
|
|
||||||
|
## Canvas Architecture
|
||||||
|
3 overlaid `<canvas>` elements:
|
||||||
|
1. `canvasMain` — final image
|
||||||
|
2. `canvasOverlay` — preview of drawing/crop/filters (cleared each frame)
|
||||||
|
3. `canvasUI` — cursors, selection frames
|
||||||
|
|
||||||
|
Zoom/pan via CSS `transform: translate() scale()` on wrapper (GPU-accelerated).
|
||||||
|
|
||||||
|
## Undo/Redo
|
||||||
|
- History as PNG blobs (`canvas.toBlob()` → `URL.createObjectURL()`)
|
||||||
|
- Max 30 snapshots
|
||||||
|
- Ctrl+Z / Ctrl+Shift+Z
|
||||||
|
|
||||||
|
## Image Loading
|
||||||
|
- File input (Open button)
|
||||||
|
- Drag & drop on viewport
|
||||||
|
- Paste from clipboard (Ctrl+V)
|
||||||
|
- Before loading — dropzone with hint, toolbar disabled
|
||||||
|
|
||||||
|
## Tools
|
||||||
|
|
||||||
|
### Crop
|
||||||
|
- Presets: Free, 1:1, 4:3, 16:9, 3:2, 2:3, 9:16
|
||||||
|
- 8 drag handles (corners + edges)
|
||||||
|
- Semi-transparent overlay outside crop, rule of thirds
|
||||||
|
- Apply / Cancel buttons
|
||||||
|
|
||||||
|
### Resize
|
||||||
|
- Width/height in px, aspect ratio lock (on by default)
|
||||||
|
- `imageSmoothingQuality = 'high'`
|
||||||
|
- Max 16384x16384
|
||||||
|
|
||||||
|
### Rotate 90 CW/CCW
|
||||||
|
- Two buttons in toolbar
|
||||||
|
- Via temp canvas with `ctx.translate()` + `ctx.rotate()`
|
||||||
|
|
||||||
|
### Flip H/V
|
||||||
|
- Two buttons, via `ctx.scale(-1, 1)` / `ctx.scale(1, -1)`
|
||||||
|
|
||||||
|
### Brightness / Contrast / Saturation
|
||||||
|
- Three sliders -100...+100
|
||||||
|
- Preview via CSS `filter` on overlay (instant)
|
||||||
|
- Commit via pixel-level ImageData processing
|
||||||
|
- Apply / Cancel
|
||||||
|
|
||||||
|
### Brush
|
||||||
|
- Sliders: size 1-100px, opacity 0-100%
|
||||||
|
- `lineCap: 'round'`, `lineJoin: 'round'`
|
||||||
|
- Drawing on overlay → commit to main on pointerup
|
||||||
|
|
||||||
|
### Eraser
|
||||||
|
- Like brush but `globalCompositeOperation: 'destination-out'`
|
||||||
|
- Draws directly on main canvas
|
||||||
|
|
||||||
|
### Line, Rectangle, Circle, Arrow
|
||||||
|
- Stroke width 1-20px, fill toggle (rect/circle)
|
||||||
|
- Shift for snap to 45 degrees (line/arrow) and square/circle (rect/circle)
|
||||||
|
- Arrow: filled triangle arrowhead
|
||||||
|
|
||||||
|
### Text
|
||||||
|
- Font select (sans-serif, serif, monospace, Manrope, JetBrains Mono)
|
||||||
|
- Size 8-200px, Bold, Italic, Color
|
||||||
|
- Click on canvas → `<textarea>` at position → Ctrl+Enter to commit
|
||||||
|
- `ctx.fillText()` with multiline text support
|
||||||
|
|
||||||
|
### Color Picker (Eyedropper)
|
||||||
|
- Click → `getImageData(x, y, 1, 1)` → hex → sets as foreground color
|
||||||
|
- Color chosen via native `<input type="color">`
|
||||||
|
|
||||||
|
## Export
|
||||||
|
- Formats: PNG, JPEG, WebP
|
||||||
|
- Quality 1-100 (for JPEG/WebP)
|
||||||
|
- Filename (pre-filled from original)
|
||||||
|
- Warning for JPEG + transparency
|
||||||
|
- `canvas.toBlob()` → download via `<a download>`
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Shortcut | Action |
|
||||||
|
|---|---|
|
||||||
|
| Ctrl+O | Open |
|
||||||
|
| Ctrl+S | Export |
|
||||||
|
| Ctrl+Z | Undo |
|
||||||
|
| Ctrl+Shift+Z | Redo |
|
||||||
|
| B | Brush |
|
||||||
|
| E | Eraser |
|
||||||
|
| T | Text |
|
||||||
|
| C | Crop |
|
||||||
|
| L | Line |
|
||||||
|
| U | Rectangle |
|
||||||
|
| O | Ellipse |
|
||||||
|
| A | Arrow |
|
||||||
|
| I | Eyedropper |
|
||||||
|
| [ / ] | Brush size +/- |
|
||||||
|
| Space+drag | Pan |
|
||||||
|
| Ctrl+scroll | Zoom |
|
||||||
|
| Ctrl+0 | Fit |
|
||||||
|
| Ctrl+1 | 100% |
|
||||||
|
| Escape | Cancel |
|
||||||
250
SVGEDITOR-FEATURES.md
Normal file
250
SVGEDITOR-FEATURES.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
# SVG Редактор — Полный функционал
|
||||||
|
|
||||||
|
**URL:** https://images.wadevelop.ru/svgeditor
|
||||||
|
**Файл:** /mnt/webdata/www/images.wadevelop.ru/public/svgeditor.html (2099 строк)
|
||||||
|
**API:** server.js — 3 эндпоинта
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Интерфейс (как фоторедактор /editor)
|
||||||
|
|
||||||
|
- **Тулбар** (44px) — горизонтальная панель инструментов сверху
|
||||||
|
- **Панель опций** (36px) — контекстная, показывает настройки активного инструмента
|
||||||
|
- **Вкладки** (30px) — несколько документов одновременно
|
||||||
|
- **Viewport** — тёмный фон (#2c2c30), белый SVG-холст по центру
|
||||||
|
- **Статус-бар** (28px) — размер холста, зум, координаты, кол-во элементов
|
||||||
|
- **Сайдбар** (56px) — навигация по сервисам (как во всех страницах)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Инструменты рисования
|
||||||
|
|
||||||
|
| Инструмент | Клавиша | Описание |
|
||||||
|
|------------|---------|----------|
|
||||||
|
| Выделение | V | Клик для выделения, drag для перемещения |
|
||||||
|
| Прямоугольник | R | Shift — квадрат |
|
||||||
|
| Эллипс | O | Shift — круг |
|
||||||
|
| Линия | L | Shift — snap к 45° |
|
||||||
|
| Перо | P | Клик по точкам, замыкание на первую точку (красный кружок), двойной клик — открытый путь |
|
||||||
|
| Текст | T | Клик → ввод текста, двойной клик — редактирование |
|
||||||
|
|
||||||
|
### После рисования:
|
||||||
|
- Автопереключение на Выделение (V)
|
||||||
|
- Элемент сразу выделен с хэндлами
|
||||||
|
- Панель свойств открывается автоматически
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Выделение и трансформация
|
||||||
|
|
||||||
|
- **Клик** на элемент → выделение + 8 хэндлов (для rect/ellipse/text/group)
|
||||||
|
- **Клик** на линию → 2 хэндла на концах
|
||||||
|
- **Drag за хэндл** → ресайз (для всех типов, включая группы и AI иконки)
|
||||||
|
- **Drag за тело** → перемещение
|
||||||
|
- **Стрелки** ←↑→↓ — перемещение на 1px, Shift+стрелки — на 10px
|
||||||
|
- **Маркиз** — клик на пустое место + drag → пунктирная рамка, выделяет верхний элемент в области
|
||||||
|
- **Proximity selection** — тонкие элементы (линии, outline иконки) ловятся в радиусе 12px
|
||||||
|
- **Уже выделенный элемент** — повторный клик внутри его bounds не сбрасывает выделение
|
||||||
|
- Хэндлы прячутся при `elementFromPoint` чтобы не блокировать клик
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Панель опций (контекстная)
|
||||||
|
|
||||||
|
### При рисовании (rect/circle/line/path):
|
||||||
|
- Заливка — цвет + вкл/выкл (✓/✗)
|
||||||
|
- Непрозрачность — слайдер 0-100%
|
||||||
|
- Обводка — цвет + вкл/выкл
|
||||||
|
- Толщина обводки — число
|
||||||
|
|
||||||
|
### При выделении элемента:
|
||||||
|
- Те же контролы с текущими значениями элемента
|
||||||
|
- Изменения применяются сразу
|
||||||
|
|
||||||
|
### При инструменте Текст:
|
||||||
|
- Размер шрифта
|
||||||
|
- Цвет
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Панель свойств (правая, автопоказ)
|
||||||
|
|
||||||
|
- Позиция: X, Y (CX, CY для эллипса, X1/Y1 для линии)
|
||||||
|
- Размер: W, H (RX, RY для эллипса)
|
||||||
|
- Стиль: Fill (цвет), Stroke (цвет), Stroke-width, Opacity
|
||||||
|
- Текст: Font-size
|
||||||
|
- Появляется автоматически при выделении
|
||||||
|
- Скрывается при снятии выделения
|
||||||
|
- Кнопка × для закрытия
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Система слоёв
|
||||||
|
|
||||||
|
- **Слой 1** создаётся автоматически (каждый слой = `<g data-layer="true">`)
|
||||||
|
- Все новые объекты создаются на **активном слое**
|
||||||
|
- Кнопка **«+ Новый слой»** внизу панели
|
||||||
|
- **Активный слой** подсвечен синим с полоской слева
|
||||||
|
- Клик по слою → делает его активным
|
||||||
|
- Каждый слой: видимость (👁), удаление (×)
|
||||||
|
- Объекты внутри слоя: клик для выделения, × для удаления
|
||||||
|
- Группы отмечены 📁 с количеством детей
|
||||||
|
- Минимум 1 слой (нельзя удалить последний)
|
||||||
|
- Очистка холста пересоздаёт Слой 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Группировка
|
||||||
|
|
||||||
|
| Действие | Клавиша | Описание |
|
||||||
|
|----------|---------|----------|
|
||||||
|
| Группировать | Ctrl+G | Все объекты активного слоя → `<g data-name="Группа">` |
|
||||||
|
| Разгруппировать | Ctrl+Shift+G | Выделенная группа → отдельные элементы в слой |
|
||||||
|
|
||||||
|
- Слои (`data-layer`) защищены от разгруппировки
|
||||||
|
- Группа двигается/масштабируется как единый объект
|
||||||
|
- В панели слоёв группа показана как 📁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекстное меню (правая кнопка)
|
||||||
|
|
||||||
|
- Копировать (Ctrl+C)
|
||||||
|
- Вырезать (Ctrl+X)
|
||||||
|
- Вставить (Ctrl+V)
|
||||||
|
- Дублировать (Ctrl+D)
|
||||||
|
- Удалить (Del)
|
||||||
|
- На передний план
|
||||||
|
- На задний план
|
||||||
|
- Группировать (Ctrl+G)
|
||||||
|
- Разгруппировать (Ctrl+Shift+G)
|
||||||
|
|
||||||
|
Пункты автоматически деактивируются когда неприменимы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вкладки
|
||||||
|
|
||||||
|
- **Новый** — первая вкладка по умолчанию
|
||||||
|
- **+** — новая вкладка (Ctrl+T)
|
||||||
|
- **×** — закрыть вкладку (минимум 1)
|
||||||
|
- Каждая вкладка сохраняет: SVG-содержимое, размер холста, историю undo/redo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Импорт / Экспорт
|
||||||
|
|
||||||
|
| Действие | Клавиша | Формат |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| Импорт SVG | Ctrl+O | .svg файл |
|
||||||
|
| Экспорт SVG | Ctrl+S | .svg с CSS-классами (st0, st1...) |
|
||||||
|
| Экспорт PNG | — | .png 800×600 (или текущий размер холста) |
|
||||||
|
| Оптимизировать | — | Очистка через /api/svg-optimize |
|
||||||
|
| Drag & Drop | — | Перетаскивание .svg файла на холст |
|
||||||
|
|
||||||
|
### Экспорт SVG:
|
||||||
|
- Inline-атрибуты → CSS-классы в `<style>` блоке
|
||||||
|
- Одинаковые стили → один класс (`st0`, `st1`, ...)
|
||||||
|
- `data-name`, `data-layer` удаляются
|
||||||
|
- Positioning styles (left, top, width, height) удаляются
|
||||||
|
- Чистый SVG без мусора
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI Генератор иконок
|
||||||
|
|
||||||
|
- Кнопка ✨ в тулбаре → модальное окно
|
||||||
|
- **45 иконок**: home, user, search, heart, star, settings, mail, phone, lock, bell, clock, cloud, download, upload, trash, plus, minus, check, close, arrow-left/right/up/down, menu, globe, link, eye, sun, moon, code, terminal, database, wifi, bookmark, share, cart, file, folder, camera и др.
|
||||||
|
- **Поиск** по названию (EN и RU)
|
||||||
|
- **Стили**: Контур / Заливка / Дуотон
|
||||||
|
- **Размеры**: 24, 32, 48, 64, 128 px
|
||||||
|
- **Цвет**: произвольный
|
||||||
|
- Иконка добавляется в центр холста на активный слой
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Навигация по холсту
|
||||||
|
|
||||||
|
| Действие | Способ |
|
||||||
|
|----------|--------|
|
||||||
|
| Зум | Колесо мыши / кнопки +/- |
|
||||||
|
| Зум по размеру | Ctrl+0 |
|
||||||
|
| Панорамирование | Space + drag / средняя кнопка мыши |
|
||||||
|
| Координаты | Отображаются в статус-баре в реальном времени |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Горячие клавиши
|
||||||
|
|
||||||
|
| Клавиша | Действие |
|
||||||
|
|---------|----------|
|
||||||
|
| V | Выделение |
|
||||||
|
| R | Прямоугольник |
|
||||||
|
| O | Эллипс |
|
||||||
|
| L | Линия |
|
||||||
|
| P | Перо |
|
||||||
|
| T | Текст |
|
||||||
|
| Del / Backspace | Удалить выделенное |
|
||||||
|
| Ctrl+Z | Отменить |
|
||||||
|
| Ctrl+Shift+Z | Повторить |
|
||||||
|
| Ctrl+C | Копировать |
|
||||||
|
| Ctrl+V | Вставить |
|
||||||
|
| Ctrl+X | Вырезать |
|
||||||
|
| Ctrl+D | Дублировать |
|
||||||
|
| Ctrl+S | Экспорт SVG |
|
||||||
|
| Ctrl+O | Импорт SVG |
|
||||||
|
| Ctrl+N | Новый холст |
|
||||||
|
| Ctrl+T | Новая вкладка |
|
||||||
|
| Ctrl+G | Группировать |
|
||||||
|
| Ctrl+Shift+G | Разгруппировать |
|
||||||
|
| Ctrl+0 | Зум по размеру |
|
||||||
|
| ←↑→↓ | Перемещение на 1px |
|
||||||
|
| Shift+←↑→↓ | Перемещение на 10px |
|
||||||
|
| Escape | Снять выделение / отменить путь |
|
||||||
|
| Shift (при рисовании) | Квадрат / круг / snap линии к 45° |
|
||||||
|
| Space+drag | Панорамирование |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Undo / Redo
|
||||||
|
|
||||||
|
- До 30 шагов истории
|
||||||
|
- Сохраняется innerHTML SVG
|
||||||
|
- История привязана к вкладке
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Серверные API
|
||||||
|
|
||||||
|
### GET /svgeditor
|
||||||
|
Отдаёт `public/svgeditor.html`
|
||||||
|
|
||||||
|
### POST /api/svg-optimize
|
||||||
|
Очистка SVG: удаление комментариев, XML declaration, metadata, пустых групп, data-* атрибутов, округление чисел до 2 знаков, минификация пробелов.
|
||||||
|
- Вход: `{ svg: string }`
|
||||||
|
- Выход: `{ svg: string, saved: number }` (процент экономии)
|
||||||
|
|
||||||
|
### POST /api/svg-ai
|
||||||
|
Генерация SVG-иконки по ключевому слову.
|
||||||
|
- Вход: `{ keyword, style, size, color }`
|
||||||
|
- Выход: `{ svg: string, keyword: string }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Новый холст (Ctrl+N)
|
||||||
|
|
||||||
|
Модальное окно с настройками:
|
||||||
|
- Ширина / Высота (100-4000 px)
|
||||||
|
- Пресеты: 1920×1080, 1080×1080, 800×600, 512×512, 24×24, 48×48
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
- Единый HTML-файл (2099 строк), все CSS/JS inline
|
||||||
|
- SVG DOM manipulation (не Canvas)
|
||||||
|
- Шрифты: JetBrains Mono + Manrope (из /vendor/fonts.css)
|
||||||
|
- Нет внешних зависимостей кроме vendor-файлов
|
||||||
|
- IIFE-обёртка для изоляции переменных
|
||||||
|
- `requestAnimationFrame` для корректного позиционирования хэндлов
|
||||||
|
- `getScreenCTM()` / `getBoundingClientRect()` для визуальных координат с учётом transform
|
||||||
3419
package-lock.json
generated
Normal file
3419
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Executable file
29
package.json
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "tinypng",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "server.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"start": "node server.js"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"type": "commonjs",
|
||||||
|
"description": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@mozilla/readability": "^0.6.0",
|
||||||
|
"archiver": "^7.0.1",
|
||||||
|
"express": "^5.1.0",
|
||||||
|
"express-rate-limit": "^8.3.0",
|
||||||
|
"express-session": "^1.18.1",
|
||||||
|
"geoip-lite": "^1.4.10",
|
||||||
|
"iconv-lite": "^0.7.2",
|
||||||
|
"jsdom": "^28.1.0",
|
||||||
|
"multer": "^1.4.5-lts.2",
|
||||||
|
"openid-client": "^6.4.2",
|
||||||
|
"opentype.js": "^1.3.4",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"sharp": "^0.34.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
250
public/SVGEDITOR-FEATURES.md
Normal file
250
public/SVGEDITOR-FEATURES.md
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
# SVG Редактор — Полный функционал
|
||||||
|
|
||||||
|
**URL:** https://images.wadevelop.ru/svgeditor
|
||||||
|
**Файл:** /mnt/webdata/www/images.wadevelop.ru/public/svgeditor.html (2099 строк)
|
||||||
|
**API:** server.js — 3 эндпоинта
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Интерфейс (как фоторедактор /editor)
|
||||||
|
|
||||||
|
- **Тулбар** (44px) — горизонтальная панель инструментов сверху
|
||||||
|
- **Панель опций** (36px) — контекстная, показывает настройки активного инструмента
|
||||||
|
- **Вкладки** (30px) — несколько документов одновременно
|
||||||
|
- **Viewport** — тёмный фон (#2c2c30), белый SVG-холст по центру
|
||||||
|
- **Статус-бар** (28px) — размер холста, зум, координаты, кол-во элементов
|
||||||
|
- **Сайдбар** (56px) — навигация по сервисам (как во всех страницах)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Инструменты рисования
|
||||||
|
|
||||||
|
| Инструмент | Клавиша | Описание |
|
||||||
|
|------------|---------|----------|
|
||||||
|
| Выделение | V | Клик для выделения, drag для перемещения |
|
||||||
|
| Прямоугольник | R | Shift — квадрат |
|
||||||
|
| Эллипс | O | Shift — круг |
|
||||||
|
| Линия | L | Shift — snap к 45° |
|
||||||
|
| Перо | P | Клик по точкам, замыкание на первую точку (красный кружок), двойной клик — открытый путь |
|
||||||
|
| Текст | T | Клик → ввод текста, двойной клик — редактирование |
|
||||||
|
|
||||||
|
### После рисования:
|
||||||
|
- Автопереключение на Выделение (V)
|
||||||
|
- Элемент сразу выделен с хэндлами
|
||||||
|
- Панель свойств открывается автоматически
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Выделение и трансформация
|
||||||
|
|
||||||
|
- **Клик** на элемент → выделение + 8 хэндлов (для rect/ellipse/text/group)
|
||||||
|
- **Клик** на линию → 2 хэндла на концах
|
||||||
|
- **Drag за хэндл** → ресайз (для всех типов, включая группы и AI иконки)
|
||||||
|
- **Drag за тело** → перемещение
|
||||||
|
- **Стрелки** ←↑→↓ — перемещение на 1px, Shift+стрелки — на 10px
|
||||||
|
- **Маркиз** — клик на пустое место + drag → пунктирная рамка, выделяет верхний элемент в области
|
||||||
|
- **Proximity selection** — тонкие элементы (линии, outline иконки) ловятся в радиусе 12px
|
||||||
|
- **Уже выделенный элемент** — повторный клик внутри его bounds не сбрасывает выделение
|
||||||
|
- Хэндлы прячутся при `elementFromPoint` чтобы не блокировать клик
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Панель опций (контекстная)
|
||||||
|
|
||||||
|
### При рисовании (rect/circle/line/path):
|
||||||
|
- Заливка — цвет + вкл/выкл (✓/✗)
|
||||||
|
- Непрозрачность — слайдер 0-100%
|
||||||
|
- Обводка — цвет + вкл/выкл
|
||||||
|
- Толщина обводки — число
|
||||||
|
|
||||||
|
### При выделении элемента:
|
||||||
|
- Те же контролы с текущими значениями элемента
|
||||||
|
- Изменения применяются сразу
|
||||||
|
|
||||||
|
### При инструменте Текст:
|
||||||
|
- Размер шрифта
|
||||||
|
- Цвет
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Панель свойств (правая, автопоказ)
|
||||||
|
|
||||||
|
- Позиция: X, Y (CX, CY для эллипса, X1/Y1 для линии)
|
||||||
|
- Размер: W, H (RX, RY для эллипса)
|
||||||
|
- Стиль: Fill (цвет), Stroke (цвет), Stroke-width, Opacity
|
||||||
|
- Текст: Font-size
|
||||||
|
- Появляется автоматически при выделении
|
||||||
|
- Скрывается при снятии выделения
|
||||||
|
- Кнопка × для закрытия
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Система слоёв
|
||||||
|
|
||||||
|
- **Слой 1** создаётся автоматически (каждый слой = `<g data-layer="true">`)
|
||||||
|
- Все новые объекты создаются на **активном слое**
|
||||||
|
- Кнопка **«+ Новый слой»** внизу панели
|
||||||
|
- **Активный слой** подсвечен синим с полоской слева
|
||||||
|
- Клик по слою → делает его активным
|
||||||
|
- Каждый слой: видимость (👁), удаление (×)
|
||||||
|
- Объекты внутри слоя: клик для выделения, × для удаления
|
||||||
|
- Группы отмечены 📁 с количеством детей
|
||||||
|
- Минимум 1 слой (нельзя удалить последний)
|
||||||
|
- Очистка холста пересоздаёт Слой 1
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Группировка
|
||||||
|
|
||||||
|
| Действие | Клавиша | Описание |
|
||||||
|
|----------|---------|----------|
|
||||||
|
| Группировать | Ctrl+G | Все объекты активного слоя → `<g data-name="Группа">` |
|
||||||
|
| Разгруппировать | Ctrl+Shift+G | Выделенная группа → отдельные элементы в слой |
|
||||||
|
|
||||||
|
- Слои (`data-layer`) защищены от разгруппировки
|
||||||
|
- Группа двигается/масштабируется как единый объект
|
||||||
|
- В панели слоёв группа показана как 📁
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Контекстное меню (правая кнопка)
|
||||||
|
|
||||||
|
- Копировать (Ctrl+C)
|
||||||
|
- Вырезать (Ctrl+X)
|
||||||
|
- Вставить (Ctrl+V)
|
||||||
|
- Дублировать (Ctrl+D)
|
||||||
|
- Удалить (Del)
|
||||||
|
- На передний план
|
||||||
|
- На задний план
|
||||||
|
- Группировать (Ctrl+G)
|
||||||
|
- Разгруппировать (Ctrl+Shift+G)
|
||||||
|
|
||||||
|
Пункты автоматически деактивируются когда неприменимы.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Вкладки
|
||||||
|
|
||||||
|
- **Новый** — первая вкладка по умолчанию
|
||||||
|
- **+** — новая вкладка (Ctrl+T)
|
||||||
|
- **×** — закрыть вкладку (минимум 1)
|
||||||
|
- Каждая вкладка сохраняет: SVG-содержимое, размер холста, историю undo/redo
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Импорт / Экспорт
|
||||||
|
|
||||||
|
| Действие | Клавиша | Формат |
|
||||||
|
|----------|---------|--------|
|
||||||
|
| Импорт SVG | Ctrl+O | .svg файл |
|
||||||
|
| Экспорт SVG | Ctrl+S | .svg с CSS-классами (st0, st1...) |
|
||||||
|
| Экспорт PNG | — | .png 800×600 (или текущий размер холста) |
|
||||||
|
| Оптимизировать | — | Очистка через /api/svg-optimize |
|
||||||
|
| Drag & Drop | — | Перетаскивание .svg файла на холст |
|
||||||
|
|
||||||
|
### Экспорт SVG:
|
||||||
|
- Inline-атрибуты → CSS-классы в `<style>` блоке
|
||||||
|
- Одинаковые стили → один класс (`st0`, `st1`, ...)
|
||||||
|
- `data-name`, `data-layer` удаляются
|
||||||
|
- Positioning styles (left, top, width, height) удаляются
|
||||||
|
- Чистый SVG без мусора
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AI Генератор иконок
|
||||||
|
|
||||||
|
- Кнопка ✨ в тулбаре → модальное окно
|
||||||
|
- **45 иконок**: home, user, search, heart, star, settings, mail, phone, lock, bell, clock, cloud, download, upload, trash, plus, minus, check, close, arrow-left/right/up/down, menu, globe, link, eye, sun, moon, code, terminal, database, wifi, bookmark, share, cart, file, folder, camera и др.
|
||||||
|
- **Поиск** по названию (EN и RU)
|
||||||
|
- **Стили**: Контур / Заливка / Дуотон
|
||||||
|
- **Размеры**: 24, 32, 48, 64, 128 px
|
||||||
|
- **Цвет**: произвольный
|
||||||
|
- Иконка добавляется в центр холста на активный слой
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Навигация по холсту
|
||||||
|
|
||||||
|
| Действие | Способ |
|
||||||
|
|----------|--------|
|
||||||
|
| Зум | Колесо мыши / кнопки +/- |
|
||||||
|
| Зум по размеру | Ctrl+0 |
|
||||||
|
| Панорамирование | Space + drag / средняя кнопка мыши |
|
||||||
|
| Координаты | Отображаются в статус-баре в реальном времени |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Горячие клавиши
|
||||||
|
|
||||||
|
| Клавиша | Действие |
|
||||||
|
|---------|----------|
|
||||||
|
| V | Выделение |
|
||||||
|
| R | Прямоугольник |
|
||||||
|
| O | Эллипс |
|
||||||
|
| L | Линия |
|
||||||
|
| P | Перо |
|
||||||
|
| T | Текст |
|
||||||
|
| Del / Backspace | Удалить выделенное |
|
||||||
|
| Ctrl+Z | Отменить |
|
||||||
|
| Ctrl+Shift+Z | Повторить |
|
||||||
|
| Ctrl+C | Копировать |
|
||||||
|
| Ctrl+V | Вставить |
|
||||||
|
| Ctrl+X | Вырезать |
|
||||||
|
| Ctrl+D | Дублировать |
|
||||||
|
| Ctrl+S | Экспорт SVG |
|
||||||
|
| Ctrl+O | Импорт SVG |
|
||||||
|
| Ctrl+N | Новый холст |
|
||||||
|
| Ctrl+T | Новая вкладка |
|
||||||
|
| Ctrl+G | Группировать |
|
||||||
|
| Ctrl+Shift+G | Разгруппировать |
|
||||||
|
| Ctrl+0 | Зум по размеру |
|
||||||
|
| ←↑→↓ | Перемещение на 1px |
|
||||||
|
| Shift+←↑→↓ | Перемещение на 10px |
|
||||||
|
| Escape | Снять выделение / отменить путь |
|
||||||
|
| Shift (при рисовании) | Квадрат / круг / snap линии к 45° |
|
||||||
|
| Space+drag | Панорамирование |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Undo / Redo
|
||||||
|
|
||||||
|
- До 30 шагов истории
|
||||||
|
- Сохраняется innerHTML SVG
|
||||||
|
- История привязана к вкладке
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Серверные API
|
||||||
|
|
||||||
|
### GET /svgeditor
|
||||||
|
Отдаёт `public/svgeditor.html`
|
||||||
|
|
||||||
|
### POST /api/svg-optimize
|
||||||
|
Очистка SVG: удаление комментариев, XML declaration, metadata, пустых групп, data-* атрибутов, округление чисел до 2 знаков, минификация пробелов.
|
||||||
|
- Вход: `{ svg: string }`
|
||||||
|
- Выход: `{ svg: string, saved: number }` (процент экономии)
|
||||||
|
|
||||||
|
### POST /api/svg-ai
|
||||||
|
Генерация SVG-иконки по ключевому слову.
|
||||||
|
- Вход: `{ keyword, style, size, color }`
|
||||||
|
- Выход: `{ svg: string, keyword: string }`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Новый холст (Ctrl+N)
|
||||||
|
|
||||||
|
Модальное окно с настройками:
|
||||||
|
- Ширина / Высота (100-4000 px)
|
||||||
|
- Пресеты: 1920×1080, 1080×1080, 800×600, 512×512, 24×24, 48×48
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Технические детали
|
||||||
|
|
||||||
|
- Единый HTML-файл (2099 строк), все CSS/JS inline
|
||||||
|
- SVG DOM manipulation (не Canvas)
|
||||||
|
- Шрифты: JetBrains Mono + Manrope (из /vendor/fonts.css)
|
||||||
|
- Нет внешних зависимостей кроме vendor-файлов
|
||||||
|
- IIFE-обёртка для изоляции переменных
|
||||||
|
- `requestAnimationFrame` для корректного позиционирования хэндлов
|
||||||
|
- `getScreenCTM()` / `getBoundingClientRect()` для визуальных координат с учётом transform
|
||||||
515
public/compress.html
Normal file
515
public/compress.html
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Сжать картинки</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
|
||||||
|
/* Mobile bottom bar */
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme variables */
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f7;
|
||||||
|
--bg-grad1: rgba(0, 67, 184, 0.04);
|
||||||
|
--bg-grad2: rgba(0, 50, 140, 0.03);
|
||||||
|
--surface-800: rgba(255,255,255,0.85);
|
||||||
|
--surface-700: #f0f0f3;
|
||||||
|
--surface-600: #dde0e6;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555;
|
||||||
|
--text-muted: #888;
|
||||||
|
--bar-bg: #e0e0e0;
|
||||||
|
--select-bg: #f0f0f3;
|
||||||
|
--select-color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--bg: #060c18;
|
||||||
|
--bg-grad1: rgba(0, 84, 230, 0.03);
|
||||||
|
--bg-grad2: rgba(0, 67, 184, 0.02);
|
||||||
|
--surface-800: rgba(9,16,32,0.8);
|
||||||
|
--surface-700: #0d1828;
|
||||||
|
--surface-600: #162040;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #c0c0c0;
|
||||||
|
--text-muted: #666;
|
||||||
|
--bar-bg: #0d1828;
|
||||||
|
--select-bg: #091020;
|
||||||
|
--select-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.dark .noise::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.dropzone-active { border-color: #0054e6 !important; background: rgba(0, 84, 230, 0.05) !important; }
|
||||||
|
.dropzone-active .drop-icon { transform: scale(1.1) translateY(-4px); }
|
||||||
|
.drop-icon { transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); }
|
||||||
|
.savings-bar { height: 6px; border-radius: 3px; background: var(--bar-bg); overflow: hidden; }
|
||||||
|
.savings-bar-fill { height: 100%; border-radius: 3px; transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); }
|
||||||
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
||||||
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
||||||
|
@keyframes pulse-border { 0%, 100% { border-color: rgba(0, 84, 230, 0.2); } 50% { border-color: rgba(0, 84, 230, 0.5); } }
|
||||||
|
.processing { animation: pulse-border 1.5s ease-in-out infinite; }
|
||||||
|
.preview-grid img { transition: transform 0.2s ease; }
|
||||||
|
.preview-grid img:hover { transform: scale(1.05); }
|
||||||
|
progress { appearance: none; height: 4px; border-radius: 2px; overflow: hidden; }
|
||||||
|
progress::-webkit-progress-bar { background: var(--bar-bg); border-radius: 2px; }
|
||||||
|
progress::-webkit-progress-value { background: linear-gradient(90deg, #0043b8, #0054e6); border-radius: 2px; transition: width 0.2s; }
|
||||||
|
select, select option { background: var(--select-bg); color: var(--select-color); }
|
||||||
|
|
||||||
|
/* Theme toggle */
|
||||||
|
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.dark .theme-toggle { background: #162040; }
|
||||||
|
.theme-toggle { background: #dde0e6; }
|
||||||
|
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
|
||||||
|
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link active" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-2xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
JPEG · PNG · WebP
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Сжать <span class="text-accent">картинки</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">до 50 файлов · до 20 МБ каждый</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<!-- Dropzone -->
|
||||||
|
<div id="dropzone" class="relative border-2 border-dashed dark:border-surface-600 border-gray-300 rounded-xl p-8 sm:p-10 text-center cursor-pointer dark:hover:border-gray-500 hover:border-gray-400 transition-all duration-300 group">
|
||||||
|
<div class="drop-icon mb-3">
|
||||||
|
<svg class="mx-auto w-10 h-10 dark:text-gray-500 text-gray-400 group-hover:text-accent transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm dark:text-gray-400 text-gray-500 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors">
|
||||||
|
Перетащите файлы сюда или <span class="text-accent font-medium">выберите</span>
|
||||||
|
</p>
|
||||||
|
<input type="file" multiple accept="image/jpeg,image/png,image/webp" id="fileInput" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div id="preview" class="preview-grid grid grid-cols-3 sm:grid-cols-4 gap-2 mt-4 hidden"></div>
|
||||||
|
|
||||||
|
<!-- File count badge -->
|
||||||
|
<div id="fileCount" class="hidden mt-3 text-center">
|
||||||
|
<span class="inline-flex items-center gap-1.5 text-xs font-mono dark:text-gray-400 text-gray-500 dark:bg-surface-700 bg-gray-100 px-3 py-1 rounded-full">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z" />
|
||||||
|
</svg>
|
||||||
|
<span id="fileCountText">0 файлов</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls -->
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-5">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Формат</label>
|
||||||
|
<select id="format" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
|
||||||
|
<option value="original">Оригинал</option>
|
||||||
|
<option value="webp">WebP</option>
|
||||||
|
<option value="jpeg">JPEG</option>
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Размер</label>
|
||||||
|
<select id="resize" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
|
||||||
|
<option value="0">Не изменять</option>
|
||||||
|
<option value="4600">4600px</option>
|
||||||
|
<option value="3600">3600px</option>
|
||||||
|
<option value="2600">2600px</option>
|
||||||
|
<option value="1600">1600px</option>
|
||||||
|
<option value="1200">1200px</option>
|
||||||
|
<option value="800">800px</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress -->
|
||||||
|
<progress id="progressBar" class="w-full mt-4 hidden" value="0" max="100"></progress>
|
||||||
|
|
||||||
|
<!-- Compress button -->
|
||||||
|
<button id="compressBtn" disabled
|
||||||
|
class="mt-5 w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent/10 text-accent/40 border border-accent/10 cursor-not-allowed
|
||||||
|
enabled:bg-accent enabled:text-white enabled:border-accent enabled:cursor-pointer
|
||||||
|
enabled:hover:bg-accent-bright enabled:hover:shadow-lg enabled:hover:shadow-accent/20
|
||||||
|
enabled:active:scale-[0.98]">
|
||||||
|
Сжать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results -->
|
||||||
|
<div id="results" class="hidden mt-5 fade-up">
|
||||||
|
<!-- Stats card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl overflow-hidden shadow-sm dark:shadow-none">
|
||||||
|
<!-- Summary header -->
|
||||||
|
<div id="summary" class="flex items-center justify-between px-5 sm:px-7 py-4 border-b dark:border-surface-600 border-gray-200 dark:bg-surface-700/30 bg-gray-50/50">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="w-8 h-8 rounded-lg bg-accent/10 flex items-center justify-center">
|
||||||
|
<svg class="w-4 h-4 text-accent" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold dark:text-white text-gray-900" id="summaryTitle">Готово</div>
|
||||||
|
<div class="text-xs dark:text-gray-500 text-gray-400 font-mono" id="summaryDetail"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-right">
|
||||||
|
<div class="text-lg font-bold font-mono" id="summaryPercent"></div>
|
||||||
|
<div class="text-xs dark:text-gray-500 text-gray-400">экономия</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats table -->
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider border-b dark:border-surface-600 border-gray-200">
|
||||||
|
<th class="text-left px-5 py-3">Файл</th>
|
||||||
|
<th class="text-right px-3 py-3">Было</th>
|
||||||
|
<th class="text-right px-3 py-3">Стало</th>
|
||||||
|
<th class="text-right px-5 py-3 w-32">Экономия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="statsBody"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Download button -->
|
||||||
|
<a id="downloadBtn" href="#" download
|
||||||
|
class="mt-4 flex items-center justify-center gap-2 w-full py-3.5 rounded-xl font-semibold text-sm tracking-wide
|
||||||
|
bg-accent text-white hover:bg-accent-bright transition-all duration-300
|
||||||
|
hover:shadow-lg hover:shadow-accent/20 active:scale-[0.98]">
|
||||||
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||||
|
</svg>
|
||||||
|
Скачать архив
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div id="errorBlock" class="hidden mt-5 bg-danger/10 border border-danger/20 rounded-xl px-5 py-4 text-sm text-danger font-medium fade-up"></div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme toggle
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const dropzone = $('dropzone');
|
||||||
|
const fileInput = $('fileInput');
|
||||||
|
const progressBar = $('progressBar');
|
||||||
|
const compressBtn = $('compressBtn');
|
||||||
|
const results = $('results');
|
||||||
|
const errorBlock = $('errorBlock');
|
||||||
|
const preview = $('preview');
|
||||||
|
|
||||||
|
let selectedFiles = null;
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (bytes < 1024) return bytes + ' Б';
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' КБ';
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(2) + ' МБ';
|
||||||
|
}
|
||||||
|
|
||||||
|
function pluralFiles(n) {
|
||||||
|
const mod = n % 10;
|
||||||
|
const mod100 = n % 100;
|
||||||
|
if (mod === 1 && mod100 !== 11) return n + ' файл';
|
||||||
|
if (mod >= 2 && mod <= 4 && (mod100 < 12 || mod100 > 14)) return n + ' файла';
|
||||||
|
return n + ' файлов';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drag & drop
|
||||||
|
dropzone.addEventListener('click', () => fileInput.click());
|
||||||
|
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('dropzone-active'); });
|
||||||
|
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('dropzone-active'));
|
||||||
|
dropzone.addEventListener('drop', e => { e.preventDefault(); dropzone.classList.remove('dropzone-active'); processFiles(e.dataTransfer.files); });
|
||||||
|
fileInput.addEventListener('change', e => processFiles(e.target.files));
|
||||||
|
|
||||||
|
function processFiles(files) {
|
||||||
|
results.classList.add('hidden');
|
||||||
|
errorBlock.classList.add('hidden');
|
||||||
|
preview.innerHTML = '';
|
||||||
|
preview.classList.add('hidden');
|
||||||
|
compressBtn.disabled = true;
|
||||||
|
|
||||||
|
if (!files.length) return;
|
||||||
|
|
||||||
|
const validTypes = ['image/jpeg', 'image/png', 'image/webp'];
|
||||||
|
const valid = Array.from(files).filter(f => validTypes.includes(f.type) && f.size <= 20 * 1024 * 1024);
|
||||||
|
|
||||||
|
if (!valid.length) {
|
||||||
|
showError('Нет подходящих файлов. Допустимы JPEG, PNG, WebP до 20 МБ.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedFiles = valid;
|
||||||
|
$('fileCount').classList.remove('hidden');
|
||||||
|
$('fileCountText').textContent = pluralFiles(valid.length);
|
||||||
|
|
||||||
|
// Show previews (max 8)
|
||||||
|
const shown = valid.slice(0, 8);
|
||||||
|
shown.forEach(file => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = e.target.result;
|
||||||
|
img.className = 'w-full aspect-square object-cover rounded-lg border dark:border-surface-600 border-gray-200';
|
||||||
|
preview.appendChild(img);
|
||||||
|
preview.classList.remove('hidden');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
if (valid.length > 8) {
|
||||||
|
const more = document.createElement('div');
|
||||||
|
more.className = 'w-full aspect-square rounded-lg border dark:border-surface-600 border-gray-200 dark:bg-surface-700 bg-gray-100 flex items-center justify-center text-xs dark:text-gray-500 text-gray-400 font-mono';
|
||||||
|
more.textContent = '+' + (valid.length - 8);
|
||||||
|
preview.appendChild(more);
|
||||||
|
preview.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
compressBtn.disabled = false;
|
||||||
|
compressBtn.textContent = 'Сжать';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(msg) {
|
||||||
|
errorBlock.textContent = msg;
|
||||||
|
errorBlock.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
compressBtn.addEventListener('click', () => {
|
||||||
|
if (!selectedFiles || !selectedFiles.length) return;
|
||||||
|
|
||||||
|
compressBtn.disabled = true;
|
||||||
|
compressBtn.textContent = 'Сжимаю...';
|
||||||
|
results.classList.add('hidden');
|
||||||
|
errorBlock.classList.add('hidden');
|
||||||
|
progressBar.hidden = false;
|
||||||
|
progressBar.value = 0;
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
selectedFiles.forEach(f => formData.append('images', f));
|
||||||
|
formData.append('resize', $('resize').value);
|
||||||
|
formData.append('format', $('format').value);
|
||||||
|
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('POST', '/compress');
|
||||||
|
xhr.responseType = 'json';
|
||||||
|
|
||||||
|
xhr.upload.onprogress = e => {
|
||||||
|
if (e.lengthComputable) progressBar.value = (e.loaded / e.total) * 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
progressBar.hidden = true;
|
||||||
|
compressBtn.textContent = 'Сжать';
|
||||||
|
compressBtn.disabled = false;
|
||||||
|
|
||||||
|
if (xhr.status === 200 && xhr.response && xhr.response.success) {
|
||||||
|
renderResults(xhr.response);
|
||||||
|
} else if (xhr.status === 429) {
|
||||||
|
showError('Слишком много запросов. Подождите минуту.');
|
||||||
|
} else {
|
||||||
|
const msg = xhr.response && xhr.response.error ? xhr.response.error : 'Ошибка при сжатии.';
|
||||||
|
showError(msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
progressBar.hidden = true;
|
||||||
|
compressBtn.textContent = 'Сжать';
|
||||||
|
compressBtn.disabled = false;
|
||||||
|
showError('Ошибка сети. Повторите позже.');
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.send(formData);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderResults(data) {
|
||||||
|
const { stats, downloadUrl } = data;
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
const totalOrig = stats.reduce((s, f) => s + f.originalSize, 0);
|
||||||
|
const totalComp = stats.reduce((s, f) => s + f.compressedSize, 0);
|
||||||
|
const totalSavings = totalOrig > 0 ? Math.round((1 - totalComp / totalOrig) * 100) : 0;
|
||||||
|
|
||||||
|
$('summaryDetail').textContent = `${formatSize(totalOrig)} → ${formatSize(totalComp)}`;
|
||||||
|
const pctEl = $('summaryPercent');
|
||||||
|
pctEl.textContent = (totalSavings > 0 ? '-' : '') + totalSavings + '%';
|
||||||
|
pctEl.className = 'text-lg font-bold font-mono ' + savingsColor(totalSavings);
|
||||||
|
|
||||||
|
// Table
|
||||||
|
const tbody = $('statsBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
stats.forEach((f, i) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.className = 'border-b dark:border-surface-600/50 border-gray-100 last:border-0' + (i % 2 === 0 ? ' dark:bg-surface-700/20 bg-gray-50/50' : '');
|
||||||
|
const sColor = savingsColor(f.savings);
|
||||||
|
row.innerHTML = `
|
||||||
|
<td class="px-5 py-2.5 font-mono text-xs dark:text-gray-300 text-gray-600 truncate max-w-[180px]" title="${f.outputFilename}">${f.outputFilename}</td>
|
||||||
|
<td class="text-right px-3 py-2.5 font-mono text-xs dark:text-gray-500 text-gray-400">${formatSize(f.originalSize)}</td>
|
||||||
|
<td class="text-right px-3 py-2.5 font-mono text-xs dark:text-gray-300 text-gray-600">${formatSize(f.compressedSize)}</td>
|
||||||
|
<td class="px-5 py-2.5">
|
||||||
|
<div class="flex items-center gap-2 justify-end">
|
||||||
|
<div class="savings-bar flex-1 max-w-[60px]">
|
||||||
|
<div class="savings-bar-fill ${barColor(f.savings)}" style="width: ${Math.max(0, Math.min(100, f.savings))}%"></div>
|
||||||
|
</div>
|
||||||
|
<span class="font-mono text-xs font-medium ${sColor} w-10 text-right">${f.savings > 0 ? '-' : ''}${f.savings}%</span>
|
||||||
|
</div>
|
||||||
|
</td>`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download
|
||||||
|
$('downloadBtn').href = downloadUrl;
|
||||||
|
|
||||||
|
results.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function savingsColor(pct) {
|
||||||
|
if (pct >= 40) return 'text-accent';
|
||||||
|
if (pct >= 15) return 'text-accent/70';
|
||||||
|
if (pct >= 0) return 'text-warn';
|
||||||
|
return 'text-danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
function barColor(pct) {
|
||||||
|
if (pct >= 40) return 'bg-accent';
|
||||||
|
if (pct >= 15) return 'bg-accent/60';
|
||||||
|
if (pct >= 0) return 'bg-warn';
|
||||||
|
return 'bg-danger';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
284
public/converter.html
Normal file
284
public/converter.html
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Converter — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script src="/vendor/marked.min.js"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
:root{--bg:#f5f5f7;--bg-grad1:rgba(0,67,184,.04);--bg-grad2:rgba(0,150,60,.03);--surface-800:rgba(255,255,255,0.85);--surface-700:#f0f0f3;--surface-600:#dde0e6;--text-primary:#1a1a2e;--text-secondary:#555;--text-muted:#888;--select-bg:#f0f0f3;--select-color:#1a1a2e}
|
||||||
|
.dark{--bg:#060c18;--bg-grad1:rgba(0,84,230,.03);--bg-grad2:rgba(0,67,184,.02);--surface-800:rgba(9,16,32,0.8);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--select-bg:#091020;--select-color:#e0e0e0}
|
||||||
|
body{background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,var(--bg-grad1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,var(--bg-grad2) 0%,transparent 50%);min-height:100vh;transition:background .3s}
|
||||||
|
.dark .noise::before{content:'';position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
|
||||||
|
.theme-toggle{position:relative;width:44px;height:24px;border-radius:12px;cursor:pointer;transition:background .3s}
|
||||||
|
.dark .theme-toggle{background:#162040}.theme-toggle{background:#dde0e6}
|
||||||
|
.theme-toggle-knob{position:absolute;top:2px;width:20px;height:20px;border-radius:50%;transition:left .3s,background .3s;display:flex;align-items:center;justify-content:center}
|
||||||
|
.dark .theme-toggle-knob{left:22px;background:#0d1828}.theme-toggle-knob{left:2px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
||||||
|
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.fade-up{animation:fadeUp .4s ease-out forwards}.fade-up-delay{animation:fadeUp .4s ease-out .1s forwards;opacity:0}
|
||||||
|
.tool-btn{transition:all .2s}.tool-btn.active{color:#0054e6;background:rgba(0,84,230,.08);border-color:rgba(0,84,230,.3)}
|
||||||
|
textarea{font-family:'JetBrains Mono',monospace;font-size:13px;resize:vertical}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg></a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg></a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg></a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg></a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg></a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg></a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg></a>
|
||||||
|
<a href="/converter" class="sidebar-link active" title="Converter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg></a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg></a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg></a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-3xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
HTML ↔ Markdown
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Format <span class="text-accent">Converter</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">конвертация между HTML и Markdown</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tool Toggle -->
|
||||||
|
<div class="flex gap-2 mb-5 fade-up-delay">
|
||||||
|
<button class="tool-btn active flex-1 px-4 py-3 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="htmlToMd">
|
||||||
|
<div class="text-sm font-semibold">HTML → Markdown</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn flex-1 px-4 py-3 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="mdToHtml">
|
||||||
|
<div class="text-sm font-semibold">Markdown → HTML</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Input</label>
|
||||||
|
<textarea id="inputArea" rows="10" placeholder="Вставьте HTML..."
|
||||||
|
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none focus:border-accent/50 transition-colors"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="processBtn"
|
||||||
|
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent text-white border border-accent cursor-pointer
|
||||||
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20 active:scale-[0.98]">
|
||||||
|
Конвертировать
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="outputBlock" class="mt-5 hidden">
|
||||||
|
<div class="flex items-center justify-between mb-1.5">
|
||||||
|
<label class="text-xs font-medium dark:text-gray-500 text-gray-400 font-mono uppercase tracking-wider">Output</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span id="stats" class="text-[10px] font-mono dark:text-gray-600 text-gray-400"></span>
|
||||||
|
<button id="copyBtn" class="px-3 py-1 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">Копировать</button>
|
||||||
|
<button id="clearBtn" class="px-3 py-1 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-400 text-gray-500 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">Очистить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="outputArea" rows="12" readonly
|
||||||
|
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none transition-colors"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
let activeTool = 'htmlToMd';
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
activeTool = btn.dataset.tool;
|
||||||
|
$('inputArea').placeholder = activeTool === 'htmlToMd' ? 'Вставьте HTML...' : 'Вставьте Markdown...';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('processBtn').addEventListener('click', () => {
|
||||||
|
const input = $('inputArea').value;
|
||||||
|
if (!input.trim()) return;
|
||||||
|
let output = '';
|
||||||
|
try {
|
||||||
|
output = activeTool === 'htmlToMd' ? htmlToMarkdown(input) : mdToHtml(input);
|
||||||
|
} catch (e) { output = 'Error: ' + e.message; }
|
||||||
|
$('outputArea').value = output;
|
||||||
|
$('outputBlock').classList.remove('hidden');
|
||||||
|
const diff = output.length - input.length;
|
||||||
|
$('stats').textContent = `${input.length} \u2192 ${output.length} (${diff > 0 ? '+' : ''}${diff})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyBtn').addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText($('outputArea').value).then(() => {
|
||||||
|
const b = $('copyBtn'); const o = b.textContent;
|
||||||
|
b.textContent = '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e!'; setTimeout(() => { b.textContent = o; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('clearBtn').addEventListener('click', () => {
|
||||||
|
$('inputArea').value = ''; $('outputArea').value = ''; $('outputBlock').classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== HTML → Markdown ====================
|
||||||
|
function htmlToMarkdown(html) {
|
||||||
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
return nodesToMd(doc.body.childNodes).replace(/\n{3,}/g, '\n\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodesToMd(nodes) {
|
||||||
|
let md = '';
|
||||||
|
for (const node of nodes) {
|
||||||
|
if (node.nodeType === 3) md += node.textContent.replace(/\s+/g, ' ');
|
||||||
|
else if (node.nodeType === 1) md += elemToMd(node);
|
||||||
|
}
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
|
||||||
|
function elemToMd(el) {
|
||||||
|
const tag = el.tagName.toLowerCase();
|
||||||
|
const inner = () => nodesToMd(el.childNodes);
|
||||||
|
switch (tag) {
|
||||||
|
case 'h1': return '\n\n# ' + inner().trim() + '\n\n';
|
||||||
|
case 'h2': return '\n\n## ' + inner().trim() + '\n\n';
|
||||||
|
case 'h3': return '\n\n### ' + inner().trim() + '\n\n';
|
||||||
|
case 'h4': return '\n\n#### ' + inner().trim() + '\n\n';
|
||||||
|
case 'h5': return '\n\n##### ' + inner().trim() + '\n\n';
|
||||||
|
case 'h6': return '\n\n###### ' + inner().trim() + '\n\n';
|
||||||
|
case 'p': return '\n\n' + inner().trim() + '\n\n';
|
||||||
|
case 'br': return '\n';
|
||||||
|
case 'hr': return '\n\n---\n\n';
|
||||||
|
case 'strong': case 'b': return '**' + inner().trim() + '**';
|
||||||
|
case 'em': case 'i': return '*' + inner().trim() + '*';
|
||||||
|
case 'del': case 's': return '~~' + inner().trim() + '~~';
|
||||||
|
case 'a': return '[' + inner().trim() + '](' + (el.getAttribute('href') || '') + ')';
|
||||||
|
case 'img': return ' || '') + ')';
|
||||||
|
case 'code':
|
||||||
|
if (el.parentElement && el.parentElement.tagName.toLowerCase() === 'pre') return el.textContent;
|
||||||
|
return '`' + el.textContent + '`';
|
||||||
|
case 'pre': {
|
||||||
|
const code = el.querySelector('code');
|
||||||
|
const lang = code ? (code.className.match(/language-(\w+)/) || [])[1] || '' : '';
|
||||||
|
return '\n\n```' + lang + '\n' + (code ? code.textContent : el.textContent) + '\n```\n\n';
|
||||||
|
}
|
||||||
|
case 'blockquote': return '\n\n' + inner().trim().split('\n').map(l => '> ' + l).join('\n') + '\n\n';
|
||||||
|
case 'ul': {
|
||||||
|
let r = '\n';
|
||||||
|
for (const c of el.children) {
|
||||||
|
if (c.tagName && c.tagName.toLowerCase() === 'li') r += '- ' + nodesToMd(c.childNodes).trim() + '\n';
|
||||||
|
}
|
||||||
|
return r + '\n';
|
||||||
|
}
|
||||||
|
case 'ol': {
|
||||||
|
let r = '\n', n = 1;
|
||||||
|
for (const c of el.children) {
|
||||||
|
if (c.tagName && c.tagName.toLowerCase() === 'li') { r += n + '. ' + nodesToMd(c.childNodes).trim() + '\n'; n++; }
|
||||||
|
}
|
||||||
|
return r + '\n';
|
||||||
|
}
|
||||||
|
case 'table': return '\n\n' + tableToMd(el) + '\n\n';
|
||||||
|
case 'script': case 'style': return '';
|
||||||
|
default: return inner();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tableToMd(table) {
|
||||||
|
const rows = [];
|
||||||
|
table.querySelectorAll('tr').forEach(tr => {
|
||||||
|
const cells = [];
|
||||||
|
tr.querySelectorAll('th, td').forEach(c => cells.push(c.textContent.trim()));
|
||||||
|
if (cells.length) rows.push(cells);
|
||||||
|
});
|
||||||
|
if (!rows.length) return '';
|
||||||
|
const cols = Math.max(...rows.map(r => r.length));
|
||||||
|
rows.forEach(r => { while (r.length < cols) r.push(''); });
|
||||||
|
const w = Array(cols).fill(3);
|
||||||
|
for (let c = 0; c < cols; c++) for (const r of rows) w[c] = Math.max(w[c], r[c].length);
|
||||||
|
let md = '| ' + rows[0].map((c, i) => c.padEnd(w[i])).join(' | ') + ' |\n';
|
||||||
|
md += '| ' + w.map(v => '-'.repeat(v)).join(' | ') + ' |\n';
|
||||||
|
for (let r = 1; r < rows.length; r++) md += '| ' + rows[r].map((c, i) => c.padEnd(w[i])).join(' | ') + ' |\n';
|
||||||
|
return md.trimEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Markdown → HTML ====================
|
||||||
|
function mdToHtml(md) {
|
||||||
|
return marked.parse(md);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
170
public/editor.css
Normal file
170
public/editor.css
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
*{margin:0;padding:0;box-sizing:border-box}
|
||||||
|
:root{--bg:#060c18;--surface-800:rgba(9,16,32,0.95);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--accent:#0054e6;--accent-dim:#0043b8;--accent-bg:rgba(0,84,230,0.08);--danger:#ff5252;--warn:#ffd600}
|
||||||
|
body{font-family:'Manrope',sans-serif;font-size:13px;background:var(--bg);color:var(--text-secondary);overflow:hidden;height:100vh;width:100vw;user-select:none}
|
||||||
|
input,select,button,textarea{font-family:inherit;font-size:inherit}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:100;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px)}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1;overflow-y:auto;scrollbar-width:none}
|
||||||
|
.sidebar-nav::-webkit-scrollbar{display:none}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative;flex-shrink:0}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:var(--accent);background:var(--accent-bg)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:var(--accent)}
|
||||||
|
|
||||||
|
/* Main layout */
|
||||||
|
.editor-wrap{margin-left:56px;height:100vh;display:flex;flex-direction:column}
|
||||||
|
|
||||||
|
/* Toolbar */
|
||||||
|
.toolbar{height:44px;background:var(--surface-800);border-bottom:1px solid var(--surface-600);display:flex;align-items:center;padding:0 12px;gap:4px;flex-shrink:0;z-index:20}
|
||||||
|
.toolbar-group{display:flex;align-items:center;gap:2px}
|
||||||
|
.toolbar-sep{width:1px;height:24px;background:var(--surface-600);margin:0 6px}
|
||||||
|
.tb{width:34px;height:34px;border:none;border-radius:8px;background:transparent;color:var(--text-primary);cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .15s;position:relative}
|
||||||
|
.tb:hover{color:#fff;background:rgba(255,255,255,.08)}
|
||||||
|
.tb.active{color:var(--accent);background:var(--accent-bg)}
|
||||||
|
.tb:disabled{opacity:.3;cursor:default;pointer-events:none}
|
||||||
|
.tb svg{width:18px;height:18px}
|
||||||
|
.tb-label{font-size:11px;font-weight:500;padding:0 8px;white-space:nowrap}
|
||||||
|
.toolbar-right{margin-left:auto;display:flex;align-items:center;gap:4px}
|
||||||
|
|
||||||
|
/* Tool Options Bar */
|
||||||
|
.tool-options{height:36px;background:var(--surface-700);border-bottom:1px solid var(--surface-600);display:none;align-items:center;padding:0 12px;gap:8px;flex-shrink:0;z-index:19}
|
||||||
|
.tool-options.visible{display:flex}
|
||||||
|
.tool-options label{font-size:11px;color:var(--text-muted);white-space:nowrap}
|
||||||
|
.tool-options input[type="range"]{width:80px;accent-color:var(--accent);height:4px;cursor:pointer}
|
||||||
|
.tool-options input[type="number"]{width:60px;background:var(--surface-600);border:1px solid transparent;color:var(--text-secondary);padding:3px 6px;border-radius:5px;text-align:center;font-family:'JetBrains Mono',monospace;font-size:11px}
|
||||||
|
.tool-options input[type="number"]:focus{border-color:var(--accent);outline:none}
|
||||||
|
.tool-options select{background:var(--surface-600);border:1px solid transparent;color:var(--text-secondary);padding:3px 8px;border-radius:5px;cursor:pointer;font-size:11px}
|
||||||
|
.tool-options select:focus{border-color:var(--accent);outline:none}
|
||||||
|
.tool-options .opt-sep{width:1px;height:20px;background:var(--surface-600)}
|
||||||
|
.opt-btn{padding:3px 10px;border:1px solid var(--surface-600);border-radius:5px;background:transparent;color:var(--text-secondary);cursor:pointer;font-size:11px;transition:all .15s}
|
||||||
|
.opt-btn:hover{border-color:var(--text-muted)}
|
||||||
|
.opt-btn.primary{background:var(--accent);color:#000;border-color:var(--accent);font-weight:600}
|
||||||
|
.opt-btn.primary:hover{background:var(--accent-dim)}
|
||||||
|
.opt-toggle{padding:3px 8px;border:1px solid var(--surface-600);border-radius:5px;background:transparent;color:var(--text-muted);cursor:pointer;font-size:11px;transition:all .15s}
|
||||||
|
.opt-toggle.on{color:var(--accent);border-color:var(--accent);background:var(--accent-bg)}
|
||||||
|
|
||||||
|
/* Viewport */
|
||||||
|
.viewport{flex:1;overflow:hidden;position:relative;background:#2c2c30}
|
||||||
|
.viewport-checker{display:none}
|
||||||
|
.canvas-container{position:absolute;transform-origin:0 0;cursor:crosshair;background:#2c2c30}
|
||||||
|
.canvas-container.has-transparency{background-image:linear-gradient(45deg,#444 25%,transparent 25%),linear-gradient(-45deg,#444 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#444 75%),linear-gradient(-45deg,transparent 75%,#444 75%);background-size:16px 16px;background-position:0 0,0 8px,8px -8px,-8px 0;background-color:#333}
|
||||||
|
.canvas-container canvas{position:absolute;top:0;left:0}
|
||||||
|
|
||||||
|
/* Drop zone */
|
||||||
|
.dropzone{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:10;cursor:pointer;transition:background .2s}
|
||||||
|
.dropzone:hover{background:rgba(0,84,230,0.03)}
|
||||||
|
.dropzone.drag-over{background:rgba(0,84,230,0.06);border:2px dashed var(--accent)}
|
||||||
|
.dropzone-icon{width:64px;height:64px;border-radius:16px;background:var(--surface-700);border:2px dashed var(--surface-600);display:flex;align-items:center;justify-content:center;margin-bottom:16px;transition:all .3s}
|
||||||
|
.dropzone:hover .dropzone-icon{border-color:var(--accent);background:var(--accent-bg)}
|
||||||
|
.dropzone-icon svg{color:var(--text-muted);width:28px;height:28px}
|
||||||
|
.dropzone-text{font-size:15px;color:var(--text-secondary);margin-bottom:6px;font-weight:500}
|
||||||
|
.dropzone-hint{font-size:12px;color:var(--text-muted)}
|
||||||
|
.dropzone.hidden{display:none}
|
||||||
|
|
||||||
|
/* Status Bar */
|
||||||
|
.statusbar{height:28px;background:var(--surface-800);border-top:1px solid var(--surface-600);display:flex;align-items:center;padding:0 12px;gap:16px;flex-shrink:0;font-family:'JetBrains Mono',monospace;font-size:10px;color:var(--text-secondary)}
|
||||||
|
.statusbar span{white-space:nowrap}
|
||||||
|
.status-accent{color:var(--accent)}
|
||||||
|
|
||||||
|
/* Export Modal */
|
||||||
|
.modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);z-index:200;display:none;align-items:center;justify-content:center}
|
||||||
|
.modal-overlay.visible{display:flex}
|
||||||
|
.modal{background:var(--surface-700);border:1px solid var(--surface-600);border-radius:12px;padding:24px;width:380px;max-width:90vw}
|
||||||
|
.modal h3{font-size:16px;color:var(--text-primary);margin-bottom:16px;font-weight:600}
|
||||||
|
.modal-row{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
||||||
|
.modal-row label{font-size:12px;color:var(--text-muted);width:70px;flex-shrink:0}
|
||||||
|
.modal-row input,.modal-row select{flex:1;background:var(--surface-600);border:1px solid transparent;color:var(--text-secondary);padding:6px 10px;border-radius:6px;font-size:12px}
|
||||||
|
.modal-row input:focus,.modal-row select:focus{border-color:var(--accent);outline:none}
|
||||||
|
.modal-warning{font-size:11px;color:var(--warn);margin-bottom:12px;display:none}
|
||||||
|
.modal-warning.visible{display:block}
|
||||||
|
.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:16px}
|
||||||
|
.modal-actions button{padding:6px 16px;border-radius:6px;border:1px solid var(--surface-600);background:transparent;color:var(--text-secondary);cursor:pointer;font-size:12px;font-weight:500;transition:all .15s}
|
||||||
|
.modal-actions button:hover{border-color:var(--text-muted)}
|
||||||
|
.modal-actions button.primary{background:var(--accent);color:#000;border-color:var(--accent)}
|
||||||
|
.modal-actions button.primary:hover{background:var(--accent-dim)}
|
||||||
|
|
||||||
|
/* Text overlay — Figma-style inline editor */
|
||||||
|
.text-overlay{position:absolute;z-index:15;display:none;pointer-events:auto;transform-origin:top left}
|
||||||
|
.text-editor{background:rgba(0,84,230,0.04);border:1.5px solid rgba(0,84,230,0.5);outline:none;padding:3px 6px;min-width:4px;min-height:1.2em;white-space:pre-wrap;word-break:break-word;cursor:text;caret-color:currentColor;user-select:text;-webkit-user-select:text;line-height:1.4;border-radius:3px;transition:border-color .15s}
|
||||||
|
.text-editor:focus{border-color:var(--accent);background:rgba(0,84,230,0.07);box-shadow:0 0 0 3px rgba(0,84,230,0.1)}
|
||||||
|
.text-editor:empty::before{content:'Введите текст…';color:rgba(255,255,255,0.25);pointer-events:none}
|
||||||
|
|
||||||
|
/* Tab Bar */
|
||||||
|
.tab-bar{height:30px;background:var(--surface-800);border-bottom:1px solid var(--surface-600);display:flex;align-items:stretch;overflow-x:auto;overflow-y:hidden;flex-shrink:0;scrollbar-width:none;z-index:18}
|
||||||
|
.tab-bar::-webkit-scrollbar{display:none}
|
||||||
|
.tab-item{display:flex;align-items:center;gap:5px;padding:0 8px 0 12px;border-right:1px solid var(--surface-600);white-space:nowrap;cursor:pointer;font-size:11px;color:var(--text-muted);flex-shrink:0;max-width:160px;transition:color .15s;position:relative}
|
||||||
|
.tab-item:hover{color:var(--text-secondary);background:rgba(255,255,255,.03)}
|
||||||
|
.tab-item.active{color:var(--text-primary);background:var(--surface-700);border-bottom:2px solid var(--accent)}
|
||||||
|
.tab-name{flex:1;overflow:hidden;text-overflow:ellipsis;min-width:40px}
|
||||||
|
.tab-close{width:16px;height:16px;border:none;background:transparent;color:transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:3px;flex-shrink:0;font-size:14px;line-height:1;padding:0;transition:all .15s}
|
||||||
|
.tab-item:hover .tab-close,.tab-item.active .tab-close{color:var(--text-muted)}
|
||||||
|
.tab-close:hover{color:var(--danger)!important;background:rgba(255,82,82,.12)}
|
||||||
|
.tab-add{width:32px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .15s;border-right:1px solid var(--surface-600)}
|
||||||
|
.tab-add:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
|
||||||
|
/* Color picker in toolbar */
|
||||||
|
.color-swatch{width:22px;height:22px;border-radius:6px;border:2px solid var(--surface-600);cursor:pointer;overflow:hidden;position:relative}
|
||||||
|
.color-swatch input{position:absolute;inset:-8px;width:calc(100% + 16px);height:calc(100% + 16px);cursor:pointer;opacity:0}
|
||||||
|
|
||||||
|
/* Crop overlay */
|
||||||
|
.crop-overlay{position:absolute;inset:0;z-index:12;display:none;pointer-events:none}
|
||||||
|
.crop-overlay.visible{display:block;pointer-events:auto}
|
||||||
|
.crop-handle{position:absolute;width:10px;height:10px;background:var(--accent);border:1px solid #000;border-radius:2px;cursor:pointer;z-index:13}
|
||||||
|
|
||||||
|
/* Context menu */
|
||||||
|
.ctx-menu{position:fixed;z-index:300;background:var(--surface-700);border:1px solid var(--surface-600);border-radius:10px;padding:4px;min-width:180px;box-shadow:0 8px 32px rgba(0,0,0,.5);display:none;backdrop-filter:blur(12px)}
|
||||||
|
.ctx-menu.visible{display:block}
|
||||||
|
.ctx-item{display:flex;align-items:center;gap:8px;padding:6px 12px;border-radius:6px;cursor:pointer;color:var(--text-secondary);font-size:12px;white-space:nowrap;transition:background .1s}
|
||||||
|
.ctx-item:hover{background:rgba(255,255,255,.06);color:var(--text-primary)}
|
||||||
|
.ctx-item.disabled{opacity:.3;pointer-events:none}
|
||||||
|
.ctx-item svg{width:15px;height:15px;flex-shrink:0;color:var(--text-muted)}
|
||||||
|
.ctx-item:hover svg{color:var(--text-secondary)}
|
||||||
|
.ctx-item .ctx-shortcut{margin-left:auto;font-size:10px;color:var(--text-muted);font-family:'JetBrains Mono',monospace}
|
||||||
|
.ctx-sep{height:1px;background:var(--surface-600);margin:4px 8px}
|
||||||
|
|
||||||
|
/* Scrollbar */
|
||||||
|
::-webkit-scrollbar{width:6px;height:6px}
|
||||||
|
::-webkit-scrollbar-track{background:transparent}
|
||||||
|
::-webkit-scrollbar-thumb{background:var(--surface-600);border-radius:3px}
|
||||||
|
|
||||||
|
/* Layer Panel */
|
||||||
|
.layer-panel{position:absolute;right:12px;bottom:40px;width:240px;max-height:400px;background:var(--surface-800);border:1px solid var(--surface-600);border-radius:10px;z-index:15;display:none;flex-direction:column;backdrop-filter:blur(12px);overflow:hidden}
|
||||||
|
.layer-panel.visible{display:flex}
|
||||||
|
.layer-panel-header{display:flex;align-items:center;justify-content:space-between;padding:8px 10px;border-bottom:1px solid var(--surface-600);font-size:11px;font-weight:600;color:var(--text-primary)}
|
||||||
|
.layer-panel-actions{display:flex;gap:2px}
|
||||||
|
.lpa-btn{width:28px;height:28px;border:none;border-radius:6px;background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:16px;transition:all .15s;padding:0}
|
||||||
|
.lpa-btn:hover{color:var(--text-secondary);background:rgba(255,255,255,.06)}
|
||||||
|
.lpa-btn:disabled{opacity:.3;cursor:default;pointer-events:none}
|
||||||
|
.layer-list{flex:1;overflow-y:auto;padding:4px}
|
||||||
|
.layer-item{display:flex;align-items:center;gap:6px;padding:5px 6px;border-radius:6px;cursor:pointer;transition:background .1s;user-select:none}
|
||||||
|
.layer-item:hover{background:rgba(255,255,255,.04)}
|
||||||
|
.layer-item.active{background:var(--accent-bg);outline:1px solid rgba(0,84,230,.3)}
|
||||||
|
.layer-item.drag-over-top{box-shadow:0 -2px 0 var(--accent)}
|
||||||
|
.layer-item.drag-over-bottom{box-shadow:0 2px 0 var(--accent)}
|
||||||
|
.layer-vis{width:20px;height:20px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;display:flex;align-items:center;justify-content:center;border-radius:4px;flex-shrink:0;font-size:12px;padding:0}
|
||||||
|
.layer-vis:hover{color:var(--text-secondary)}
|
||||||
|
.layer-vis.off{opacity:.3}
|
||||||
|
.layer-thumb{width:32px;height:32px;border-radius:4px;border:1px solid var(--surface-600);background-image:linear-gradient(45deg,#333 25%,transparent 25%),linear-gradient(-45deg,#333 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#333 75%),linear-gradient(-45deg,transparent 75%,#333 75%);background-size:8px 8px;background-position:0 0,0 4px,4px -4px,-4px 0;flex-shrink:0;overflow:hidden;position:relative}
|
||||||
|
.layer-thumb canvas{width:100%;height:100%;display:block}
|
||||||
|
.layer-info{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
|
||||||
|
.layer-name{font-size:11px;color:var(--text-secondary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||||
|
.layer-name-input{font-size:11px;color:var(--text-secondary);background:var(--surface-600);border:1px solid var(--accent);border-radius:3px;padding:1px 4px;outline:none;width:100%;font-family:inherit}
|
||||||
|
.layer-meta{font-size:9px;color:var(--text-muted);font-family:'JetBrains Mono',monospace}
|
||||||
|
.layer-props{padding:6px 10px;border-top:1px solid var(--surface-600);display:flex;flex-direction:column;gap:5px}
|
||||||
|
.layer-props label{font-size:10px;color:var(--text-muted)}
|
||||||
|
.layer-props select{background:var(--surface-600);border:1px solid transparent;color:var(--text-secondary);padding:3px 6px;border-radius:4px;font-size:10px;cursor:pointer;width:100%;font-family:inherit}
|
||||||
|
.layer-props select:focus{border-color:var(--accent);outline:none}
|
||||||
|
.layer-props input[type="range"]{width:100%;accent-color:var(--accent);height:3px}
|
||||||
|
.lp-row{display:flex;align-items:center;gap:6px}
|
||||||
|
.lp-row label{width:55px;flex-shrink:0}
|
||||||
|
.lp-row span{font-size:10px;color:var(--text-muted);font-family:'JetBrains Mono',monospace;width:28px;text-align:right;flex-shrink:0}
|
||||||
|
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 8px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:2px;flex:none}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.editor-wrap{margin-left:0;height:calc(100vh - 52px)}
|
||||||
|
}
|
||||||
272
public/editor.html
Normal file
272
public/editor.html
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Editor — images.wadevelop.ru</title>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/editor.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg></a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg></a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg></a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg></a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg></a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg></a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg></a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg></a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg></a>
|
||||||
|
<a href="/editor" class="sidebar-link active" title="Editor"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg></a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Editor -->
|
||||||
|
<div class="editor-wrap">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="toolbar">
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button class="tb" id="btnOpen" title="Open (Ctrl+O)"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 9.776c.112-.017.227-.026.344-.026h15.812c.117 0 .232.009.344.026m-16.5 0a2.25 2.25 0 00-1.883 2.542l.857 6a2.25 2.25 0 002.227 1.932H19.05a2.25 2.25 0 002.227-1.932l.857-6a2.25 2.25 0 00-1.883-2.542m-16.5 0V6A2.25 2.25 0 016 3.75h3.879a1.5 1.5 0 011.06.44l2.122 2.12a1.5 1.5 0 001.06.44H18A2.25 2.25 0 0120.25 9v.776"/></svg></button>
|
||||||
|
<button class="tb" id="btnExport" title="Export (Ctrl+S)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
|
||||||
|
<!-- Drawing tools -->
|
||||||
|
<div class="toolbar-group" id="toolsGroup">
|
||||||
|
<button class="tb" data-tool="select" title="Выделение (V)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path stroke-linecap="round" stroke-linejoin="round" d="M13 13l6 6"/></svg></button>
|
||||||
|
<button class="tb" data-tool="crop" title="Вырезать (C)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5"/></svg></button>
|
||||||
|
<button class="tb" data-tool="brush" title="Кисть (B)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg></button>
|
||||||
|
<button class="tb" data-tool="eraser" title="Ластик (E)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5.505 14.706L10.293 19.5H14.5m-9-4.794l8.5-8.5a2 2 0 012.828 0l2.466 2.466a2 2 0 010 2.828l-8.5 8.5H6.706a2 2 0 01-1.414-.586l-1.2-1.2a2 2 0 010-2.828z"/><path stroke-linecap="round" d="M8 21h12"/></svg></button>
|
||||||
|
<button class="tb" data-tool="line" title="Линия (L)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="5" y1="19" x2="19" y2="5" stroke-linecap="round"/></svg></button>
|
||||||
|
<button class="tb" data-tool="rect" title="Прямоугольник (U)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" stroke-linecap="round" stroke-linejoin="round"/></svg></button>
|
||||||
|
<button class="tb" data-tool="circle" title="Круг (O)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><ellipse cx="12" cy="12" rx="9" ry="9"/></svg></button>
|
||||||
|
<button class="tb" data-tool="arrow" title="Стрелка (A)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 19.5l15-15m0 0H8.25m11.25 0v11.25"/></svg></button>
|
||||||
|
<button class="tb" data-tool="text" title="Текст (T)" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M12.2,19.6V4H6.9L5.8,5V4.4l0.9-0.8h11.8l0.6,0.8V5l-0.8-1h-5.7v15.6H12.2z"/>
|
||||||
|
|
||||||
|
</svg></button>
|
||||||
|
<button class="tb" data-tool="eyedropper" title="Пипетка (I)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path d="M20.9,6c0-0.6-0.2-1.2-0.7-1.7c-0.4-0.4-1-0.7-1.7-0.7c-0.6,0-1.2,0.2-1.7,0.7l-3,3l-1.2-1.2c0,0-0.1,0-0.2,0
|
||||||
|
c0,0,0,0.1,0,0.2l1.2,1.2l-7.8,7.8c-0.4,0.4-0.7,1-0.7,1.7c0,0.6,0.2,1.1,0.6,1.6l-2.3,2.3c0,0,0,0.1,0,0.2c0,0,0.1,0,0.1,0
|
||||||
|
s0.1,0,0.1,0L6,18.7c0.4,0.4,1,0.6,1.6,0.6c0.6,0,1.2-0.2,1.7-0.7l7.8-7.8l1.2,1.2c0,0,0.1,0,0.1,0s0.1,0,0.1,0c0,0,0-0.1,0-0.2
|
||||||
|
l-1.2-1.2l3-3C20.6,7.3,20.9,6.7,20.9,6z M9.1,18.5c-0.8,0.8-2.2,0.8-3,0c-0.4-0.4-0.6-0.9-0.6-1.5c0-0.6,0.2-1.1,0.6-1.5l7.8-7.8
|
||||||
|
l3,3L9.1,18.5z M20,7.5l-3,3l-3-3l3-3c0.4-0.4,0.9-0.6,1.5-0.6c0.6,0,1.1,0.2,1.5,0.6c0.4,0.4,0.6,0.9,0.6,1.5
|
||||||
|
C20.6,6.6,20.4,7.1,20,7.5C20,7.5,20,7.5,20,7.5z"/>
|
||||||
|
|
||||||
|
</svg></button>
|
||||||
|
<button class="tb" data-tool="fill" title="Заливка (G)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <path d="M20.5,19.5c0,1.3-1,2.3-2.3,2.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,2.1-4.5,2.3-4.5C18.4,15,20.5,18.2,20.5,19.5z"/>
|
||||||
|
<path d="M10.4,5L10.4,5L7.4,1.9L7.2,2.1l3,3l-6.8,6.8c-0.4,0.4-0.7,1-0.7,1.6s0.2,1.2,0.7,1.6L8.3,20c0.4,0.4,1,0.7,1.6,0.7
|
||||||
|
c0.6,0,1.2-0.2,1.6-0.7l6.9-6.9L10.4,5z M3,13.5c0-0.5,0.2-1.1,0.6-1.5l6.8-6.8l7.7,7.7l-4,4H5.7h0L3.6,15C3.2,14.6,3,14,3,13.5z"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</svg></button>
|
||||||
|
<button class="tb" data-tool="clone" title="Штамп (S)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75"/><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 3h9.75a1.125 1.125 0 011.125 1.125v9.75c0 .621-.504 1.125-1.125 1.125H8.25a1.125 1.125 0 01-1.125-1.125v-9.75c0-.621.504-1.125 1.125-1.125z"/></svg></button>
|
||||||
|
<button class="tb" data-tool="lasso" title="Лассо (W)" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="3 2" d="M12 4c-2 1.5-5 2-7 5s-1 6 1 8 5 2 8 1 5-4 5-7-2-6-5-7"/><path stroke-linecap="round" stroke-linejoin="round" d="M12 4c1 1 3 2 3 4"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
|
||||||
|
<!-- Transform -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button class="tb" id="btnRotCCW" title="Повернуть CCW" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/></svg></button>
|
||||||
|
<button class="tb" id="btnRotCW" title="Повернуть CW" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 15l6-6m0 0l-6-6m6 6H9a6 6 0 000 12h3"/></svg></button>
|
||||||
|
<button class="tb" id="btnFlipH" title="Flip H" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg></button>
|
||||||
|
<button class="tb" id="btnFlipV" title="Flip V" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5"/></svg></button>
|
||||||
|
<button class="tb" id="btnResize" title="Resize" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"/></svg></button>
|
||||||
|
<button class="tb" id="btnAdjust" title="Adjustments" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 11-3 0m3 0a1.5 1.5 0 10-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 01-3 0m3 0a1.5 1.5 0 00-3 0m-9.75 0h9.75"/></svg></button>
|
||||||
|
<button class="tb" id="btnBlur" title="Размытие" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9" stroke-dasharray="3 2"/><circle cx="12" cy="12" r="5" opacity=".5"/><circle cx="12" cy="12" r="2" opacity=".3"/></svg></button>
|
||||||
|
<button class="tb" id="btnSharpen" title="Резкость" disabled><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3l2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3z"/></svg></button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
|
||||||
|
<!-- Filters -->
|
||||||
|
<div class="toolbar-group" id="filtersGroup">
|
||||||
|
<button class="tb" id="btnGrayscale" title="Ч/Б" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="9"/><path d="M12 3v18" stroke-width="1.5"/><path d="M12 3a9 9 0 010 18" fill="currentColor" opacity=".35"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb" id="btnSepia" title="Сепия" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="5" width="18" height="14" rx="2"/><circle cx="8.5" cy="10.5" r="2"/><path stroke-linecap="round" stroke-linejoin="round" d="M21 15l-5-5-4 4-3-2-4 4"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb" id="btnInvert" title="Инверсия" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M12 3v18" stroke-width="1"/><rect x="3" y="3" width="9" height="18" fill="currentColor" opacity=".5" rx="2"/></svg>
|
||||||
|
</button>
|
||||||
|
<button class="tb" id="btnAutoLevels" title="Авто-уровни" disabled>
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 18h2m0 0a2 2 0 104 0m-4 0a2 2 0 114 0m0 0h12M3 12h6m0 0a2 2 0 104 0m-4 0a2 2 0 114 0m0 0h8M3 6h10m0 0a2 2 0 104 0m-4 0a2 2 0 114 0m0 0h4"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
|
||||||
|
<!-- Undo/Redo -->
|
||||||
|
<div class="toolbar-group">
|
||||||
|
<button class="tb" id="btnUndo" title="Undo (Ctrl+Z)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/></svg></button>
|
||||||
|
<button class="tb" id="btnRedo" title="Redo (Ctrl+Shift+Z)" disabled><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 15l6-6m0 0l-6-6m6 6H9a6 6 0 000 12h3"/></svg></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right side: color + zoom -->
|
||||||
|
<div class="toolbar-right">
|
||||||
|
<button class="tb" id="btnLayers" title="Слои"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 2.25l9.75 5.25L12 12.75 2.25 7.5 12 2.25z"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l9.75 5.25L21.75 12"/><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 16.5l9.75 5.25 9.75-5.25"/></svg></button>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
<div class="color-swatch" id="colorSwatch" title="Color">
|
||||||
|
<input type="color" id="colorPicker" value="#0054e6">
|
||||||
|
</div>
|
||||||
|
<div class="toolbar-sep"></div>
|
||||||
|
<button class="tb" id="btnZoomOut" title="Zoom Out"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM13.5 10.5h-6"/></svg></button>
|
||||||
|
<span id="zoomLabel" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);min-width:40px;text-align:center">100%</span>
|
||||||
|
<button class="tb" id="btnZoomIn" title="Zoom In"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6"/></svg></button>
|
||||||
|
<button class="tb" id="btnFit" title="Fit (Ctrl+0)"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m9 0h4.5v4.5m0 9v4.5h-4.5m-9 0H3.75v-4.5"/></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tool Options (context-sensitive) -->
|
||||||
|
<div class="tool-options" id="toolOptions"></div>
|
||||||
|
|
||||||
|
<!-- Tab Bar -->
|
||||||
|
<div class="tab-bar" id="tabBar"></div>
|
||||||
|
|
||||||
|
<!-- Viewport -->
|
||||||
|
<div class="viewport" id="viewport">
|
||||||
|
<div class="viewport-checker"></div>
|
||||||
|
<div class="canvas-container" id="canvasContainer">
|
||||||
|
<canvas id="canvasMain"></canvas>
|
||||||
|
<canvas id="canvasOverlay"></canvas>
|
||||||
|
</div>
|
||||||
|
<canvas id="canvasUI" style="position:absolute;inset:0;z-index:5;pointer-events:none"></canvas>
|
||||||
|
|
||||||
|
<!-- Drop zone -->
|
||||||
|
<div class="dropzone" id="dropzone">
|
||||||
|
<div class="dropzone-icon">
|
||||||
|
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div class="dropzone-text">Перетащите изображение, вставьте из буфера или нажмите</div>
|
||||||
|
<div class="dropzone-hint">Ctrl+O / Ctrl+V / Drag & Drop</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text overlay — contenteditable inline editor -->
|
||||||
|
<div class="text-overlay" id="textOverlay">
|
||||||
|
<div id="textArea" class="text-editor" contenteditable="true" spellcheck="false" role="textbox" aria-multiline="true"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Layer Panel -->
|
||||||
|
<div class="layer-panel" id="layerPanel">
|
||||||
|
<div class="layer-panel-header">
|
||||||
|
<span>Слои</span>
|
||||||
|
<div class="layer-panel-actions">
|
||||||
|
<button class="lpa-btn" id="lpAdd" title="Новый слой (Ctrl+Shift+N)">+</button>
|
||||||
|
<button class="lpa-btn" id="lpDel" title="Удалить слой">×</button>
|
||||||
|
<button class="lpa-btn" id="lpMerge" title="Объединить вниз (Ctrl+E)">↓</button>
|
||||||
|
<button class="lpa-btn" id="lpFlatten" title="Свести всё (Ctrl+Shift+E)">F</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="layer-list" id="layerList"></div>
|
||||||
|
<div class="layer-props" id="layerProps">
|
||||||
|
<div class="lp-row">
|
||||||
|
<label>Режим</label>
|
||||||
|
<select id="lpBlend">
|
||||||
|
<option value="source-over">Normal</option>
|
||||||
|
<option value="multiply">Multiply</option>
|
||||||
|
<option value="screen">Screen</option>
|
||||||
|
<option value="overlay">Overlay</option>
|
||||||
|
<option value="darken">Darken</option>
|
||||||
|
<option value="lighten">Lighten</option>
|
||||||
|
<option value="color-dodge">Color Dodge</option>
|
||||||
|
<option value="color-burn">Color Burn</option>
|
||||||
|
<option value="hard-light">Hard Light</option>
|
||||||
|
<option value="soft-light">Soft Light</option>
|
||||||
|
<option value="difference">Difference</option>
|
||||||
|
<option value="exclusion">Exclusion</option>
|
||||||
|
<option value="hue">Hue</option>
|
||||||
|
<option value="saturation">Saturation</option>
|
||||||
|
<option value="color">Color</option>
|
||||||
|
<option value="luminosity">Luminosity</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="lp-row">
|
||||||
|
<label>Прозр.</label>
|
||||||
|
<input type="range" id="lpOpacity" min="0" max="100" value="100">
|
||||||
|
<span id="lpOpacityVal">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Status Bar -->
|
||||||
|
<div class="statusbar">
|
||||||
|
<span id="statusDims">Нет изображения</span>
|
||||||
|
<span id="statusZoom">100%</span>
|
||||||
|
<span id="statusCursor">0, 0</span>
|
||||||
|
<span id="statusHistory"></span>
|
||||||
|
<span id="statusLayer"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export Modal -->
|
||||||
|
<div class="modal-overlay" id="exportModal">
|
||||||
|
<div class="modal">
|
||||||
|
<h3>Экспорт</h3>
|
||||||
|
<div class="modal-row">
|
||||||
|
<label>Имя файла</label>
|
||||||
|
<input type="text" id="exportName" value="image">
|
||||||
|
</div>
|
||||||
|
<div class="modal-row">
|
||||||
|
<label>Формат</label>
|
||||||
|
<select id="exportFormat">
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="jpeg">JPEG</option>
|
||||||
|
<option value="webp">WebP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="modal-row" id="qualityRow">
|
||||||
|
<label>Качество</label>
|
||||||
|
<input type="range" id="exportQuality" min="1" max="100" value="92">
|
||||||
|
<span id="exportQualityVal" style="font-family:'JetBrains Mono',monospace;font-size:11px;color:var(--text-muted);width:30px;text-align:right">92</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-warning" id="exportWarning">Изображение содержит прозрачность. JPEG заменит её белым фоном.</div>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button id="exportCancel">Отмена</button>
|
||||||
|
<button id="exportConfirm" class="primary">Скачать</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Context Menu -->
|
||||||
|
<div class="ctx-menu" id="ctxMenu">
|
||||||
|
<div class="ctx-item" data-action="undo"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/></svg>Отменить<span class="ctx-shortcut">Ctrl+Z</span></div>
|
||||||
|
<div class="ctx-item" data-action="redo"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 15l6-6m0 0l-6-6m6 6H9a6 6 0 000 12h3"/></svg>Повторить<span class="ctx-shortcut">Ctrl+Shift+Z</span></div>
|
||||||
|
<div class="ctx-sep"></div>
|
||||||
|
<div class="ctx-item" data-action="crop-sel"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5m0 9V18A2.25 2.25 0 0118 20.25h-1.5m-9 0H6A2.25 2.25 0 013.75 18v-1.5"/></svg>Обрезать по выделению</div>
|
||||||
|
<div class="ctx-sep"></div>
|
||||||
|
<div class="ctx-item" data-action="resize"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"/></svg>Изменить размер...</div>
|
||||||
|
<div class="ctx-item" data-action="rot-cw"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15 15l6-6m0 0l-6-6m6 6H9a6 6 0 000 12h3"/></svg>Повернуть 90° вправо</div>
|
||||||
|
<div class="ctx-item" data-action="rot-ccw"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3"/></svg>Повернуть 90° влево</div>
|
||||||
|
<div class="ctx-item" data-action="flip-h"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>Отразить по горизонтали</div>
|
||||||
|
<div class="ctx-item" data-action="flip-v"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7.5L7.5 3m0 0L12 7.5M7.5 3v13.5m13.5 0L16.5 21m0 0L12 16.5m4.5 4.5V7.5"/></svg>Отразить по вертикали</div>
|
||||||
|
<div class="ctx-sep"></div>
|
||||||
|
<div class="ctx-item" data-action="fit"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 3.75v4.5m0-4.5h4.5m9 0h4.5v4.5m0 9v4.5h-4.5m-9 0H3.75v-4.5"/></svg>Вписать в экран<span class="ctx-shortcut">Ctrl+0</span></div>
|
||||||
|
<div class="ctx-item" data-action="zoom100"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607zM10.5 7.5v6m3-3h-6"/></svg>Масштаб 100%<span class="ctx-shortcut">Ctrl+1</span></div>
|
||||||
|
<div class="ctx-sep"></div>
|
||||||
|
<div class="ctx-item" data-action="copy"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9.75a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"/></svg>Копировать в буфер</div>
|
||||||
|
<div class="ctx-item" data-action="paste"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m3.75 9v6m3-3H9m1.5-12H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>Вставить из буфера<span class="ctx-shortcut">Ctrl+V</span></div>
|
||||||
|
<div class="ctx-sep"></div>
|
||||||
|
<div class="ctx-item" data-action="export"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>Экспорт...<span class="ctx-shortcut">Ctrl+S</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden file input -->
|
||||||
|
<input type="file" id="fileInput" accept="image/*" style="display:none">
|
||||||
|
|
||||||
|
<script src="/editor.js" defer></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3601
public/editor.js
Normal file
3601
public/editor.js
Normal file
File diff suppressed because it is too large
Load Diff
260
public/formatter.html
Normal file
260
public/formatter.html
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Code Formatter — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
:root{--bg:#f5f5f7;--bg-grad1:rgba(0,67,184,.04);--bg-grad2:rgba(0,150,60,.03);--surface-800:rgba(255,255,255,0.85);--surface-700:#f0f0f3;--surface-600:#dde0e6;--text-primary:#1a1a2e;--text-secondary:#555;--text-muted:#888;--select-bg:#f0f0f3;--select-color:#1a1a2e}
|
||||||
|
.dark{--bg:#060c18;--bg-grad1:rgba(0,84,230,.03);--bg-grad2:rgba(0,67,184,.02);--surface-800:rgba(9,16,32,0.8);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--select-bg:#091020;--select-color:#e0e0e0}
|
||||||
|
body{background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,var(--bg-grad1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,var(--bg-grad2) 0%,transparent 50%);min-height:100vh;transition:background .3s}
|
||||||
|
.dark .noise::before{content:'';position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
|
||||||
|
.theme-toggle{position:relative;width:44px;height:24px;border-radius:12px;cursor:pointer;transition:background .3s}
|
||||||
|
.dark .theme-toggle{background:#162040}.theme-toggle{background:#dde0e6}
|
||||||
|
.theme-toggle-knob{position:absolute;top:2px;width:20px;height:20px;border-radius:50%;transition:left .3s,background .3s;display:flex;align-items:center;justify-content:center}
|
||||||
|
.dark .theme-toggle-knob{left:22px;background:#0d1828}.theme-toggle-knob{left:2px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
||||||
|
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.fade-up{animation:fadeUp .4s ease-out forwards}.fade-up-delay{animation:fadeUp .4s ease-out .1s forwards;opacity:0}
|
||||||
|
.tool-btn{transition:all .2s}.tool-btn.active{color:#0054e6;background:rgba(0,84,230,.08);border-color:rgba(0,84,230,.3)}
|
||||||
|
textarea{font-family:'JetBrains Mono',monospace;font-size:13px;resize:vertical}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg></a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg></a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg></a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg></a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg></a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg></a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg></a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg></a>
|
||||||
|
<a href="/formatter" class="sidebar-link active" title="Formatter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg></a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg></a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-3xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
CSS · JavaScript
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Code <span class="text-accent">Formatter</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">форматирование и beautify кода</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tool Toggle -->
|
||||||
|
<div class="flex gap-2 mb-5 fade-up-delay">
|
||||||
|
<button class="tool-btn active flex-1 px-4 py-3 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="css">
|
||||||
|
<div class="text-sm font-semibold">CSS</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn flex-1 px-4 py-3 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="js">
|
||||||
|
<div class="text-sm font-semibold">JavaScript</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Input</label>
|
||||||
|
<textarea id="inputArea" rows="10" placeholder="Вставьте CSS..."
|
||||||
|
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none focus:border-accent/50 transition-colors"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="processBtn"
|
||||||
|
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent text-white border border-accent cursor-pointer
|
||||||
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20 active:scale-[0.98]">
|
||||||
|
Форматировать
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="outputBlock" class="mt-5 hidden">
|
||||||
|
<div class="flex items-center justify-between mb-1.5">
|
||||||
|
<label class="text-xs font-medium dark:text-gray-500 text-gray-400 font-mono uppercase tracking-wider">Output</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span id="stats" class="text-[10px] font-mono dark:text-gray-600 text-gray-400"></span>
|
||||||
|
<button id="copyBtn" class="px-3 py-1 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">Копировать</button>
|
||||||
|
<button id="clearBtn" class="px-3 py-1 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-400 text-gray-500 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">Очистить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="outputArea" rows="14" readonly
|
||||||
|
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none transition-colors"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
let activeTool = 'css';
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
activeTool = btn.dataset.tool;
|
||||||
|
$('inputArea').placeholder = activeTool === 'css' ? 'Вставьте CSS...' : 'Вставьте JavaScript...';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('processBtn').addEventListener('click', () => {
|
||||||
|
const input = $('inputArea').value;
|
||||||
|
if (!input.trim()) return;
|
||||||
|
let output = '';
|
||||||
|
try {
|
||||||
|
output = activeTool === 'css' ? formatCss(input) : formatJs(input);
|
||||||
|
} catch (e) { output = 'Error: ' + e.message; }
|
||||||
|
$('outputArea').value = output;
|
||||||
|
$('outputBlock').classList.remove('hidden');
|
||||||
|
const diff = output.length - input.length;
|
||||||
|
$('stats').textContent = `${input.length} \u2192 ${output.length} (${diff > 0 ? '+' : ''}${diff})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyBtn').addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText($('outputArea').value).then(() => {
|
||||||
|
const b = $('copyBtn'); const o = b.textContent;
|
||||||
|
b.textContent = '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e!'; setTimeout(() => { b.textContent = o; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('clearBtn').addEventListener('click', () => {
|
||||||
|
$('inputArea').value = ''; $('outputArea').value = ''; $('outputBlock').classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== CSS Formatter ====================
|
||||||
|
function formatCss(input) {
|
||||||
|
const preserved = [];
|
||||||
|
const hold = m => { preserved.push(m); return '\x00' + (preserved.length - 1) + '\x00'; };
|
||||||
|
|
||||||
|
let css = input;
|
||||||
|
css = css.replace(/(["'])(?:(?!\1|\\).|\\.)*\1/g, hold);
|
||||||
|
css = css.replace(/\/\*[\s\S]*?\*\//g, hold);
|
||||||
|
|
||||||
|
css = css.replace(/\s+/g, ' ').trim();
|
||||||
|
css = css.replace(/\s*\{\s*/g, ' {\n');
|
||||||
|
css = css.replace(/\s*\}\s*/g, '\n}\n\n');
|
||||||
|
css = css.replace(/;\s*/g, ';\n');
|
||||||
|
|
||||||
|
let indent = 0;
|
||||||
|
const lines = css.split('\n');
|
||||||
|
const formatted = [];
|
||||||
|
for (const raw of lines) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line) { if (formatted.length && formatted[formatted.length - 1] !== '') formatted.push(''); continue; }
|
||||||
|
if (line.startsWith('}')) indent = Math.max(0, indent - 1);
|
||||||
|
formatted.push(' '.repeat(indent) + line);
|
||||||
|
if (line.endsWith('{')) indent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = formatted.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
||||||
|
result = result.replace(/\x00(\d+)\x00/g, (_, i) => preserved[parseInt(i)]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== JS Formatter ====================
|
||||||
|
function formatJs(input) {
|
||||||
|
const preserved = [];
|
||||||
|
const hold = m => { preserved.push(m); return '\x00' + (preserved.length - 1) + '\x00'; };
|
||||||
|
|
||||||
|
let js = input;
|
||||||
|
js = js.replace(/`(?:[^`\\]|\\.)*`/gs, hold);
|
||||||
|
js = js.replace(/(["'])(?:(?!\1|\\).|\\.)*\1/g, hold);
|
||||||
|
js = js.replace(/\/\*[\s\S]*?\*\//g, hold);
|
||||||
|
js = js.replace(/\/\/[^\n]*/g, hold);
|
||||||
|
|
||||||
|
js = js.replace(/\s+/g, ' ').trim();
|
||||||
|
js = js.replace(/\s*\{\s*/g, ' {\n');
|
||||||
|
js = js.replace(/\s*\}\s*/g, '\n}\n');
|
||||||
|
js = js.replace(/;\s*/g, ';\n');
|
||||||
|
|
||||||
|
let indent = 0;
|
||||||
|
const lines = js.split('\n');
|
||||||
|
const formatted = [];
|
||||||
|
for (const raw of lines) {
|
||||||
|
const line = raw.trim();
|
||||||
|
if (!line) { if (formatted.length && formatted[formatted.length - 1] !== '') formatted.push(''); continue; }
|
||||||
|
if (line.startsWith('}')) indent = Math.max(0, indent - 1);
|
||||||
|
formatted.push(' '.repeat(indent) + line);
|
||||||
|
if (line.endsWith('{')) indent++;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = formatted.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
||||||
|
result = result.replace(/\x00(\d+)\x00/g, (_, i) => preserved[parseInt(i)]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
813
public/httpclient.html
Normal file
813
public/httpclient.html
Normal file
@ -0,0 +1,813 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>HTTP Client — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="/vendor/hljs-github-dark.css">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
:root{--bg:#f5f5f7;--bg-grad1:rgba(0,67,184,.04);--bg-grad2:rgba(0,50,140,.03);--surface-800:rgba(255,255,255,0.85);--surface-700:#f0f0f3;--surface-600:#dde0e6;--text-primary:#1a1a2e;--text-secondary:#555;--text-muted:#888;--select-bg:#fff;--select-color:#1a1a2e}
|
||||||
|
.dark{--bg:#060c18;--bg-grad1:rgba(0,84,230,.03);--bg-grad2:rgba(0,67,184,.02);--surface-800:rgba(9,16,32,0.8);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--select-bg:#091020;--select-color:#e0e0e0}
|
||||||
|
body{background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,var(--bg-grad1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,var(--bg-grad2) 0%,transparent 50%);min-height:100vh;transition:background .3s}
|
||||||
|
.dark .noise::before{content:'';position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
|
||||||
|
.theme-toggle{position:relative;width:44px;height:24px;border-radius:12px;cursor:pointer;transition:background .3s}
|
||||||
|
.dark .theme-toggle{background:#162040}
|
||||||
|
.theme-toggle{background:#dde0e6}
|
||||||
|
.theme-toggle-knob{position:absolute;top:2px;width:20px;height:20px;border-radius:50%;transition:left .3s,background .3s;display:flex;align-items:center;justify-content:center}
|
||||||
|
.dark .theme-toggle-knob{left:22px;background:#0d1828}
|
||||||
|
.theme-toggle-knob{left:2px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
||||||
|
|
||||||
|
/* HTTP Client UI */
|
||||||
|
.panel {
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.tab-btn {
|
||||||
|
padding: 6px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tab-btn:hover { color: var(--text-secondary); background: rgba(0,84,230,.05); }
|
||||||
|
.tab-btn.active { color: #0054e6; background: rgba(0,84,230,.08); }
|
||||||
|
.kv-row { display: flex; gap: 6px; align-items: center; margin-bottom: 6px; }
|
||||||
|
.kv-row input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.kv-row input:focus { border-color: rgba(0,84,230,.5); }
|
||||||
|
.kv-row input::placeholder { color: var(--text-muted); }
|
||||||
|
.kv-del {
|
||||||
|
width: 24px; height: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: all .15s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.kv-del:hover { color: #ff5252; background: rgba(255,82,82,.1); }
|
||||||
|
.kv-add {
|
||||||
|
font-size: 11px; font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
.kv-add:hover { color: #0054e6; }
|
||||||
|
.method-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1px 7px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.m-GET { background: rgba(0,168,107,.15); color: #00a86b; }
|
||||||
|
.m-POST { background: rgba(255,165,0,.15); color: #ffa500; }
|
||||||
|
.m-PUT { background: rgba(0,84,230,.15); color: #6b9fff; }
|
||||||
|
.m-PATCH { background: rgba(148,0,211,.15); color: #cc66ff; }
|
||||||
|
.m-DELETE { background: rgba(255,82,82,.15); color: #ff5252; }
|
||||||
|
.m-HEAD { background: rgba(150,150,150,.15); color: #aaa; }
|
||||||
|
.m-OPTIONS{ background: rgba(150,150,150,.15); color: #aaa; }
|
||||||
|
.status-2xx { color: #00a86b; }
|
||||||
|
.status-3xx { color: #6b9fff; }
|
||||||
|
.status-4xx { color: #ffa500; }
|
||||||
|
.status-5xx { color: #ff5252; }
|
||||||
|
.send-btn {
|
||||||
|
background: #0054e6;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
transition: background .2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.send-btn:hover { background: #0043b8; }
|
||||||
|
.send-btn:disabled { background: #0054e680; cursor: not-allowed; }
|
||||||
|
.url-input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.url-input:focus { border-color: rgba(0,84,230,.5); }
|
||||||
|
.url-input::placeholder { color: var(--text-muted); }
|
||||||
|
select.method-select {
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
select.method-select:focus { border-color: rgba(0,84,230,.5); }
|
||||||
|
.response-body-pre {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
padding: 12px;
|
||||||
|
margin: 0;
|
||||||
|
min-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
.hljs { background: transparent !important; padding: 0 !important; }
|
||||||
|
.history-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .15s;
|
||||||
|
}
|
||||||
|
.history-item:hover { background: rgba(255,255,255,.04); }
|
||||||
|
.history-url {
|
||||||
|
flex: 1;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.history-meta {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.body-type-select {
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
textarea.body-area {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 160px;
|
||||||
|
line-height: 1.6;
|
||||||
|
transition: border-color .2s;
|
||||||
|
}
|
||||||
|
textarea.body-area:focus { border-color: rgba(0,84,230,.5); }
|
||||||
|
textarea.body-area::placeholder { color: var(--text-muted); }
|
||||||
|
.spinner {
|
||||||
|
width: 16px; height: 16px;
|
||||||
|
border: 2px solid rgba(255,255,255,.3);
|
||||||
|
border-top-color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin .6s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.panels-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
@media(max-width: 900px) {
|
||||||
|
.panels-grid { grid-template-columns: 1fr; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link active" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-7xl mx-auto px-4 py-6">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
GET · POST · PUT · DELETE
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
HTTP <span class="text-accent">Клиент</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">тест API запросов</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL Bar -->
|
||||||
|
<div class="flex gap-2 mb-4">
|
||||||
|
<select id="method" class="method-select">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="PATCH">PATCH</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
<option value="HEAD">HEAD</option>
|
||||||
|
<option value="OPTIONS">OPTIONS</option>
|
||||||
|
</select>
|
||||||
|
<input id="urlInput" type="text" class="url-input" placeholder="https://api.example.com/endpoint" autocomplete="off" spellcheck="false" />
|
||||||
|
<button id="sendBtn" class="send-btn">
|
||||||
|
<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
Отправить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Panels -->
|
||||||
|
<div class="panels-grid mb-4">
|
||||||
|
|
||||||
|
<!-- REQUEST PANEL -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="flex border-b border-surface-600">
|
||||||
|
<button class="tab-btn active" data-reqtab="params">Параметры</button>
|
||||||
|
<button class="tab-btn" data-reqtab="headers">Заголовки</button>
|
||||||
|
<button class="tab-btn" data-reqtab="body">Тело</button>
|
||||||
|
<button class="tab-btn" data-reqtab="auth">Авторизация</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Params tab -->
|
||||||
|
<div id="reqTab-params" class="p-3">
|
||||||
|
<div id="params-rows"></div>
|
||||||
|
<div class="kv-add" id="params-add">+ Добавить параметр</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Headers tab -->
|
||||||
|
<div id="reqTab-headers" class="p-3 hidden">
|
||||||
|
<div id="headers-rows"></div>
|
||||||
|
<div class="kv-add" id="headers-add">+ Добавить заголовок</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body tab -->
|
||||||
|
<div id="reqTab-body" class="p-3 hidden">
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono text-muted">Тип:</span>
|
||||||
|
<select id="bodyType" class="body-type-select">
|
||||||
|
<option value="none">нет</option>
|
||||||
|
<option value="json">JSON</option>
|
||||||
|
<option value="form">form-urlencoded</option>
|
||||||
|
<option value="raw">текст</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="body-none" class="text-xs font-mono text-muted py-4 text-center">Тело запроса не отправляется</div>
|
||||||
|
<div id="body-json" class="hidden">
|
||||||
|
<textarea id="bodyJson" class="body-area" placeholder='{"key": "value"}'></textarea>
|
||||||
|
</div>
|
||||||
|
<div id="body-form" class="hidden">
|
||||||
|
<div id="form-rows"></div>
|
||||||
|
<div class="kv-add" id="form-add">+ Добавить поле</div>
|
||||||
|
</div>
|
||||||
|
<div id="body-raw" class="hidden">
|
||||||
|
<textarea id="bodyRaw" class="body-area" placeholder="Raw request body..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Auth tab -->
|
||||||
|
<div id="reqTab-auth" class="p-3 hidden">
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono text-muted">Тип:</span>
|
||||||
|
<select id="authType" class="body-type-select">
|
||||||
|
<option value="none">нет</option>
|
||||||
|
<option value="bearer">Bearer Токен</option>
|
||||||
|
<option value="basic">Basic Auth</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="auth-none" class="text-xs font-mono text-muted py-4 text-center">Без авторизации</div>
|
||||||
|
<div id="auth-bearer" class="hidden">
|
||||||
|
<input id="bearerToken" type="text" class="kv-row" style="width:100%;display:block;margin:0;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Токен..." />
|
||||||
|
</div>
|
||||||
|
<div id="auth-basic" class="hidden">
|
||||||
|
<input id="basicUser" type="text" style="width:100%;display:block;margin-bottom:8px;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Логин" />
|
||||||
|
<input id="basicPass" type="password" style="width:100%;display:block;border-radius:8px;padding:8px 12px;background:var(--select-bg);border:1px solid var(--surface-600);font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--select-color);outline:none" placeholder="Пароль" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RESPONSE PANEL -->
|
||||||
|
<div class="panel">
|
||||||
|
<!-- Status bar -->
|
||||||
|
<div id="responseStatusBar" class="flex items-center gap-3 px-4 py-2 border-b border-surface-600 min-h-[41px]">
|
||||||
|
<span class="text-xs font-mono text-muted">Нажмите «Отправить» для запроса</span>
|
||||||
|
</div>
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex border-b border-surface-600">
|
||||||
|
<button class="tab-btn active" data-restab="body">Тело</button>
|
||||||
|
<button class="tab-btn" data-restab="headers">Заголовки</button>
|
||||||
|
</div>
|
||||||
|
<!-- Body -->
|
||||||
|
<div id="resTab-body">
|
||||||
|
<div id="responseBodyWrap" style="max-height:480px;overflow:auto;position:relative">
|
||||||
|
<div id="responseBodyEmpty" class="flex flex-col items-center justify-center py-16 text-center">
|
||||||
|
<svg width="32" height="32" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1" class="mb-3 opacity-20"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
<span class="text-xs font-mono text-muted">Ответ появится здесь</span>
|
||||||
|
</div>
|
||||||
|
<pre id="responseBodyPre" class="response-body-pre hidden"><code id="responseBodyCode"></code></pre>
|
||||||
|
</div>
|
||||||
|
<div id="responseCopyBar" class="hidden flex items-center justify-between px-3 py-1.5 border-t border-surface-600">
|
||||||
|
<span id="responseSizeLabel" class="text-xs font-mono text-muted"></span>
|
||||||
|
<button id="copyBodyBtn" class="text-xs font-mono text-muted hover:text-accent transition-colors">Копировать</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Response Headers -->
|
||||||
|
<div id="resTab-headers" class="hidden p-3" style="max-height:480px;overflow:auto">
|
||||||
|
<div id="responseHeadersContent" class="text-xs font-mono space-y-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- History -->
|
||||||
|
<div class="panel">
|
||||||
|
<div class="flex items-center justify-between px-4 py-2 border-b border-surface-600 cursor-pointer select-none" id="historyToggle">
|
||||||
|
<span class="text-xs font-mono font-semibold" style="color:var(--text-secondary)">История запросов</span>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<button id="clearHistoryBtn" class="text-xs font-mono text-muted hover:text-danger transition-colors hidden">Очистить</button>
|
||||||
|
<svg id="historyArrow" width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" style="color:var(--text-muted);transition:transform .2s"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="historyList" class="px-2 py-1" style="max-height:260px;overflow-y:auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script src="/vendor/highlight.min.js"></script>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
// Theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) html.className = savedTheme;
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) html.className = '';
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
let reqTabs = {}, resTabs = {};
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
// KV row builder
|
||||||
|
function makeKvRow(container, keyVal, valVal) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'kv-row';
|
||||||
|
const k = document.createElement('input');
|
||||||
|
k.type = 'text'; k.placeholder = 'Ключ'; k.value = keyVal || '';
|
||||||
|
k.className = 'kv-input-key';
|
||||||
|
const v = document.createElement('input');
|
||||||
|
v.type = 'text'; v.placeholder = 'Значение'; v.value = valVal || '';
|
||||||
|
v.className = 'kv-input-val';
|
||||||
|
const del = document.createElement('div');
|
||||||
|
del.className = 'kv-del';
|
||||||
|
del.innerHTML = '<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>';
|
||||||
|
del.onclick = () => row.remove();
|
||||||
|
row.appendChild(k); row.appendChild(v); row.appendChild(del);
|
||||||
|
container.appendChild(row);
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getKvRows(container) {
|
||||||
|
const result = {};
|
||||||
|
container.querySelectorAll('.kv-row').forEach(row => {
|
||||||
|
const k = row.querySelector('.kv-input-key')?.value?.trim();
|
||||||
|
const v = row.querySelector('.kv-input-val')?.value?.trim();
|
||||||
|
if (k) result[k] = v || '';
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init KV containers
|
||||||
|
const paramsRows = document.getElementById('params-rows');
|
||||||
|
const headersRows = document.getElementById('headers-rows');
|
||||||
|
const formRows = document.getElementById('form-rows');
|
||||||
|
makeKvRow(paramsRows, '', '');
|
||||||
|
makeKvRow(headersRows, 'Content-Type', 'application/json');
|
||||||
|
makeKvRow(formRows, '', '');
|
||||||
|
|
||||||
|
document.getElementById('params-add').onclick = () => makeKvRow(paramsRows, '', '');
|
||||||
|
document.getElementById('headers-add').onclick = () => makeKvRow(headersRows, '', '');
|
||||||
|
document.getElementById('form-add').onclick = () => makeKvRow(formRows, '', '');
|
||||||
|
|
||||||
|
// Request tabs
|
||||||
|
document.querySelectorAll('[data-reqtab]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('[data-reqtab]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const tab = btn.dataset.reqtab;
|
||||||
|
['params','headers','body','auth'].forEach(t => {
|
||||||
|
document.getElementById('reqTab-' + t).classList.toggle('hidden', t !== tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Response tabs
|
||||||
|
document.querySelectorAll('[data-restab]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('[data-restab]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const tab = btn.dataset.restab;
|
||||||
|
['body','headers'].forEach(t => {
|
||||||
|
document.getElementById('resTab-' + t).classList.toggle('hidden', t !== tab);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Body type
|
||||||
|
document.getElementById('bodyType').addEventListener('change', function() {
|
||||||
|
['none','json','form','raw'].forEach(t => {
|
||||||
|
document.getElementById('body-' + t).classList.toggle('hidden', t !== this.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auth type
|
||||||
|
document.getElementById('authType').addEventListener('change', function() {
|
||||||
|
['none','bearer','basic'].forEach(t => {
|
||||||
|
document.getElementById('auth-' + t).classList.toggle('hidden', t !== this.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build request config
|
||||||
|
function buildRequest() {
|
||||||
|
const method = document.getElementById('method').value;
|
||||||
|
let url = document.getElementById('urlInput').value.trim();
|
||||||
|
if (!url) return null;
|
||||||
|
if (!url.startsWith('http://') && !url.startsWith('https://')) {
|
||||||
|
url = 'https://' + url;
|
||||||
|
document.getElementById('urlInput').value = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Params → append to URL
|
||||||
|
const params = getKvRows(paramsRows);
|
||||||
|
if (Object.keys(params).length > 0) {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
Object.entries(params).forEach(([k, v]) => u.searchParams.set(k, v));
|
||||||
|
url = u.toString();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = getKvRows(headersRows);
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
const authType = document.getElementById('authType').value;
|
||||||
|
if (authType === 'bearer') {
|
||||||
|
const tok = document.getElementById('bearerToken').value.trim();
|
||||||
|
if (tok) headers['Authorization'] = 'Bearer ' + tok;
|
||||||
|
} else if (authType === 'basic') {
|
||||||
|
const u = document.getElementById('basicUser').value;
|
||||||
|
const p = document.getElementById('basicPass').value;
|
||||||
|
headers['Authorization'] = 'Basic ' + btoa(u + ':' + p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Body
|
||||||
|
let body = null;
|
||||||
|
const bodyType = document.getElementById('bodyType').value;
|
||||||
|
if (!['GET','HEAD'].includes(method)) {
|
||||||
|
if (bodyType === 'json') {
|
||||||
|
body = document.getElementById('bodyJson').value.trim();
|
||||||
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
||||||
|
} else if (bodyType === 'form') {
|
||||||
|
const formData = getKvRows(formRows);
|
||||||
|
body = new URLSearchParams(formData).toString();
|
||||||
|
headers['Content-Type'] = 'application/x-www-form-urlencoded';
|
||||||
|
} else if (bodyType === 'raw') {
|
||||||
|
body = document.getElementById('bodyRaw').value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { method, url, headers, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send
|
||||||
|
async function sendRequest() {
|
||||||
|
if (isLoading) return;
|
||||||
|
const req = buildRequest();
|
||||||
|
if (!req) {
|
||||||
|
document.getElementById('urlInput').focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = true;
|
||||||
|
const sendBtn = document.getElementById('sendBtn');
|
||||||
|
sendBtn.disabled = true;
|
||||||
|
sendBtn.innerHTML = '<div class="spinner"></div> Отправка...';
|
||||||
|
|
||||||
|
// Status bar: loading
|
||||||
|
const statusBar = document.getElementById('responseStatusBar');
|
||||||
|
statusBar.innerHTML = '<span class="text-xs font-mono text-muted">Отправка запроса...</span>';
|
||||||
|
|
||||||
|
// Hide response
|
||||||
|
document.getElementById('responseBodyEmpty').classList.remove('hidden');
|
||||||
|
document.getElementById('responseBodyPre').classList.add('hidden');
|
||||||
|
document.getElementById('responseCopyBar').classList.add('hidden');
|
||||||
|
document.getElementById('responseHeadersContent').innerHTML = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const t0 = performance.now();
|
||||||
|
const resp = await fetch('/api/proxy', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(req),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
const elapsed = Math.round(performance.now() - t0);
|
||||||
|
|
||||||
|
if (data.error && !data.status) {
|
||||||
|
// Network/timeout error
|
||||||
|
statusBar.innerHTML = `<span class="text-xs font-mono" style="color:#ff5252">Ошибка: ${escHtml(data.error)}</span><span class="text-xs font-mono text-muted ml-auto">${data.time || elapsed}мс</span>`;
|
||||||
|
showBody('❌ ' + data.error, 'plaintext');
|
||||||
|
addToHistory(req.method, req.url, null, data.time || elapsed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status badge
|
||||||
|
const statusClass = data.status >= 500 ? 'status-5xx' : data.status >= 400 ? 'status-4xx' : data.status >= 300 ? 'status-3xx' : 'status-2xx';
|
||||||
|
const sizeStr = formatSize(data.size);
|
||||||
|
statusBar.innerHTML = `
|
||||||
|
<span class="text-sm font-mono font-bold ${statusClass}">${data.status} ${escHtml(data.statusText || '')}</span>
|
||||||
|
<span class="text-xs font-mono text-muted">${data.time}мс</span>
|
||||||
|
<span class="text-xs font-mono text-muted">${sizeStr}</span>
|
||||||
|
${data.truncated ? '<span class="text-xs font-mono" style="color:#ffa500">⚠ обрезано до 10MB</span>' : ''}
|
||||||
|
${data.url !== req.url ? '<span class="text-xs font-mono text-muted ml-auto" title="' + escHtml(data.url) + '">→ redirect</span>' : ''}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Response body
|
||||||
|
const ct = (data.headers?.['content-type'] || '').toLowerCase();
|
||||||
|
let lang = 'plaintext';
|
||||||
|
let bodyText = data.body || '';
|
||||||
|
if (ct.includes('json')) {
|
||||||
|
lang = 'json';
|
||||||
|
try { bodyText = JSON.stringify(JSON.parse(bodyText), null, 2); } catch {}
|
||||||
|
} else if (ct.includes('html')) {
|
||||||
|
lang = 'xml';
|
||||||
|
} else if (ct.includes('xml')) {
|
||||||
|
lang = 'xml';
|
||||||
|
} else if (ct.includes('javascript')) {
|
||||||
|
lang = 'javascript';
|
||||||
|
}
|
||||||
|
showBody(bodyText, lang);
|
||||||
|
|
||||||
|
// Response headers
|
||||||
|
const hContent = document.getElementById('responseHeadersContent');
|
||||||
|
hContent.innerHTML = '';
|
||||||
|
if (data.headers) {
|
||||||
|
Object.entries(data.headers).sort().forEach(([k, v]) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'flex gap-2 py-0.5';
|
||||||
|
row.innerHTML = `<span style="color:#6b9fff;min-width:160px">${escHtml(k)}</span><span style="color:var(--text-secondary)">${escHtml(v)}</span>`;
|
||||||
|
hContent.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
const copyBar = document.getElementById('responseCopyBar');
|
||||||
|
copyBar.classList.remove('hidden');
|
||||||
|
document.getElementById('responseSizeLabel').textContent = sizeStr;
|
||||||
|
document.getElementById('copyBodyBtn').onclick = () => {
|
||||||
|
navigator.clipboard.writeText(data.body || '').then(() => {
|
||||||
|
document.getElementById('copyBodyBtn').textContent = 'Скопировано!';
|
||||||
|
setTimeout(() => document.getElementById('copyBodyBtn').textContent = 'Копировать', 1500);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
addToHistory(req.method, req.url, data.status, data.time);
|
||||||
|
} catch (err) {
|
||||||
|
statusBar.innerHTML = `<span class="text-xs font-mono" style="color:#ff5252">Ошибка соединения: ${escHtml(err.message)}</span>`;
|
||||||
|
showBody('Ошибка: ' + err.message, 'plaintext');
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
sendBtn.disabled = false;
|
||||||
|
sendBtn.innerHTML = '<svg width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg> Отправить';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBody(text, lang) {
|
||||||
|
const empty = document.getElementById('responseBodyEmpty');
|
||||||
|
const pre = document.getElementById('responseBodyPre');
|
||||||
|
const code = document.getElementById('responseBodyCode');
|
||||||
|
empty.classList.add('hidden');
|
||||||
|
pre.classList.remove('hidden');
|
||||||
|
code.textContent = text;
|
||||||
|
code.className = 'language-' + lang;
|
||||||
|
if (window.hljs && lang !== 'plaintext') {
|
||||||
|
hljs.highlightElement(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatSize(bytes) {
|
||||||
|
if (!bytes) return '0 B';
|
||||||
|
if (bytes < 1024) return bytes + ' B';
|
||||||
|
if (bytes < 1024*1024) return (bytes/1024).toFixed(1) + ' KB';
|
||||||
|
return (bytes/(1024*1024)).toFixed(1) + ' MB';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send button + Enter
|
||||||
|
document.getElementById('sendBtn').onclick = sendRequest;
|
||||||
|
document.getElementById('urlInput').addEventListener('keydown', e => {
|
||||||
|
if (e.key === 'Enter') sendRequest();
|
||||||
|
});
|
||||||
|
|
||||||
|
// History (server-side, per session)
|
||||||
|
const methodColors = { GET:'#00a86b', POST:'#ffa500', PUT:'#6b9fff', PATCH:'#cc66ff', DELETE:'#ff5252', HEAD:'#aaa', OPTIONS:'#aaa' };
|
||||||
|
|
||||||
|
async function addToHistory(method, url, status, time) {
|
||||||
|
await fetch('/api/history/add', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ method, url, status, time, ts: Date.now() }),
|
||||||
|
}).catch(() => {});
|
||||||
|
renderHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderHistory() {
|
||||||
|
const list = document.getElementById('historyList');
|
||||||
|
const clearBtn = document.getElementById('clearHistoryBtn');
|
||||||
|
let h = [];
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/history');
|
||||||
|
h = await r.json();
|
||||||
|
} catch {}
|
||||||
|
if (!Array.isArray(h) || h.length === 0) {
|
||||||
|
list.innerHTML = '<div class="text-xs font-mono text-muted py-4 text-center">История пуста</div>';
|
||||||
|
clearBtn.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearBtn.classList.remove('hidden');
|
||||||
|
list.innerHTML = '';
|
||||||
|
h.forEach((item) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'history-item';
|
||||||
|
const statusClass = !item.status ? 'text-muted' : item.status >= 500 ? 'status-5xx' : item.status >= 400 ? 'status-4xx' : item.status >= 300 ? 'status-3xx' : 'status-2xx';
|
||||||
|
const statusText = item.status ? item.status : '—';
|
||||||
|
div.innerHTML = `
|
||||||
|
<span class="method-badge m-${item.method}" style="min-width:56px;justify-content:center">${item.method}</span>
|
||||||
|
<span class="history-url">${escHtml(item.url)}</span>
|
||||||
|
<span class="history-meta ${statusClass}">${statusText}</span>
|
||||||
|
<span class="history-meta">${item.time}мс</span>
|
||||||
|
`;
|
||||||
|
div.onclick = () => loadHistoryItem(item);
|
||||||
|
list.appendChild(div);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistoryItem(item) {
|
||||||
|
document.getElementById('method').value = item.method;
|
||||||
|
document.getElementById('urlInput').value = item.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('clearHistoryBtn').onclick = async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await fetch('/api/history', { method: 'DELETE' }).catch(() => {});
|
||||||
|
renderHistory();
|
||||||
|
};
|
||||||
|
|
||||||
|
// History toggle
|
||||||
|
let historyOpen = true;
|
||||||
|
document.getElementById('historyToggle').onclick = () => {
|
||||||
|
historyOpen = !historyOpen;
|
||||||
|
document.getElementById('historyList').style.display = historyOpen ? '' : 'none';
|
||||||
|
document.getElementById('historyArrow').style.transform = historyOpen ? '' : 'rotate(-90deg)';
|
||||||
|
};
|
||||||
|
|
||||||
|
renderHistory();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
355
public/index.html
Normal file
355
public/index.html
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>WA Dev Tools — Инструменты</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
|
||||||
|
/* Mobile bottom bar */
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme variables */
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f7;
|
||||||
|
--bg-grad1: rgba(0, 67, 184, 0.04);
|
||||||
|
--bg-grad2: rgba(0, 50, 140, 0.03);
|
||||||
|
--surface-800: rgba(255,255,255,0.85);
|
||||||
|
--surface-700: #f0f0f3;
|
||||||
|
--surface-600: #dde0e6;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555;
|
||||||
|
--text-muted: #888;
|
||||||
|
--bar-bg: #e0e0e0;
|
||||||
|
--select-bg: #f0f0f3;
|
||||||
|
--select-color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--bg: #060c18;
|
||||||
|
--bg-grad1: rgba(0, 84, 230, 0.03);
|
||||||
|
--bg-grad2: rgba(0, 67, 184, 0.02);
|
||||||
|
--surface-800: rgba(9,16,32,0.8);
|
||||||
|
--surface-700: #0d1828;
|
||||||
|
--surface-600: #162040;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #c0c0c0;
|
||||||
|
--text-muted: #666;
|
||||||
|
--bar-bg: #0d1828;
|
||||||
|
--select-bg: #091020;
|
||||||
|
--select-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.dark .noise::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme toggle */
|
||||||
|
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.dark .theme-toggle { background: #162040; }
|
||||||
|
.theme-toggle { background: #dde0e6; }
|
||||||
|
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
|
||||||
|
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
|
||||||
|
|
||||||
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
||||||
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
||||||
|
|
||||||
|
/* Dashboard tiles */
|
||||||
|
.tool-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 16px;
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
background: var(--surface-700);
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all .2s;
|
||||||
|
}
|
||||||
|
.tool-tile:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: rgba(0,84,230,.3);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,.3);
|
||||||
|
background: var(--surface-800);
|
||||||
|
}
|
||||||
|
.tile-icon {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(0,84,230,.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #0054e6;
|
||||||
|
transition: background .2s;
|
||||||
|
}
|
||||||
|
.tool-tile:hover .tile-icon { background: rgba(0,84,230,.15); }
|
||||||
|
.tile-title { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
|
||||||
|
.tile-desc { font-size: 12px; color: var(--text-muted); font-family: 'JetBrains Mono', monospace; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link active" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-5xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-10 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
images.wadevelop.ru
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
WA Dev <span class="text-accent">Tools</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">Инструменты разработчика</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tools Grid -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 fade-up-delay">
|
||||||
|
|
||||||
|
<!-- Сжать картинки -->
|
||||||
|
<a href="/compress" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Сжать картинки</div>
|
||||||
|
<div class="tile-desc">JPEG, PNG, WebP до 20МБ</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Markdown -->
|
||||||
|
<a href="/md" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Markdown</div>
|
||||||
|
<div class="tile-desc">Просмотр Markdown файлов</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Placeholder -->
|
||||||
|
<a href="/placeholder" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Placeholder</div>
|
||||||
|
<div class="tile-desc">Генератор заглушек для изображений</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- URL Parser -->
|
||||||
|
<a href="/parser" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">URL Parser</div>
|
||||||
|
<div class="tile-desc">Извлечение контента из URL</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Пароли -->
|
||||||
|
<a href="/password" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Пароли</div>
|
||||||
|
<div class="tile-desc">Генератор надёжных паролей</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Sanitizer -->
|
||||||
|
<a href="/sanitizer" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Sanitizer</div>
|
||||||
|
<div class="tile-desc">Очистка HTML от мусора</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Конвертер -->
|
||||||
|
<a href="/converter" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Конвертер</div>
|
||||||
|
<div class="tile-desc">Конвертация форматов данных</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Форматтер -->
|
||||||
|
<a href="/formatter" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Форматтер</div>
|
||||||
|
<div class="tile-desc">Форматирование кода</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Редактор -->
|
||||||
|
<a href="/editor" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Редактор</div>
|
||||||
|
<div class="tile-desc">Фоторедактор в браузере</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- HTTP Client -->
|
||||||
|
<a href="/httpclient" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">HTTP Client</div>
|
||||||
|
<div class="tile-desc">Тест API запросов — Postman lite</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Redirect Analyzer -->
|
||||||
|
<a href="/redirects" class="tool-tile group">
|
||||||
|
<div class="tile-icon">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="tile-title">Redirect Analyzer</div>
|
||||||
|
<div class="tile-desc">Анализ цепочки HTTP-редиректов</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme toggle
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
401
public/md.html
Normal file
401
public/md.html
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Markdown Viewer</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script src="/vendor/marked.min.js"></script>
|
||||||
|
<script src="/vendor/highlight.min.js"></script>
|
||||||
|
<link id="hljsTheme" href="/vendor/hljs-github-dark.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme variables */
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f7;
|
||||||
|
--bg-grad1: rgba(0, 67, 184, 0.04);
|
||||||
|
--bg-grad2: rgba(0, 50, 140, 0.03);
|
||||||
|
--surface-800: rgba(255,255,255,0.85);
|
||||||
|
--surface-700: #f0f0f3;
|
||||||
|
--surface-600: #dde0e6;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555;
|
||||||
|
--text-muted: #888;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--bg: #060c18;
|
||||||
|
--bg-grad1: rgba(0, 84, 230, 0.03);
|
||||||
|
--bg-grad2: rgba(0, 67, 184, 0.02);
|
||||||
|
--surface-800: rgba(9,16,32,0.8);
|
||||||
|
--surface-700: #0d1828;
|
||||||
|
--surface-600: #162040;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #c0c0c0;
|
||||||
|
--text-muted: #666;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.dark .noise::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme toggle */
|
||||||
|
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.dark .theme-toggle { background: #162040; }
|
||||||
|
.theme-toggle { background: #dde0e6; }
|
||||||
|
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
|
||||||
|
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
|
||||||
|
|
||||||
|
/* Dropzone */
|
||||||
|
.md-dropzone { border: 2px dashed var(--surface-600); transition: all .3s; }
|
||||||
|
.md-dropzone.active { border-color: #0054e6; background: rgba(0,84,230,.05); }
|
||||||
|
|
||||||
|
/* Markdown rendered output */
|
||||||
|
.md-output {
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.75;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
.md-output h1 { font-size: 2em; font-weight: 800; margin: 1.2em 0 .6em; padding-bottom: .3em; border-bottom: 1px solid var(--surface-600); }
|
||||||
|
.md-output h2 { font-size: 1.5em; font-weight: 700; margin: 1em 0 .5em; padding-bottom: .25em; border-bottom: 1px solid var(--surface-600); }
|
||||||
|
.md-output h3 { font-size: 1.25em; font-weight: 600; margin: .9em 0 .4em; }
|
||||||
|
.md-output h4 { font-size: 1.1em; font-weight: 600; margin: .8em 0 .3em; }
|
||||||
|
.md-output p { margin: .6em 0; }
|
||||||
|
.md-output ul, .md-output ol { margin: .6em 0; padding-left: 1.8em; }
|
||||||
|
.md-output li { margin: .25em 0; }
|
||||||
|
.md-output ul li { list-style: disc; }
|
||||||
|
.md-output ol li { list-style: decimal; }
|
||||||
|
.md-output a { color: #0054e6; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
|
.md-output blockquote { border-left: 3px solid #0054e6; padding: .5em 1em; margin: .8em 0; color: var(--text-secondary); background: rgba(0,84,230,.03); border-radius: 0 8px 8px 0; }
|
||||||
|
.md-output code { font-family: 'JetBrains Mono', monospace; font-size: .88em; padding: .15em .4em; border-radius: 4px; background: var(--surface-700); }
|
||||||
|
.md-output pre { margin: .8em 0; border-radius: 10px; overflow-x: auto; }
|
||||||
|
.md-output pre code { display: block; padding: 1em 1.2em; background: var(--surface-700); font-size: .85em; line-height: 1.6; border-radius: 10px; }
|
||||||
|
.md-output table { border-collapse: collapse; width: 100%; margin: .8em 0; font-size: .92em; }
|
||||||
|
.md-output th, .md-output td { border: 1px solid var(--surface-600); padding: .5em .8em; text-align: left; }
|
||||||
|
.md-output th { background: var(--surface-700); font-weight: 600; }
|
||||||
|
.md-output hr { border: none; border-top: 1px solid var(--surface-600); margin: 1.5em 0; }
|
||||||
|
.md-output img { max-width: 100%; border-radius: 8px; margin: .8em 0; }
|
||||||
|
|
||||||
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
||||||
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tab-btn { transition: all .2s; }
|
||||||
|
.tab-btn.active { color: #0054e6; border-color: #0054e6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link active" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-4xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
MD Viewer
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Просмотр <span class="text-accent">Markdown</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">drag-drop файл или вставьте текст</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex gap-2 mb-5 border-b dark:border-surface-600 border-gray-200 pb-3">
|
||||||
|
<button class="tab-btn active text-sm font-medium px-3 py-1.5 border-b-2 border-transparent dark:text-gray-400 text-gray-500" data-tab="file">
|
||||||
|
Файл
|
||||||
|
</button>
|
||||||
|
<button class="tab-btn text-sm font-medium px-3 py-1.5 border-b-2 border-transparent dark:text-gray-400 text-gray-500" data-tab="text">
|
||||||
|
Текст
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File tab -->
|
||||||
|
<div id="tabFile" class="tab-content">
|
||||||
|
<div id="dropzone" class="md-dropzone rounded-xl p-8 sm:p-10 text-center cursor-pointer group">
|
||||||
|
<svg class="mx-auto w-10 h-10 dark:text-gray-500 text-gray-400 group-hover:text-accent transition-colors mb-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />
|
||||||
|
</svg>
|
||||||
|
<p class="text-sm dark:text-gray-400 text-gray-500">
|
||||||
|
Перетащите <span class="text-accent font-medium">.md</span> файл сюда или <span class="text-accent font-medium cursor-pointer">выберите</span>
|
||||||
|
</p>
|
||||||
|
<p id="fileName" class="mt-2 text-xs font-mono dark:text-gray-600 text-gray-400 hidden"></p>
|
||||||
|
<input type="file" accept=".md,.markdown,.txt" id="fileInput" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer" style="position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text tab -->
|
||||||
|
<div id="tabText" class="tab-content hidden">
|
||||||
|
<textarea id="mdInput" class="w-full h-48 px-4 py-3 rounded-xl text-sm font-mono dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none focus:border-accent/50 resize-y transition-colors" placeholder="Вставьте Markdown текст..."></textarea>
|
||||||
|
<button id="renderBtn" class="mt-3 px-5 py-2.5 rounded-xl text-sm font-semibold bg-accent text-white hover:bg-accent-bright transition-all active:scale-[0.98]">
|
||||||
|
Рендер
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Output -->
|
||||||
|
<div id="outputCard" class="hidden mt-5 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="flex items-center justify-between mb-4 pb-3 border-b dark:border-surface-600 border-gray-200">
|
||||||
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Результат</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="downloadHtmlBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||||||
|
Скачать HTML
|
||||||
|
</button>
|
||||||
|
<button id="copyHtmlBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200">
|
||||||
|
Скопировать HTML
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Rendered markdown -->
|
||||||
|
<div id="mdOutput" class="md-output"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
|
||||||
|
function applyHljsTheme() {
|
||||||
|
const isDark = html.classList.contains('dark');
|
||||||
|
document.getElementById('hljsTheme').href = isDark ? '/vendor/hljs-github-dark.css' : '/vendor/hljs-github.css';
|
||||||
|
}
|
||||||
|
applyHljsTheme();
|
||||||
|
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
applyHljsTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const tab = btn.dataset.tab;
|
||||||
|
document.getElementById('tabFile').classList.toggle('hidden', tab !== 'file');
|
||||||
|
document.getElementById('tabText').classList.toggle('hidden', tab !== 'text');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Marked config
|
||||||
|
marked.setOptions({
|
||||||
|
highlight: function(code, lang) {
|
||||||
|
if (lang && hljs.getLanguage(lang)) {
|
||||||
|
return hljs.highlight(code, { language: lang }).value;
|
||||||
|
}
|
||||||
|
return hljs.highlightAuto(code).value;
|
||||||
|
},
|
||||||
|
breaks: true,
|
||||||
|
gfm: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderMarkdown(md) {
|
||||||
|
const output = document.getElementById('mdOutput');
|
||||||
|
const card = document.getElementById('outputCard');
|
||||||
|
output.innerHTML = marked.parse(md);
|
||||||
|
card.classList.remove('hidden');
|
||||||
|
// Re-highlight any code blocks
|
||||||
|
output.querySelectorAll('pre code').forEach(block => {
|
||||||
|
hljs.highlightElement(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// File drop
|
||||||
|
const dropzone = document.getElementById('dropzone');
|
||||||
|
const fileInput = document.getElementById('fileInput');
|
||||||
|
|
||||||
|
// Make dropzone relative for absolute file input
|
||||||
|
dropzone.style.position = 'relative';
|
||||||
|
|
||||||
|
dropzone.addEventListener('dragover', e => { e.preventDefault(); dropzone.classList.add('active'); });
|
||||||
|
dropzone.addEventListener('dragleave', () => dropzone.classList.remove('active'));
|
||||||
|
dropzone.addEventListener('drop', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
dropzone.classList.remove('active');
|
||||||
|
const file = e.dataTransfer.files[0];
|
||||||
|
if (file) readFile(file);
|
||||||
|
});
|
||||||
|
fileInput.addEventListener('change', e => {
|
||||||
|
if (e.target.files[0]) readFile(e.target.files[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
function readFile(file) {
|
||||||
|
const nameEl = document.getElementById('fileName');
|
||||||
|
nameEl.textContent = file.name;
|
||||||
|
nameEl.classList.remove('hidden');
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = e => renderMarkdown(e.target.result);
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text render
|
||||||
|
document.getElementById('renderBtn').addEventListener('click', () => {
|
||||||
|
const text = document.getElementById('mdInput').value.trim();
|
||||||
|
if (text) renderMarkdown(text);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also render on Ctrl+Enter in textarea
|
||||||
|
document.getElementById('mdInput').addEventListener('keydown', e => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
document.getElementById('renderBtn').click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy HTML
|
||||||
|
document.getElementById('copyHtmlBtn').addEventListener('click', () => {
|
||||||
|
const html = document.getElementById('mdOutput').innerHTML;
|
||||||
|
navigator.clipboard.writeText(html).then(() => {
|
||||||
|
const btn = document.getElementById('copyHtmlBtn');
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = 'Скопировано!';
|
||||||
|
btn.classList.add('text-accent');
|
||||||
|
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download HTML
|
||||||
|
document.getElementById('downloadHtmlBtn').addEventListener('click', () => {
|
||||||
|
const body = document.getElementById('mdOutput').innerHTML;
|
||||||
|
const fullHtml = '<!DOCTYPE html>\n<html>\n<head>\n<meta charset="UTF-8">\n<title>Markdown</title>\n<style>\nbody{font-family:sans-serif;max-width:800px;margin:40px auto;padding:0 20px;line-height:1.7;color:#333}\npre{background:#f5f5f5;padding:1em;border-radius:6px;overflow-x:auto}\ncode{font-family:monospace;font-size:.9em;background:#f0f0f0;padding:2px 5px;border-radius:3px}\npre code{background:none;padding:0}\ntable{border-collapse:collapse;width:100%}\nth,td{border:1px solid #ddd;padding:8px;text-align:left}\nth{background:#f5f5f5}\nblockquote{border-left:3px solid #0054e6;padding:.5em 1em;margin:1em 0;color:#666;background:#f9f9f9}\nimg{max-width:100%}\n</style>\n</head>\n<body>\n' + body + '\n</body>\n</html>';
|
||||||
|
const blob = new Blob([fullHtml], { type: 'text/html' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'markdown.html';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
document.getElementById('clearBtn').addEventListener('click', () => {
|
||||||
|
document.getElementById('mdOutput').innerHTML = '';
|
||||||
|
document.getElementById('outputCard').classList.add('hidden');
|
||||||
|
document.getElementById('mdInput').value = '';
|
||||||
|
document.getElementById('fileName').classList.add('hidden');
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
564
public/parser.html
Normal file
564
public/parser.html
Normal file
@ -0,0 +1,564 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>URL Parser — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f7;
|
||||||
|
--bg-grad1: rgba(0, 67, 184, 0.04);
|
||||||
|
--bg-grad2: rgba(0, 50, 140, 0.03);
|
||||||
|
--surface-800: rgba(255,255,255,0.85);
|
||||||
|
--surface-700: #f0f0f3;
|
||||||
|
--surface-600: #dde0e6;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555;
|
||||||
|
--text-muted: #888;
|
||||||
|
--select-bg: #f0f0f3;
|
||||||
|
--select-color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--bg: #060c18;
|
||||||
|
--bg-grad1: rgba(0, 84, 230, 0.03);
|
||||||
|
--bg-grad2: rgba(0, 67, 184, 0.02);
|
||||||
|
--surface-800: rgba(9,16,32,0.8);
|
||||||
|
--surface-700: #0d1828;
|
||||||
|
--surface-600: #162040;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #c0c0c0;
|
||||||
|
--text-muted: #666;
|
||||||
|
--select-bg: #091020;
|
||||||
|
--select-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.dark .noise::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.dark .theme-toggle { background: #162040; }
|
||||||
|
.theme-toggle { background: #dde0e6; }
|
||||||
|
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
|
||||||
|
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
|
||||||
|
|
||||||
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
||||||
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
||||||
|
@keyframes pulse-border { 0%, 100% { border-color: rgba(0, 84, 230, 0.2); } 50% { border-color: rgba(0, 84, 230, 0.5); } }
|
||||||
|
.processing { animation: pulse-border 1.5s ease-in-out infinite; }
|
||||||
|
|
||||||
|
.article-content { color: var(--text-primary); line-height: 1.75; font-size: 15px; }
|
||||||
|
.article-content h1 { font-size: 1.8em; font-weight: 800; margin: 1em 0 .5em; }
|
||||||
|
.article-content h2 { font-size: 1.4em; font-weight: 700; margin: .9em 0 .4em; }
|
||||||
|
.article-content h3 { font-size: 1.2em; font-weight: 600; margin: .8em 0 .3em; }
|
||||||
|
.article-content p { margin: .6em 0; }
|
||||||
|
.article-content ul, .article-content ol { margin: .6em 0; padding-left: 1.8em; }
|
||||||
|
.article-content li { margin: .25em 0; }
|
||||||
|
.article-content ul li { list-style: disc; }
|
||||||
|
.article-content ol li { list-style: decimal; }
|
||||||
|
.article-content a { color: #0054e6; text-decoration: underline; text-underline-offset: 2px; }
|
||||||
|
.article-content blockquote { border-left: 3px solid #0054e6; padding: .5em 1em; margin: .8em 0; color: var(--text-secondary); background: rgba(0,84,230,.03); border-radius: 0 8px 8px 0; }
|
||||||
|
.article-content code { font-family: 'JetBrains Mono', monospace; font-size: .88em; padding: .15em .4em; border-radius: 4px; background: var(--surface-700); }
|
||||||
|
.article-content pre { margin: .8em 0; border-radius: 10px; overflow-x: auto; }
|
||||||
|
.article-content pre code { display: block; padding: 1em 1.2em; background: var(--surface-700); }
|
||||||
|
.article-content img { max-width: 100%; border-radius: 8px; margin: .8em 0; }
|
||||||
|
.article-content table { border-collapse: collapse; width: 100%; margin: .8em 0; }
|
||||||
|
.article-content th, .article-content td { border: 1px solid var(--surface-600); padding: .5em .8em; text-align: left; }
|
||||||
|
.article-content th { background: var(--surface-700); font-weight: 600; }
|
||||||
|
|
||||||
|
.mode-btn { transition: all .2s; }
|
||||||
|
.mode-btn.active { color: #0054e6; background: rgba(0,84,230,.08); border-color: rgba(0,84,230,.3); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link active" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-3xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
URL Article Parser
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Парсер <span class="text-accent">статей</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">извлечение контента из любой веб-страницы</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input Card -->
|
||||||
|
<div id="inputCard" class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="url" id="urlInput" placeholder="https://example.com/article"
|
||||||
|
class="flex-1 px-4 py-3 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-xl text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800"
|
||||||
|
>
|
||||||
|
<button id="parseBtn"
|
||||||
|
class="px-6 py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent text-white border border-accent cursor-pointer
|
||||||
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20
|
||||||
|
active:scale-[0.98] whitespace-nowrap">
|
||||||
|
Парсить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mode buttons -->
|
||||||
|
<div class="flex gap-2 mt-4">
|
||||||
|
<button class="mode-btn active flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="full">
|
||||||
|
Полный
|
||||||
|
</button>
|
||||||
|
<button class="mode-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="metadata">
|
||||||
|
Метаданные
|
||||||
|
</button>
|
||||||
|
<button class="mode-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="text">
|
||||||
|
Текст
|
||||||
|
</button>
|
||||||
|
<button class="mode-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors" data-mode="preview">
|
||||||
|
Превью
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error -->
|
||||||
|
<div id="errorBlock" class="hidden mt-5 bg-danger/10 border border-danger/20 rounded-xl px-5 py-4 text-sm text-danger font-medium fade-up"></div>
|
||||||
|
|
||||||
|
<!-- Loading -->
|
||||||
|
<div id="loadingBlock" class="hidden mt-5">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-8 backdrop-blur-sm shadow-sm dark:shadow-none text-center processing">
|
||||||
|
<div class="inline-flex items-center gap-3">
|
||||||
|
<svg class="animate-spin w-5 h-5 text-accent" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"/></svg>
|
||||||
|
<span class="text-sm dark:text-gray-400 text-gray-500 font-mono">Загружаю и парсю страницу...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result: Full article -->
|
||||||
|
<div id="resultFull" class="hidden mt-5 fade-up space-y-4">
|
||||||
|
<!-- Preview card -->
|
||||||
|
<div id="previewCard" class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl overflow-hidden backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<img id="leadImage" class="w-full h-48 object-cover hidden" alt="">
|
||||||
|
<div class="p-5 sm:p-7">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<span id="siteName" class="text-xs font-mono dark:text-gray-500 text-gray-400"></span>
|
||||||
|
<span id="articleDate" class="text-xs font-mono dark:text-gray-500 text-gray-400"></span>
|
||||||
|
</div>
|
||||||
|
<h2 id="articleTitle" class="text-xl font-bold dark:text-white text-gray-900 mb-2"></h2>
|
||||||
|
<p id="articleAuthor" class="text-sm dark:text-gray-500 text-gray-400 hidden"></p>
|
||||||
|
<p id="articleExcerpt" class="text-sm dark:text-gray-400 text-gray-500 mt-2 leading-relaxed"></p>
|
||||||
|
<div class="flex items-center gap-3 mt-3">
|
||||||
|
<span id="wordCount" class="text-xs font-mono dark:text-gray-600 text-gray-400"></span>
|
||||||
|
<span id="articleLang" class="text-xs font-mono dark:text-gray-600 text-gray-400"></span>
|
||||||
|
<span id="imageCount" class="text-xs font-mono dark:text-gray-600 text-gray-400"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action buttons -->
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
<button id="copyJsonBtn" class="px-4 py-2 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||||||
|
Скопировать JSON
|
||||||
|
</button>
|
||||||
|
<button id="copyTextBtn" class="px-4 py-2 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200">
|
||||||
|
Скопировать текст
|
||||||
|
</button>
|
||||||
|
<button id="copyHtmlBtn" class="px-4 py-2 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200">
|
||||||
|
Скопировать HTML
|
||||||
|
</button>
|
||||||
|
<button id="clearBtn" class="ml-auto px-4 py-2 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Article content -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<div class="flex items-center justify-between mb-4 pb-3 border-b dark:border-surface-600 border-gray-200">
|
||||||
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Контент</span>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button id="viewHtmlBtn" class="mode-btn active px-3 py-1 rounded text-xs font-medium dark:text-gray-400 text-gray-500 border dark:border-surface-600 border-gray-200" data-view="html">HTML</button>
|
||||||
|
<button id="viewTextBtn" class="mode-btn px-3 py-1 rounded text-xs font-medium dark:text-gray-400 text-gray-500 border dark:border-surface-600 border-gray-200" data-view="text">Text</button>
|
||||||
|
<button id="viewJsonBtn" class="mode-btn px-3 py-1 rounded text-xs font-medium dark:text-gray-400 text-gray-500 border dark:border-surface-600 border-gray-200" data-view="json">JSON</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="contentHtml" class="article-content"></div>
|
||||||
|
<pre id="contentText" class="hidden whitespace-pre-wrap text-sm font-mono dark:text-gray-300 text-gray-600 leading-relaxed max-h-[600px] overflow-y-auto"></pre>
|
||||||
|
<pre id="contentJson" class="hidden whitespace-pre-wrap text-xs font-mono dark:text-gray-300 text-gray-600 leading-relaxed max-h-[600px] overflow-y-auto dark:bg-surface-700 bg-gray-50 p-4 rounded-xl"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result: Metadata only -->
|
||||||
|
<div id="resultMetadata" class="hidden mt-5 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<div class="flex items-center justify-between mb-4 pb-3 border-b dark:border-surface-600 border-gray-200">
|
||||||
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Метаданные</span>
|
||||||
|
<button id="copyMetaJsonBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||||||
|
Скопировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre id="metadataJson" class="whitespace-pre-wrap text-xs font-mono dark:text-gray-300 text-gray-600 leading-relaxed dark:bg-surface-700 bg-gray-50 p-4 rounded-xl"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result: Text only -->
|
||||||
|
<div id="resultText" class="hidden mt-5 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<div class="flex items-center justify-between mb-4 pb-3 border-b dark:border-surface-600 border-gray-200">
|
||||||
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Текст</span>
|
||||||
|
<button id="copyPlainTextBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||||||
|
Скопировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre id="plainText" class="whitespace-pre-wrap text-sm dark:text-gray-300 text-gray-600 leading-relaxed max-h-[600px] overflow-y-auto"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result: Preview -->
|
||||||
|
<div id="resultPreview" class="hidden mt-5 fade-up">
|
||||||
|
<div id="previewLinkCard" class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl overflow-hidden backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<img id="previewImg" class="w-full h-52 object-cover hidden" alt="">
|
||||||
|
<div class="p-5 sm:p-7">
|
||||||
|
<p id="previewSite" class="text-xs font-mono dark:text-gray-500 text-gray-400 mb-1"></p>
|
||||||
|
<h3 id="previewTitle" class="text-lg font-bold dark:text-white text-gray-900 mb-2"></h3>
|
||||||
|
<p id="previewDesc" class="text-sm dark:text-gray-400 text-gray-500 leading-relaxed"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="copyPreviewJsonBtn" class="mt-3 px-4 py-2 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||||||
|
Скопировать JSON
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Reference -->
|
||||||
|
<div class="mt-8 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<h3 class="text-sm font-semibold dark:text-white text-gray-800 mb-3">API</h3>
|
||||||
|
<div class="space-y-2 text-xs font-mono dark:text-gray-400 text-gray-500 leading-relaxed">
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/parse?url=URL</code> — полный парсинг (title, content, images, links)</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/metadata?url=URL</code> — только метаданные</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/text?url=URL</code> — только plain text</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/preview?url=URL</code> — превью-карточка (title + image + description)</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/health</code> — проверка доступности</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
let currentMode = 'full';
|
||||||
|
let lastResult = null;
|
||||||
|
let lastMetadata = null;
|
||||||
|
let lastText = null;
|
||||||
|
let lastPreview = null;
|
||||||
|
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
// Mode buttons
|
||||||
|
document.querySelectorAll('.mode-btn[data-mode]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.mode-btn[data-mode]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
currentMode = btn.dataset.mode;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// View toggle (HTML/Text/JSON)
|
||||||
|
document.querySelectorAll('.mode-btn[data-view]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.mode-btn[data-view]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
const view = btn.dataset.view;
|
||||||
|
$('contentHtml').classList.toggle('hidden', view !== 'html');
|
||||||
|
$('contentText').classList.toggle('hidden', view !== 'text');
|
||||||
|
$('contentJson').classList.toggle('hidden', view !== 'json');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
$('parseBtn').addEventListener('click', doParse);
|
||||||
|
$('urlInput').addEventListener('keydown', e => { if (e.key === 'Enter') doParse(); });
|
||||||
|
|
||||||
|
async function doParse() {
|
||||||
|
const url = $('urlInput').value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
hideAll();
|
||||||
|
$('loadingBlock').classList.remove('hidden');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (currentMode === 'full') {
|
||||||
|
const res = await fetch('/parse?url=' + encodeURIComponent(url));
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Ошибка');
|
||||||
|
lastResult = data;
|
||||||
|
showFullResult(data);
|
||||||
|
} else if (currentMode === 'metadata') {
|
||||||
|
const res = await fetch('/metadata?url=' + encodeURIComponent(url));
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Ошибка');
|
||||||
|
lastMetadata = data;
|
||||||
|
showMetadata(data);
|
||||||
|
} else if (currentMode === 'text') {
|
||||||
|
const res = await fetch('/text?url=' + encodeURIComponent(url));
|
||||||
|
if (!res.ok) {
|
||||||
|
const data = await res.json();
|
||||||
|
throw new Error(data.error || 'Ошибка');
|
||||||
|
}
|
||||||
|
const text = await res.text();
|
||||||
|
lastText = text;
|
||||||
|
showText(text);
|
||||||
|
} else if (currentMode === 'preview') {
|
||||||
|
const res = await fetch('/preview?url=' + encodeURIComponent(url));
|
||||||
|
const data = await res.json();
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Ошибка');
|
||||||
|
lastPreview = data;
|
||||||
|
showPreview(data);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
$('errorBlock').textContent = err.message;
|
||||||
|
$('errorBlock').classList.remove('hidden');
|
||||||
|
} finally {
|
||||||
|
$('loadingBlock').classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAll() {
|
||||||
|
$('errorBlock').classList.add('hidden');
|
||||||
|
$('resultFull').classList.add('hidden');
|
||||||
|
$('resultMetadata').classList.add('hidden');
|
||||||
|
$('resultText').classList.add('hidden');
|
||||||
|
$('resultPreview').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFullResult(data) {
|
||||||
|
// Lead image
|
||||||
|
if (data.lead_image) {
|
||||||
|
$('leadImage').src = data.lead_image;
|
||||||
|
$('leadImage').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
$('leadImage').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('articleTitle').textContent = data.title || 'Без заголовка';
|
||||||
|
$('siteName').textContent = data.site || '';
|
||||||
|
$('articleDate').textContent = data.date_published ? new Date(data.date_published).toLocaleDateString('ru') : '';
|
||||||
|
|
||||||
|
if (data.author) {
|
||||||
|
$('articleAuthor').textContent = data.author;
|
||||||
|
$('articleAuthor').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
$('articleAuthor').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('articleExcerpt').textContent = data.excerpt || '';
|
||||||
|
$('wordCount').textContent = data.word_count ? data.word_count + ' слов' : '';
|
||||||
|
$('articleLang').textContent = data.lang ? 'lang: ' + data.lang : '';
|
||||||
|
$('imageCount').textContent = data.images ? data.images.length + ' изобр.' : '';
|
||||||
|
|
||||||
|
// Content views
|
||||||
|
$('contentHtml').innerHTML = data.content_html || '<p class="dark:text-gray-500 text-gray-400">Контент не извлечён</p>';
|
||||||
|
$('contentText').textContent = data.content_text || '';
|
||||||
|
$('contentJson').textContent = JSON.stringify(data, null, 2);
|
||||||
|
|
||||||
|
// Reset view to HTML
|
||||||
|
document.querySelectorAll('.mode-btn[data-view]').forEach(b => b.classList.remove('active'));
|
||||||
|
$('viewHtmlBtn').classList.add('active');
|
||||||
|
$('contentHtml').classList.remove('hidden');
|
||||||
|
$('contentText').classList.add('hidden');
|
||||||
|
$('contentJson').classList.add('hidden');
|
||||||
|
|
||||||
|
$('resultFull').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showMetadata(data) {
|
||||||
|
$('metadataJson').textContent = JSON.stringify(data, null, 2);
|
||||||
|
$('resultMetadata').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showText(text) {
|
||||||
|
$('plainText').textContent = text;
|
||||||
|
$('resultText').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showPreview(data) {
|
||||||
|
if (data.image) {
|
||||||
|
$('previewImg').src = data.image;
|
||||||
|
$('previewImg').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
$('previewImg').classList.add('hidden');
|
||||||
|
}
|
||||||
|
$('previewSite').textContent = data.site || '';
|
||||||
|
$('previewTitle').textContent = data.title || '';
|
||||||
|
$('previewDesc').textContent = data.description || '';
|
||||||
|
$('resultPreview').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy helpers
|
||||||
|
function copyToClipboard(text, btn) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = 'Скопировано!';
|
||||||
|
btn.classList.add('text-accent');
|
||||||
|
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$('copyJsonBtn').addEventListener('click', () => {
|
||||||
|
if (lastResult) copyToClipboard(JSON.stringify(lastResult, null, 2), $('copyJsonBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyTextBtn').addEventListener('click', () => {
|
||||||
|
if (lastResult) copyToClipboard(lastResult.content_text || '', $('copyTextBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyHtmlBtn').addEventListener('click', () => {
|
||||||
|
if (lastResult) copyToClipboard(lastResult.content_html || '', $('copyHtmlBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyMetaJsonBtn').addEventListener('click', () => {
|
||||||
|
if (lastMetadata) copyToClipboard(JSON.stringify(lastMetadata, null, 2), $('copyMetaJsonBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyPlainTextBtn').addEventListener('click', () => {
|
||||||
|
if (lastText) copyToClipboard(lastText, $('copyPlainTextBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyPreviewJsonBtn').addEventListener('click', () => {
|
||||||
|
if (lastPreview) copyToClipboard(JSON.stringify(lastPreview, null, 2), $('copyPreviewJsonBtn'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
$('clearBtn').addEventListener('click', () => {
|
||||||
|
hideAll();
|
||||||
|
lastResult = null;
|
||||||
|
lastMetadata = null;
|
||||||
|
lastText = null;
|
||||||
|
lastPreview = null;
|
||||||
|
$('urlInput').value = '';
|
||||||
|
$('urlInput').focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-focus
|
||||||
|
$('urlInput').focus();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
482
public/password.html
Normal file
482
public/password.html
Normal file
@ -0,0 +1,482 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Password Generator — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f7;
|
||||||
|
--bg-grad1: rgba(0, 67, 184, 0.04);
|
||||||
|
--bg-grad2: rgba(0, 50, 140, 0.03);
|
||||||
|
--surface-800: rgba(255,255,255,0.85);
|
||||||
|
--surface-700: #f0f0f3;
|
||||||
|
--surface-600: #dde0e6;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555;
|
||||||
|
--text-muted: #888;
|
||||||
|
--select-bg: #f0f0f3;
|
||||||
|
--select-color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--bg: #060c18;
|
||||||
|
--bg-grad1: rgba(0, 84, 230, 0.03);
|
||||||
|
--bg-grad2: rgba(0, 67, 184, 0.02);
|
||||||
|
--surface-800: rgba(9,16,32,0.8);
|
||||||
|
--surface-700: #0d1828;
|
||||||
|
--surface-600: #162040;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #c0c0c0;
|
||||||
|
--text-muted: #666;
|
||||||
|
--select-bg: #091020;
|
||||||
|
--select-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.dark .noise::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.dark .theme-toggle { background: #162040; }
|
||||||
|
.theme-toggle { background: #dde0e6; }
|
||||||
|
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
|
||||||
|
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
|
||||||
|
|
||||||
|
select, select option { background: var(--select-bg); color: var(--select-color); }
|
||||||
|
|
||||||
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
||||||
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
||||||
|
|
||||||
|
/* Toggle switch */
|
||||||
|
.toggle { position: relative; width: 40px; height: 22px; border-radius: 11px; cursor: pointer; transition: background 0.2s; flex-shrink: 0; }
|
||||||
|
.toggle.off { background: var(--surface-600); }
|
||||||
|
.toggle.on { background: #0054e6; }
|
||||||
|
.toggle-knob { position: absolute; top: 2px; left: 2px; width: 18px; height: 18px; border-radius: 50%; background: #fff; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
|
||||||
|
.toggle.on .toggle-knob { left: 20px; }
|
||||||
|
|
||||||
|
/* Strength bar */
|
||||||
|
.strength-bar { height: 6px; border-radius: 3px; background: var(--surface-700); overflow: hidden; }
|
||||||
|
.strength-bar-fill { height: 100%; border-radius: 3px; transition: width 0.4s, background 0.4s; }
|
||||||
|
|
||||||
|
/* Password display */
|
||||||
|
.pw-display {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
word-break: break-all;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Range slider */
|
||||||
|
input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 100%;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--surface-700);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0054e6;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,84,230,0.3);
|
||||||
|
}
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0054e6;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,84,230,0.3);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link active" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-2xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
Crypto · Client-side
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Генератор <span class="text-accent">паролей</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">криптографически стойкая генерация</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<!-- Length -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-2 font-mono uppercase tracking-wider">Длина</label>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<input type="range" id="lengthRange" min="4" max="128" value="16" class="flex-1">
|
||||||
|
<input type="number" id="lengthInput" min="4" max="128" value="16"
|
||||||
|
class="w-20 px-3 py-2 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono text-center focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Toggles -->
|
||||||
|
<div class="space-y-3 mb-5">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm dark:text-gray-300 text-gray-600">Верхний регистр <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(A-Z)</span></span>
|
||||||
|
<div class="toggle on" data-opt="upper"><div class="toggle-knob"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm dark:text-gray-300 text-gray-600">Нижний регистр <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(a-z)</span></span>
|
||||||
|
<div class="toggle on" data-opt="lower"><div class="toggle-knob"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm dark:text-gray-300 text-gray-600">Цифры <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(0-9)</span></span>
|
||||||
|
<div class="toggle on" data-opt="digits"><div class="toggle-knob"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm dark:text-gray-300 text-gray-600">Символы <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(!@#$%^&*...)</span></span>
|
||||||
|
<div class="toggle off" data-opt="symbols"><div class="toggle-knob"></div></div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span class="text-sm dark:text-gray-300 text-gray-600">Исключить похожие <span class="font-mono text-xs dark:text-gray-500 text-gray-400">(l, 1, I, O, 0, o)</span></span>
|
||||||
|
<div class="toggle off" data-opt="exclude"><div class="toggle-knob"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Count -->
|
||||||
|
<div class="mb-5">
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Количество</label>
|
||||||
|
<select id="countSelect" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
|
||||||
|
<option value="1">1 пароль</option>
|
||||||
|
<option value="5">5 паролей</option>
|
||||||
|
<option value="10">10 паролей</option>
|
||||||
|
<option value="25">25 паролей</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate button -->
|
||||||
|
<button id="generateBtn"
|
||||||
|
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent text-white border border-accent cursor-pointer
|
||||||
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20
|
||||||
|
active:scale-[0.98]">
|
||||||
|
Сгенерировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Result -->
|
||||||
|
<div id="resultCard" class="mt-5 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<!-- Strength indicator -->
|
||||||
|
<div id="strengthBlock" class="mb-5 hidden">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span id="strengthLabel" class="text-xs font-medium font-mono uppercase tracking-wider"></span>
|
||||||
|
<span id="entropyLabel" class="text-xs font-mono dark:text-gray-500 text-gray-400"></span>
|
||||||
|
</div>
|
||||||
|
<div class="strength-bar">
|
||||||
|
<div id="strengthFill" class="strength-bar-fill" style="width:0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Single password display -->
|
||||||
|
<div id="singleResult" class="hidden">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div id="passwordDisplay" class="pw-display flex-1 text-lg sm:text-xl dark:text-white text-gray-900 dark:bg-surface-700/50 bg-gray-50 rounded-xl px-4 py-3 border dark:border-surface-600 border-gray-200 select-all"></div>
|
||||||
|
<button id="copyBtn" class="px-4 py-3 rounded-xl text-sm font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200 whitespace-nowrap flex-shrink-0">
|
||||||
|
Копировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Multi password display -->
|
||||||
|
<div id="multiResult" class="hidden space-y-2">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Пароли</span>
|
||||||
|
<button id="copyAllBtn" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">
|
||||||
|
Копировать все
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="passwordList"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
const opts = { upper: true, lower: true, digits: true, symbols: false, exclude: false };
|
||||||
|
const CHARSETS = {
|
||||||
|
upper: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
|
||||||
|
lower: 'abcdefghijklmnopqrstuvwxyz',
|
||||||
|
digits: '0123456789',
|
||||||
|
symbols: '!@#$%^&*()-_=+[]{}|;:,.<>?/~`',
|
||||||
|
};
|
||||||
|
const SIMILAR = 'l1IO0o';
|
||||||
|
|
||||||
|
// Toggle handlers
|
||||||
|
document.querySelectorAll('.toggle[data-opt]').forEach(el => {
|
||||||
|
el.addEventListener('click', () => {
|
||||||
|
const opt = el.dataset.opt;
|
||||||
|
opts[opt] = !opts[opt];
|
||||||
|
el.classList.toggle('on', opts[opt]);
|
||||||
|
el.classList.toggle('off', !opts[opt]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Length sync
|
||||||
|
const lengthRange = document.getElementById('lengthRange');
|
||||||
|
const lengthInput = document.getElementById('lengthInput');
|
||||||
|
lengthRange.addEventListener('input', () => { lengthInput.value = lengthRange.value; });
|
||||||
|
lengthInput.addEventListener('input', () => {
|
||||||
|
let v = parseInt(lengthInput.value, 10);
|
||||||
|
if (v < 4) v = 4;
|
||||||
|
if (v > 128) v = 128;
|
||||||
|
lengthRange.value = v;
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildCharset() {
|
||||||
|
let cs = '';
|
||||||
|
if (opts.upper) cs += CHARSETS.upper;
|
||||||
|
if (opts.lower) cs += CHARSETS.lower;
|
||||||
|
if (opts.digits) cs += CHARSETS.digits;
|
||||||
|
if (opts.symbols) cs += CHARSETS.symbols;
|
||||||
|
if (opts.exclude) {
|
||||||
|
cs = cs.split('').filter(c => !SIMILAR.includes(c)).join('');
|
||||||
|
}
|
||||||
|
return cs;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generatePassword(length, charset) {
|
||||||
|
const arr = new Uint32Array(length);
|
||||||
|
crypto.getRandomValues(arr);
|
||||||
|
let pw = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
pw += charset[arr[i] % charset.length];
|
||||||
|
}
|
||||||
|
return pw;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcEntropy(length, charsetSize) {
|
||||||
|
if (charsetSize <= 1) return 0;
|
||||||
|
return Math.floor(length * Math.log2(charsetSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStrength(entropy) {
|
||||||
|
if (entropy < 28) return { label: 'Слабый', color: 'text-danger', bar: 'bg-danger', pct: 15 };
|
||||||
|
if (entropy < 36) return { label: 'Средний', color: 'text-warn', bar: 'bg-warn', pct: 35 };
|
||||||
|
if (entropy < 60) return { label: 'Сильный', color: 'text-accent/70', bar: 'bg-accent/60', pct: 65 };
|
||||||
|
return { label: 'Очень сильный', color: 'text-accent', bar: 'bg-accent', pct: 100 };
|
||||||
|
}
|
||||||
|
|
||||||
|
function showStrength(length, charsetSize) {
|
||||||
|
const block = document.getElementById('strengthBlock');
|
||||||
|
const entropy = calcEntropy(length, charsetSize);
|
||||||
|
const s = getStrength(entropy);
|
||||||
|
|
||||||
|
document.getElementById('strengthLabel').textContent = s.label;
|
||||||
|
document.getElementById('strengthLabel').className = 'text-xs font-medium font-mono uppercase tracking-wider ' + s.color;
|
||||||
|
document.getElementById('entropyLabel').textContent = entropy + ' бит энтропии';
|
||||||
|
const fill = document.getElementById('strengthFill');
|
||||||
|
fill.className = 'strength-bar-fill ' + s.bar;
|
||||||
|
fill.style.width = s.pct + '%';
|
||||||
|
block.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyText(text, btn) {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = 'Скопировано!';
|
||||||
|
btn.classList.add('text-accent');
|
||||||
|
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('generateBtn').addEventListener('click', generate);
|
||||||
|
|
||||||
|
function generate() {
|
||||||
|
const charset = buildCharset();
|
||||||
|
if (!charset) {
|
||||||
|
alert('Выберите хотя бы один набор символов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = parseInt(lengthInput.value, 10) || 16;
|
||||||
|
const count = parseInt(document.getElementById('countSelect').value, 10);
|
||||||
|
|
||||||
|
showStrength(length, charset.length);
|
||||||
|
|
||||||
|
const singleResult = document.getElementById('singleResult');
|
||||||
|
const multiResult = document.getElementById('multiResult');
|
||||||
|
|
||||||
|
if (count === 1) {
|
||||||
|
const pw = generatePassword(length, charset);
|
||||||
|
document.getElementById('passwordDisplay').textContent = pw;
|
||||||
|
singleResult.classList.remove('hidden');
|
||||||
|
multiResult.classList.add('hidden');
|
||||||
|
|
||||||
|
document.getElementById('copyBtn').onclick = () => copyText(pw, document.getElementById('copyBtn'));
|
||||||
|
} else {
|
||||||
|
const passwords = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
passwords.push(generatePassword(length, charset));
|
||||||
|
}
|
||||||
|
|
||||||
|
const list = document.getElementById('passwordList');
|
||||||
|
list.innerHTML = '';
|
||||||
|
passwords.forEach((pw, i) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'flex items-center gap-2 py-2' + (i > 0 ? ' border-t dark:border-surface-600/50 border-gray-100' : '');
|
||||||
|
row.innerHTML = `
|
||||||
|
<span class="pw-display flex-1 text-sm dark:text-gray-200 text-gray-700 select-all break-all">${escapeHtml(pw)}</span>
|
||||||
|
<button class="copy-row-btn px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-400 text-gray-500 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200 whitespace-nowrap flex-shrink-0">Копировать</button>
|
||||||
|
`;
|
||||||
|
row.querySelector('.copy-row-btn').addEventListener('click', function() { copyText(pw, this); });
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
singleResult.classList.add('hidden');
|
||||||
|
multiResult.classList.remove('hidden');
|
||||||
|
|
||||||
|
document.getElementById('copyAllBtn').onclick = () => copyText(passwords.join('\n'), document.getElementById('copyAllBtn'));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('resultCard').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(str) {
|
||||||
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate on load
|
||||||
|
generate();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
412
public/placeholder.html
Normal file
412
public/placeholder.html
Normal file
@ -0,0 +1,412 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Placeholder — генератор заглушек</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #f5f5f7;
|
||||||
|
--bg-grad1: rgba(0, 67, 184, 0.04);
|
||||||
|
--bg-grad2: rgba(0, 50, 140, 0.03);
|
||||||
|
--surface-800: rgba(255,255,255,0.85);
|
||||||
|
--surface-700: #f0f0f3;
|
||||||
|
--surface-600: #dde0e6;
|
||||||
|
--text-primary: #1a1a2e;
|
||||||
|
--text-secondary: #555;
|
||||||
|
--text-muted: #888;
|
||||||
|
--select-bg: #f0f0f3;
|
||||||
|
--select-color: #1a1a2e;
|
||||||
|
}
|
||||||
|
.dark {
|
||||||
|
--bg: #060c18;
|
||||||
|
--bg-grad1: rgba(0, 84, 230, 0.03);
|
||||||
|
--bg-grad2: rgba(0, 67, 184, 0.02);
|
||||||
|
--surface-800: rgba(9,16,32,0.8);
|
||||||
|
--surface-700: #0d1828;
|
||||||
|
--surface-600: #162040;
|
||||||
|
--text-primary: #fff;
|
||||||
|
--text-secondary: #c0c0c0;
|
||||||
|
--text-muted: #666;
|
||||||
|
--select-bg: #091020;
|
||||||
|
--select-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
background-image:
|
||||||
|
radial-gradient(ellipse at 20% 50%, var(--bg-grad1) 0%, transparent 50%),
|
||||||
|
radial-gradient(ellipse at 80% 20%, var(--bg-grad2) 0%, transparent 50%);
|
||||||
|
min-height: 100vh;
|
||||||
|
transition: background 0.3s;
|
||||||
|
}
|
||||||
|
.dark .noise::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle { position: relative; width: 44px; height: 24px; border-radius: 12px; cursor: pointer; transition: background 0.3s; }
|
||||||
|
.dark .theme-toggle { background: #162040; }
|
||||||
|
.theme-toggle { background: #dde0e6; }
|
||||||
|
.theme-toggle-knob { position: absolute; top: 2px; width: 20px; height: 20px; border-radius: 50%; transition: left 0.3s, background 0.3s; display: flex; align-items: center; justify-content: center; }
|
||||||
|
.dark .theme-toggle-knob { left: 22px; background: #0d1828; }
|
||||||
|
.theme-toggle-knob { left: 2px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.15); }
|
||||||
|
|
||||||
|
select, select option { background: var(--select-bg); color: var(--select-color); }
|
||||||
|
|
||||||
|
@keyframes fadeUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
.fade-up { animation: fadeUp 0.4s ease-out forwards; }
|
||||||
|
.fade-up-delay { animation: fadeUp 0.4s ease-out 0.1s forwards; opacity: 0; }
|
||||||
|
|
||||||
|
.preview-frame {
|
||||||
|
background: repeating-conic-gradient(rgba(128,128,128,.08) 0% 25%, transparent 0% 50%) 50%/16px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 160px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.preview-frame img { max-width: 100%; max-height: 400px; display: block; border-radius: 8px; }
|
||||||
|
|
||||||
|
.color-swatch {
|
||||||
|
width: 36px; height: 36px; border-radius: 8px; border: 2px solid var(--surface-600);
|
||||||
|
cursor: pointer; transition: transform .15s;
|
||||||
|
}
|
||||||
|
.color-swatch:hover { transform: scale(1.1); }
|
||||||
|
input[type="color"] { position: absolute; opacity: 0; width: 0; height: 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link active" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-2xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
PNG · JPG · WebP
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
<span class="text-accent">Placeholder</span> генератор
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">изображения-заглушки любого размера</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Controls Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<!-- Size -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Ширина</label>
|
||||||
|
<input type="number" id="width" value="600" min="1" max="4000" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Высота</label>
|
||||||
|
<input type="number" id="height" value="400" min="1" max="4000" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Colors -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Фон</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<div id="bgSwatch" class="color-swatch" style="background:#3B82F6"></div>
|
||||||
|
<input type="color" id="bgPicker" value="#3B82F6">
|
||||||
|
</div>
|
||||||
|
<input type="text" id="bgColor" value="3B82F6" maxlength="20" class="flex-1 px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Текст</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="relative">
|
||||||
|
<div id="fgSwatch" class="color-swatch" style="background:#FFFFFF"></div>
|
||||||
|
<input type="color" id="fgPicker" value="#FFFFFF">
|
||||||
|
</div>
|
||||||
|
<input type="text" id="fgColor" value="FFFFFF" maxlength="20" class="flex-1 px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Text & Font size -->
|
||||||
|
<div class="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Надпись</label>
|
||||||
|
<input type="text" id="text" placeholder="600x400" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm font-mono focus:outline-none focus:border-accent/50 transition-colors dark:text-white text-gray-800">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Формат</label>
|
||||||
|
<select id="format" class="w-full px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-sm focus:outline-none focus:border-accent/50 transition-colors">
|
||||||
|
<option value="png">PNG</option>
|
||||||
|
<option value="jpg">JPG</option>
|
||||||
|
<option value="webp">WebP</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate URL -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">URL</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<input type="text" id="urlField" readonly class="flex-1 px-3 py-2.5 dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 rounded-lg text-xs font-mono focus:outline-none dark:text-gray-300 text-gray-600 truncate">
|
||||||
|
<button id="copyUrlBtn" class="px-4 py-2.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200 whitespace-nowrap">
|
||||||
|
Копировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generate button -->
|
||||||
|
<button id="generateBtn"
|
||||||
|
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent text-white border border-accent cursor-pointer
|
||||||
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20
|
||||||
|
active:scale-[0.98]">
|
||||||
|
Сгенерировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview -->
|
||||||
|
<div id="previewCard" class="mt-5 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<span class="text-xs font-mono dark:text-gray-500 text-gray-400 uppercase tracking-wider">Превью</span>
|
||||||
|
<a id="downloadLink" href="#" download class="px-3 py-1.5 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-300 text-gray-600 hover:text-accent transition-colors border dark:border-surface-600 border-gray-200 no-underline">
|
||||||
|
Скачать
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="preview-frame p-3">
|
||||||
|
<img id="previewImg" src="/placeholder-img/600x400/3B82F6/FFFFFF" alt="placeholder">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- API Reference -->
|
||||||
|
<div class="mt-5 fade-up">
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm shadow-sm dark:shadow-none">
|
||||||
|
<h3 class="text-sm font-semibold dark:text-white text-gray-800 mb-3">API</h3>
|
||||||
|
<div class="space-y-2 text-xs font-mono dark:text-gray-400 text-gray-500 leading-relaxed">
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/placeholder-img/:WxH/:bg/:fg</code> — картинка без текста</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">/placeholder-img/:WxH/:bg/:fg?text=Hello</code> — с текстом</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">?format=png|jpg|webp</code> — формат выходного файла</p>
|
||||||
|
<p><code class="dark:bg-surface-700 bg-gray-100 px-2 py-0.5 rounded">?fontsize=30</code> — размер шрифта (авто по умолчанию)</p>
|
||||||
|
<p class="dark:text-gray-500 text-gray-400 pt-1">Цвета: HEX (<code class="dark:bg-surface-700 bg-gray-100 px-1.5 py-0.5 rounded">3B82F6</code>) или имя (<code class="dark:bg-surface-700 bg-gray-100 px-1.5 py-0.5 rounded">red</code>, <code class="dark:bg-surface-700 bg-gray-100 px-1.5 py-0.5 rounded">yellow</code>)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div><!-- /main-content -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Elements
|
||||||
|
const widthEl = document.getElementById('width');
|
||||||
|
const heightEl = document.getElementById('height');
|
||||||
|
const bgColorEl = document.getElementById('bgColor');
|
||||||
|
const fgColorEl = document.getElementById('fgColor');
|
||||||
|
const bgPickerEl = document.getElementById('bgPicker');
|
||||||
|
const fgPickerEl = document.getElementById('fgPicker');
|
||||||
|
const bgSwatchEl = document.getElementById('bgSwatch');
|
||||||
|
const fgSwatchEl = document.getElementById('fgSwatch');
|
||||||
|
const textEl = document.getElementById('text');
|
||||||
|
const formatEl = document.getElementById('format');
|
||||||
|
const urlField = document.getElementById('urlField');
|
||||||
|
const previewImg = document.getElementById('previewImg');
|
||||||
|
const downloadLink = document.getElementById('downloadLink');
|
||||||
|
|
||||||
|
// Color picker sync
|
||||||
|
bgSwatchEl.addEventListener('click', () => bgPickerEl.click());
|
||||||
|
fgSwatchEl.addEventListener('click', () => fgPickerEl.click());
|
||||||
|
|
||||||
|
bgPickerEl.addEventListener('input', e => {
|
||||||
|
const hex = e.target.value.replace('#', '').toUpperCase();
|
||||||
|
bgColorEl.value = hex;
|
||||||
|
bgSwatchEl.style.background = e.target.value;
|
||||||
|
updateUrl();
|
||||||
|
});
|
||||||
|
fgPickerEl.addEventListener('input', e => {
|
||||||
|
const hex = e.target.value.replace('#', '').toUpperCase();
|
||||||
|
fgColorEl.value = hex;
|
||||||
|
fgSwatchEl.style.background = e.target.value;
|
||||||
|
updateUrl();
|
||||||
|
});
|
||||||
|
bgColorEl.addEventListener('input', () => {
|
||||||
|
const v = bgColorEl.value.trim();
|
||||||
|
if (/^[0-9A-Fa-f]{6}$/.test(v)) {
|
||||||
|
bgPickerEl.value = '#' + v;
|
||||||
|
bgSwatchEl.style.background = '#' + v;
|
||||||
|
} else {
|
||||||
|
bgSwatchEl.style.background = v;
|
||||||
|
}
|
||||||
|
updateUrl();
|
||||||
|
});
|
||||||
|
fgColorEl.addEventListener('input', () => {
|
||||||
|
const v = fgColorEl.value.trim();
|
||||||
|
if (/^[0-9A-Fa-f]{6}$/.test(v)) {
|
||||||
|
fgPickerEl.value = '#' + v;
|
||||||
|
fgSwatchEl.style.background = '#' + v;
|
||||||
|
} else {
|
||||||
|
fgSwatchEl.style.background = v;
|
||||||
|
}
|
||||||
|
updateUrl();
|
||||||
|
});
|
||||||
|
|
||||||
|
[widthEl, heightEl, textEl, formatEl].forEach(el => el.addEventListener('input', updateUrl));
|
||||||
|
|
||||||
|
function buildPath() {
|
||||||
|
const w = widthEl.value || 600;
|
||||||
|
const h = heightEl.value || 400;
|
||||||
|
const bg = bgColorEl.value.trim() || '3B82F6';
|
||||||
|
const fg = fgColorEl.value.trim() || 'FFFFFF';
|
||||||
|
let url = `/placeholder-img/${w}x${h}/${bg}/${fg}`;
|
||||||
|
const params = [];
|
||||||
|
const txt = textEl.value.trim();
|
||||||
|
if (txt) params.push('text=' + encodeURIComponent(txt));
|
||||||
|
const fmt = formatEl.value;
|
||||||
|
if (fmt !== 'png') params.push('format=' + fmt);
|
||||||
|
if (params.length) url += '?' + params.join('&');
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrl() {
|
||||||
|
const p = buildPath();
|
||||||
|
urlField.value = location.origin + p;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generate() {
|
||||||
|
const p = buildPath();
|
||||||
|
previewImg.src = p;
|
||||||
|
downloadLink.href = p;
|
||||||
|
updateUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('generateBtn').addEventListener('click', generate);
|
||||||
|
|
||||||
|
// Copy URL
|
||||||
|
document.getElementById('copyUrlBtn').addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(urlField.value).then(() => {
|
||||||
|
const btn = document.getElementById('copyUrlBtn');
|
||||||
|
const orig = btn.textContent;
|
||||||
|
btn.textContent = 'Скопировано!';
|
||||||
|
btn.classList.add('text-accent');
|
||||||
|
setTimeout(() => { btn.textContent = orig; btn.classList.remove('text-accent'); }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Init
|
||||||
|
updateUrl();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
712
public/redirects.html
Normal file
712
public/redirects.html
Normal file
@ -0,0 +1,712 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Redirect Analyzer — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
:root{--bg:#f5f5f7;--bg-grad1:rgba(0,67,184,.04);--bg-grad2:rgba(0,50,140,.03);--surface-800:rgba(255,255,255,0.85);--surface-700:#f0f0f3;--surface-600:#dde0e6;--text-primary:#1a1a2e;--text-secondary:#555;--text-muted:#888;--select-bg:#fff;--select-color:#1a1a2e}
|
||||||
|
.dark{--bg:#060c18;--bg-grad1:rgba(0,84,230,.03);--bg-grad2:rgba(0,67,184,.02);--surface-800:rgba(9,16,32,0.8);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--select-bg:#091020;--select-color:#e0e0e0}
|
||||||
|
body{background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,var(--bg-grad1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,var(--bg-grad2) 0%,transparent 50%);min-height:100vh;transition:background .3s}
|
||||||
|
.dark .noise::before{content:'';position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
|
||||||
|
.theme-toggle{position:relative;width:44px;height:24px;border-radius:12px;cursor:pointer;transition:background .3s}
|
||||||
|
.dark .theme-toggle{background:#162040}
|
||||||
|
.theme-toggle{background:#dde0e6}
|
||||||
|
.theme-toggle-knob{position:absolute;top:2px;width:20px;height:20px;border-radius:50%;transition:left .3s,background .3s;display:flex;align-items:center;justify-content:center}
|
||||||
|
.dark .theme-toggle-knob{left:22px;background:#0d1828}
|
||||||
|
.theme-toggle-knob{left:2px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
||||||
|
|
||||||
|
/* Redirect Analyzer UI */
|
||||||
|
.panel {
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.url-input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--surface-800);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
height: 40px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.url-input:focus { border-color: rgba(0,84,230,.5); }
|
||||||
|
.url-input::placeholder { color: var(--text-muted); }
|
||||||
|
select.ctrl-select {
|
||||||
|
background: var(--select-bg);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--select-color);
|
||||||
|
outline: none;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 40px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
select.ctrl-select:focus { border-color: rgba(0,84,230,.5); }
|
||||||
|
.analyze-btn {
|
||||||
|
background: #0054e6;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0 20px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
transition: background .2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
height: 40px;
|
||||||
|
font-family: 'Manrope', sans-serif;
|
||||||
|
}
|
||||||
|
.analyze-btn:hover { background: #0043b8; }
|
||||||
|
.analyze-btn:disabled { background: #0054e680; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* Chain cards */
|
||||||
|
.chain-step {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.step-card {
|
||||||
|
background: var(--surface-700);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
transition: border-color .2s;
|
||||||
|
}
|
||||||
|
.step-card:hover { border-color: rgba(0,84,230,.3); }
|
||||||
|
.step-card.final { border-color: rgba(0,168,107,.3); background: rgba(0,168,107,.04); }
|
||||||
|
.step-num {
|
||||||
|
width: 24px; height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(0,84,230,.15);
|
||||||
|
color: #6b9fff;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.step-num.final { background: rgba(0,168,107,.15); color: #00a86b; }
|
||||||
|
.status-badge {
|
||||||
|
display: inline-flex; align-items: center;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.s-2xx { background: rgba(0,168,107,.15); color: #00a86b; }
|
||||||
|
.s-3xx { background: rgba(0,84,230,.15); color: #6b9fff; }
|
||||||
|
.s-4xx { background: rgba(255,165,0,.15); color: #ffa500; }
|
||||||
|
.s-5xx { background: rgba(255,82,82,.15); color: #ff5252; }
|
||||||
|
.arrow-connector {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 16px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.arrow-line {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: var(--surface-600);
|
||||||
|
}
|
||||||
|
.final-badge {
|
||||||
|
display: inline-flex; align-items: center; gap: 4px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
background: rgba(0,168,107,.15);
|
||||||
|
color: #00a86b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary */
|
||||||
|
.stat-card {
|
||||||
|
background: var(--surface-700);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
display: flex; flex-direction: column; gap: 4px;
|
||||||
|
}
|
||||||
|
.stat-val {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
.stat-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Issues */
|
||||||
|
.issue-item {
|
||||||
|
display: flex; align-items: flex-start; gap: 10px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.issue-error { background: rgba(255,82,82,.07); color: #ff5252; border: 1px solid rgba(255,82,82,.2); }
|
||||||
|
.issue-warn { background: rgba(255,214,0,.07); color: #ffd600; border: 1px solid rgba(255,214,0,.15); }
|
||||||
|
.issue-info { background: rgba(0,84,230,.07); color: #6b9fff; border: 1px solid rgba(0,84,230,.15); }
|
||||||
|
|
||||||
|
/* Headers table */
|
||||||
|
.headers-table { width: 100%; border-collapse: collapse; font-size: 11px; font-family: 'JetBrains Mono', monospace; }
|
||||||
|
.headers-table th { text-align: left; padding: 5px 10px; color: var(--text-muted); border-bottom: 1px solid var(--surface-600); font-weight: 600; }
|
||||||
|
.headers-table td { padding: 5px 10px; color: var(--text-secondary); border-bottom: 1px solid rgba(255,255,255,.03); vertical-align: top; word-break: break-all; }
|
||||||
|
.headers-table td:first-child { color: #6b9fff; white-space: nowrap; min-width: 160px; }
|
||||||
|
|
||||||
|
/* Details accordion */
|
||||||
|
.details-accordion summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
display: flex; align-items: center; gap: 6px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface-700);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
user-select: none;
|
||||||
|
transition: color .15s;
|
||||||
|
}
|
||||||
|
.details-accordion summary:hover { color: var(--text-secondary); }
|
||||||
|
.details-accordion[open] summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; border-bottom-color: transparent; }
|
||||||
|
.details-accordion .details-body {
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-top: none;
|
||||||
|
border-bottom-left-radius: 8px;
|
||||||
|
border-bottom-right-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Export btns */
|
||||||
|
.export-btn {
|
||||||
|
background: var(--surface-700);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 7px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
padding: 6px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s;
|
||||||
|
display: flex; align-items: center; gap: 5px;
|
||||||
|
}
|
||||||
|
.export-btn:hover { color: #0054e6; border-color: rgba(0,84,230,.4); background: rgba(0,84,230,.06); }
|
||||||
|
|
||||||
|
/* Spinner */
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
.spinner { animation: spin 0.8s linear infinite; }
|
||||||
|
|
||||||
|
/* Copy button */
|
||||||
|
.copy-btn {
|
||||||
|
background: var(--surface-700);
|
||||||
|
border: 1px solid var(--surface-600);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
padding: 4px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
.copy-btn:hover { color: #0054e6; border-color: rgba(0,84,230,.4); }
|
||||||
|
|
||||||
|
/* URL truncate */
|
||||||
|
.url-text {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
word-break: break-all;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.url-text a { color: #6b9fff; text-decoration: none; }
|
||||||
|
.url-text a:hover { text-decoration: underline; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">IW</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link" title="Sanitizer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link active" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 p-6 max-w-5xl mx-auto">
|
||||||
|
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="mb-8 text-center">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
HTTP · HTTPS · 301 · 302
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
Redirect <span class="text-accent">Анализатор</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">анализ цепочки HTTP-редиректов</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- URL Bar -->
|
||||||
|
<div class="panel mb-6">
|
||||||
|
<div class="p-4 flex flex-wrap gap-3 items-center">
|
||||||
|
<input id="urlInput" type="text" class="url-input" placeholder="https://example.com или example.com" />
|
||||||
|
<select id="uaSelect" class="ctrl-select">
|
||||||
|
<option value="desktop">Десктоп</option>
|
||||||
|
<option value="mobile">Мобильный</option>
|
||||||
|
<option value="googlebot">Googlebot</option>
|
||||||
|
</select>
|
||||||
|
<select id="methodSelect" class="ctrl-select">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="HEAD">HEAD</option>
|
||||||
|
</select>
|
||||||
|
<button id="analyzeBtn" class="analyze-btn">
|
||||||
|
<svg id="btnIcon" width="15" height="15" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803a7.5 7.5 0 0010.607 0z"/></svg>
|
||||||
|
Анализировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- States -->
|
||||||
|
<div id="stateEmpty">
|
||||||
|
<div class="panel p-10 text-center">
|
||||||
|
<svg class="mx-auto mb-4 opacity-20" width="48" height="48" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
<p class="text-sm font-mono" style="color:var(--text-muted)">Введите URL для анализа цепочки редиректов</p>
|
||||||
|
<p class="text-xs font-mono mt-2" style="color:var(--text-muted)">Пример: <span style="color:#6b9fff">http://google.com</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stateLoading" class="hidden">
|
||||||
|
<div class="panel p-10 text-center">
|
||||||
|
<svg class="spinner mx-auto mb-4" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#0054e6" stroke-width="2"><path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/></svg>
|
||||||
|
<p class="text-sm font-mono" style="color:var(--text-muted)">Анализируем...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stateError" class="hidden">
|
||||||
|
<div class="panel p-6">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<svg width="18" height="18" fill="none" stroke="#ff5252" viewBox="0 0 24 24" stroke-width="2" class="flex-shrink-0 mt-0.5"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"/></svg>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-semibold" style="color:#ff5252">Ошибка</p>
|
||||||
|
<p id="errorMsg" class="text-sm font-mono mt-1" style="color:var(--text-muted)"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stateResult" class="hidden space-y-6">
|
||||||
|
|
||||||
|
<!-- Summary stats -->
|
||||||
|
<div id="summaryBlock" class="grid grid-cols-2 sm:grid-cols-4 gap-3"></div>
|
||||||
|
|
||||||
|
<!-- Final URL -->
|
||||||
|
<div class="panel p-4 flex items-center gap-3">
|
||||||
|
<span class="text-xs font-mono flex-shrink-0" style="color:var(--text-muted)">КОНЕЧНЫЙ URL</span>
|
||||||
|
<span id="finalUrlText" class="url-text"></span>
|
||||||
|
<button class="copy-btn" id="copyFinalUrl">Копировать</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Issues -->
|
||||||
|
<div id="issuesBlock" class="hidden space-y-2">
|
||||||
|
<p class="text-xs font-mono font-semibold uppercase tracking-wider" style="color:var(--text-muted)">SEO Проблемы</p>
|
||||||
|
<div id="issuesList" class="space-y-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chain -->
|
||||||
|
<div>
|
||||||
|
<p class="text-xs font-mono font-semibold uppercase tracking-wider mb-3" style="color:var(--text-muted)">Цепочка редиректов</p>
|
||||||
|
<div id="chainContainer" class="space-y-1"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Headers detail -->
|
||||||
|
<details class="details-accordion" id="headersAccordion">
|
||||||
|
<summary>
|
||||||
|
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 6.75h12M8.25 12h12m-12 5.25h12M3.75 6.75h.007v.008H3.75V6.75zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zM3.75 12h.007v.008H3.75V12zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm-.375 5.25h.007v.008H3.75v-.008zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z"/></svg>
|
||||||
|
Детальные заголовки ответов
|
||||||
|
<svg class="ml-auto" width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/></svg>
|
||||||
|
</summary>
|
||||||
|
<div class="details-body">
|
||||||
|
<div id="headersDetail" class="overflow-x-auto"></div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- Export -->
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<button class="export-btn" id="btnCopyJson">
|
||||||
|
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184"/></svg>
|
||||||
|
Копировать JSON
|
||||||
|
</button>
|
||||||
|
<button class="export-btn" id="btnDownloadJson">
|
||||||
|
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
|
||||||
|
Скачать JSON
|
||||||
|
</button>
|
||||||
|
<button class="export-btn" id="btnDownloadCsv">
|
||||||
|
<svg width="13" height="13" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3"/></svg>
|
||||||
|
Скачать CSV
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- /stateResult -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
// Theme
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
// State
|
||||||
|
let lastResult = null;
|
||||||
|
|
||||||
|
// UI refs
|
||||||
|
const urlInput = document.getElementById('urlInput');
|
||||||
|
const uaSelect = document.getElementById('uaSelect');
|
||||||
|
const methodSel = document.getElementById('methodSelect');
|
||||||
|
const analyzeBtn = document.getElementById('analyzeBtn');
|
||||||
|
const btnIcon = document.getElementById('btnIcon');
|
||||||
|
|
||||||
|
function showState(name) {
|
||||||
|
['Empty','Loading','Error','Result'].forEach(s => {
|
||||||
|
document.getElementById('state' + s).classList.toggle('hidden', s.toLowerCase() !== name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusClass(code) {
|
||||||
|
if (code >= 500) return 's-5xx';
|
||||||
|
if (code >= 400) return 's-4xx';
|
||||||
|
if (code >= 300) return 's-3xx';
|
||||||
|
return 's-2xx';
|
||||||
|
}
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderChain(data) {
|
||||||
|
const container = document.getElementById('chainContainer');
|
||||||
|
container.innerHTML = '';
|
||||||
|
const chain = data.chain;
|
||||||
|
|
||||||
|
chain.forEach((step, i) => {
|
||||||
|
const isFinal = (i === chain.length - 1);
|
||||||
|
const isRedirect = step.status >= 300 && step.status < 400;
|
||||||
|
const sc = statusClass(step.status);
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'chain-step';
|
||||||
|
|
||||||
|
const cardHtml = `
|
||||||
|
<div class="step-card ${isFinal ? 'final' : ''}">
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<div class="step-num ${isFinal ? 'final' : ''}">${i + 1}</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<div class="flex flex-wrap items-center gap-2 mb-1">
|
||||||
|
<span class="status-badge ${sc}">${escHtml(step.status)} ${escHtml(step.statusText)}</span>
|
||||||
|
${isFinal ? '<span class="final-badge"><svg width="10" height="10" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>ФИНАЛ</span>' : ''}
|
||||||
|
<span class="text-xs font-mono ml-auto" style="color:var(--text-muted)">${step.time}ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="url-text"><a href="${escHtml(step.url)}" target="_blank" rel="noopener">${escHtml(step.url)}</a></div>
|
||||||
|
${step.location ? `<div class="text-xs font-mono mt-1" style="color:var(--text-muted)">Location: <span style="color:#6b9fff">${escHtml(step.location)}</span></div>` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
card.innerHTML = cardHtml;
|
||||||
|
container.appendChild(card);
|
||||||
|
|
||||||
|
// Arrow between steps
|
||||||
|
if (!isFinal && i < chain.length - 1) {
|
||||||
|
const next = chain[i + 1];
|
||||||
|
const arrow = document.createElement('div');
|
||||||
|
arrow.className = 'arrow-connector';
|
||||||
|
arrow.innerHTML = `
|
||||||
|
<div class="arrow-line"></div>
|
||||||
|
<span class="status-badge ${sc}">${escHtml(step.status)}</span>
|
||||||
|
<svg width="12" height="12" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 13.5L12 21m0 0l-7.5-7.5M12 21V3"/></svg>
|
||||||
|
<div class="arrow-line"></div>`;
|
||||||
|
container.appendChild(arrow);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSummary(data) {
|
||||||
|
const redirectCount = data.chain.filter(s => s.status >= 300 && s.status < 400).length;
|
||||||
|
document.getElementById('summaryBlock').innerHTML = `
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">${redirectCount}</div>
|
||||||
|
<div class="stat-label">Редиректов</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">${data.total_time}ms</div>
|
||||||
|
<div class="stat-label">Общее время</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val">${data.chain.length}</div>
|
||||||
|
<div class="stat-label">Всего шагов</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card">
|
||||||
|
<div class="stat-val ${data.loop_detected ? 'text-red-400' : ''}">${data.loop_detected ? 'ПЕТЛЯ' : 'OK'}</div>
|
||||||
|
<div class="stat-label">Петля</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIssues(data) {
|
||||||
|
const issuesBlock = document.getElementById('issuesBlock');
|
||||||
|
const issuesList = document.getElementById('issuesList');
|
||||||
|
const issues = data.issues || [];
|
||||||
|
const hasLoop = data.loop_detected;
|
||||||
|
|
||||||
|
const messages = [];
|
||||||
|
if (hasLoop) {
|
||||||
|
messages.push({ cls: 'issue-error', icon: '❌', text: 'Петля редиректов — URL уже встречался в цепочке' });
|
||||||
|
}
|
||||||
|
if (issues.includes('long_chain')) {
|
||||||
|
const n = data.chain.filter(s => s.status >= 300 && s.status < 400).length;
|
||||||
|
messages.push({ cls: 'issue-warn', icon: '⚠', text: `Chain too long — ${n} редиректов (рекомендуется не более 3)` });
|
||||||
|
}
|
||||||
|
if (issues.includes('mixed_protocol')) {
|
||||||
|
messages.push({ cls: 'issue-warn', icon: '⚠', text: 'Небезопасный редирект — переход с HTTPS на HTTP' });
|
||||||
|
}
|
||||||
|
if (issues.includes('302_not_301')) {
|
||||||
|
messages.push({ cls: 'issue-info', icon: 'ℹ', text: '302 вместо 301 — временный редирект не передаёт PageRank' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
issuesBlock.classList.add('hidden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
issuesBlock.classList.remove('hidden');
|
||||||
|
issuesList.innerHTML = messages.map(m =>
|
||||||
|
`<div class="issue-item ${m.cls}"><span>${m.icon}</span><span>${m.text}</span></div>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderHeaders(data) {
|
||||||
|
const container = document.getElementById('headersDetail');
|
||||||
|
const rows = data.chain.map((step, i) => {
|
||||||
|
const hdrs = Object.entries(step.headers || {});
|
||||||
|
const hdrRows = hdrs.map(([k,v]) => `<tr><td>${escHtml(k)}</td><td>${escHtml(v)}</td></tr>`).join('');
|
||||||
|
return `
|
||||||
|
<div class="px-4 pt-3 pb-1">
|
||||||
|
<p class="text-xs font-mono font-semibold mb-2" style="color:var(--text-muted)">Шаг ${i+1} — ${escHtml(step.status)} ${escHtml(step.url.length > 60 ? step.url.substring(0,60)+'...' : step.url)}</p>
|
||||||
|
<table class="headers-table mb-3">
|
||||||
|
<thead><tr><th>Заголовок</th><th>Значение</th></tr></thead>
|
||||||
|
<tbody>${hdrRows || '<tr><td colspan="2" style="color:var(--text-muted)">— нет заголовков —</td></tr>'}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
}).join('<hr style="border-color:var(--surface-600);margin:0">');
|
||||||
|
container.innerHTML = rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function analyze() {
|
||||||
|
let url = urlInput.value.trim();
|
||||||
|
if (!url) return;
|
||||||
|
// Auto-add protocol
|
||||||
|
if (!/^https?:\/\//i.test(url)) url = 'http://' + url;
|
||||||
|
|
||||||
|
analyzeBtn.disabled = true;
|
||||||
|
btnIcon.innerHTML = '<circle cx="12" cy="12" r="10" stroke="#fff" stroke-width="2" fill="none" stroke-dasharray="32" stroke-dashoffset="32" style="animation:spin .8s linear infinite"/>';
|
||||||
|
showState('loading');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/redirect-analyze', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ url, userAgent: uaSelect.value, method: methodSel.value }),
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
|
||||||
|
if (!resp.ok || data.error) {
|
||||||
|
document.getElementById('errorMsg').textContent = data.error || `HTTP ${resp.status}`;
|
||||||
|
showState('error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastResult = data;
|
||||||
|
|
||||||
|
// Final URL
|
||||||
|
const finalUrl = data.final_url || url;
|
||||||
|
document.getElementById('finalUrlText').innerHTML = `<a href="${escHtml(finalUrl)}" target="_blank" rel="noopener">${escHtml(finalUrl)}</a>`;
|
||||||
|
|
||||||
|
renderSummary(data);
|
||||||
|
renderIssues(data);
|
||||||
|
renderChain(data);
|
||||||
|
renderHeaders(data);
|
||||||
|
showState('result');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
document.getElementById('errorMsg').textContent = err.message;
|
||||||
|
showState('error');
|
||||||
|
} finally {
|
||||||
|
analyzeBtn.disabled = false;
|
||||||
|
btnIcon.innerHTML = '<path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803a7.5 7.5 0 0010.607 0z"/>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Events
|
||||||
|
analyzeBtn.addEventListener('click', analyze);
|
||||||
|
urlInput.addEventListener('keydown', e => { if (e.key === 'Enter') analyze(); });
|
||||||
|
|
||||||
|
// Copy final URL
|
||||||
|
document.getElementById('copyFinalUrl').addEventListener('click', () => {
|
||||||
|
if (!lastResult) return;
|
||||||
|
navigator.clipboard.writeText(lastResult.final_url || '').then(() => {
|
||||||
|
const btn = document.getElementById('copyFinalUrl');
|
||||||
|
btn.textContent = 'Скопировано!';
|
||||||
|
setTimeout(() => btn.textContent = 'Копировать', 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export
|
||||||
|
document.getElementById('btnCopyJson').addEventListener('click', () => {
|
||||||
|
if (!lastResult) return;
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(lastResult, null, 2)).then(() => {
|
||||||
|
const btn = document.getElementById('btnCopyJson');
|
||||||
|
btn.innerHTML = btn.innerHTML.replace('Копировать JSON','Скопировано!');
|
||||||
|
setTimeout(() => btn.innerHTML = btn.innerHTML.replace('Скопировано!','Копировать JSON'), 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnDownloadJson').addEventListener('click', () => {
|
||||||
|
if (!lastResult) return;
|
||||||
|
const blob = new Blob([JSON.stringify(lastResult, null, 2)], { type: 'application/json' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = 'redirect-chain.json';
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnDownloadCsv').addEventListener('click', () => {
|
||||||
|
if (!lastResult) return;
|
||||||
|
const rows = [['Step','URL','Status','StatusText','Location','Time(ms)']];
|
||||||
|
lastResult.chain.forEach((s, i) => {
|
||||||
|
rows.push([i+1, s.url, s.status, s.statusText, s.location||'', s.time]);
|
||||||
|
});
|
||||||
|
const csv = rows.map(r => r.map(c => '"'+String(c).replace(/"/g,'""')+'"').join(',')).join('\n');
|
||||||
|
const blob = new Blob([csv], { type: 'text/csv' });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = 'redirect-chain.csv';
|
||||||
|
a.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pre-fill from query param
|
||||||
|
const qp = new URLSearchParams(location.search);
|
||||||
|
if (qp.get('url')) { urlInput.value = qp.get('url'); analyze(); }
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
309
public/sanitizer.html
Normal file
309
public/sanitizer.html
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>HTML Sanitizer — images.wadevelop.ru</title>
|
||||||
|
<script src="/vendor/tailwind.js"></script>
|
||||||
|
<link href="/vendor/fonts.css" rel="stylesheet">
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
mono: ['JetBrains Mono', 'monospace'],
|
||||||
|
sans: ['Manrope', 'sans-serif'],
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
surface: { 900: '#060c18', 800: '#091020', 700: '#0d1828', 600: '#162040' },
|
||||||
|
accent: { DEFAULT: '#0054e6', dim: '#0043b8', bright: '#6b9fff' },
|
||||||
|
warn: '#ffd600',
|
||||||
|
danger: '#ff5252',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.sidebar{position:fixed;top:0;left:0;width:56px;height:100vh;display:flex;flex-direction:column;align-items:center;padding:16px 0 12px;z-index:50;border-right:1px solid var(--surface-600);background:var(--surface-800);backdrop-filter:blur(12px);transition:background .3s,border-color .3s}
|
||||||
|
.sidebar-logo{margin-bottom:24px;color:var(--text-muted);font-size:10px;font-weight:700;letter-spacing:.08em;font-family:'JetBrains Mono',monospace}
|
||||||
|
.sidebar-nav{display:flex;flex-direction:column;gap:6px;flex:1}
|
||||||
|
.sidebar-link{width:40px;height:40px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:var(--text-muted);transition:all .2s;text-decoration:none;position:relative}
|
||||||
|
.sidebar-link:hover{color:var(--text-secondary);background:rgba(255,255,255,.04)}
|
||||||
|
.sidebar-link.active{color:#0054e6;background:rgba(0,84,230,.08)}
|
||||||
|
.sidebar-link.active::before{content:'';position:absolute;left:-8px;top:50%;transform:translateY(-50%);width:3px;height:20px;border-radius:0 3px 3px 0;background:#0054e6}
|
||||||
|
.sidebar-bottom{margin-top:auto;display:flex;flex-direction:column;align-items:center;gap:8px}
|
||||||
|
.main-content{margin-left:56px}
|
||||||
|
@media(max-width:640px){
|
||||||
|
.sidebar{position:fixed;top:auto;bottom:0;left:0;width:100%;height:52px;flex-direction:row;justify-content:center;padding:0 16px;border-right:none;border-top:1px solid var(--surface-600);gap:0}
|
||||||
|
.sidebar-logo{display:none}
|
||||||
|
.sidebar-nav{flex-direction:row;gap:4px;flex:none}
|
||||||
|
.sidebar-bottom{margin-top:0;margin-left:auto;flex-direction:row}
|
||||||
|
.sidebar-link.active::before{left:50%;top:auto;bottom:-6px;transform:translateX(-50%);width:20px;height:3px;border-radius:3px 3px 0 0}
|
||||||
|
.main-content{margin-left:0;padding-bottom:60px}
|
||||||
|
}
|
||||||
|
:root{--bg:#f5f5f7;--bg-grad1:rgba(0,67,184,.04);--bg-grad2:rgba(0,150,60,.03);--surface-800:rgba(255,255,255,0.85);--surface-700:#f0f0f3;--surface-600:#dde0e6;--text-primary:#1a1a2e;--text-secondary:#555;--text-muted:#888;--select-bg:#f0f0f3;--select-color:#1a1a2e}
|
||||||
|
.dark{--bg:#060c18;--bg-grad1:rgba(0,84,230,.03);--bg-grad2:rgba(0,67,184,.02);--surface-800:rgba(9,16,32,0.8);--surface-700:#0d1828;--surface-600:#162040;--text-primary:#fff;--text-secondary:#c0c0c0;--text-muted:#666;--select-bg:#091020;--select-color:#e0e0e0}
|
||||||
|
body{background:var(--bg);background-image:radial-gradient(ellipse at 20% 50%,var(--bg-grad1) 0%,transparent 50%),radial-gradient(ellipse at 80% 20%,var(--bg-grad2) 0%,transparent 50%);min-height:100vh;transition:background .3s}
|
||||||
|
.dark .noise::before{content:'';position:fixed;inset:0;background:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E");pointer-events:none;z-index:0}
|
||||||
|
.theme-toggle{position:relative;width:44px;height:24px;border-radius:12px;cursor:pointer;transition:background .3s}
|
||||||
|
.dark .theme-toggle{background:#162040}.theme-toggle{background:#dde0e6}
|
||||||
|
.theme-toggle-knob{position:absolute;top:2px;width:20px;height:20px;border-radius:50%;transition:left .3s,background .3s;display:flex;align-items:center;justify-content:center}
|
||||||
|
.dark .theme-toggle-knob{left:22px;background:#0d1828}.theme-toggle-knob{left:2px;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.15)}
|
||||||
|
select,select option{background:var(--select-bg);color:var(--select-color)}
|
||||||
|
@keyframes fadeUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}}.fade-up{animation:fadeUp .4s ease-out forwards}.fade-up-delay{animation:fadeUp .4s ease-out .1s forwards;opacity:0}
|
||||||
|
.tool-btn{transition:all .2s}.tool-btn.active{color:#0054e6;background:rgba(0,84,230,.08);border-color:rgba(0,84,230,.3)}
|
||||||
|
.sub-btn.active{color:#0054e6;background:rgba(0,84,230,.12);border-color:rgba(0,84,230,.3)}
|
||||||
|
textarea{font-family:'JetBrains Mono',monospace;font-size:13px;resize:vertical}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="noise dark:text-gray-200 text-gray-700 font-sans antialiased">
|
||||||
|
|
||||||
|
<nav class="sidebar">
|
||||||
|
<div class="sidebar-logo">WA</div>
|
||||||
|
<div class="sidebar-nav">
|
||||||
|
<a href="/" class="sidebar-link" title="Главная"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg></a>
|
||||||
|
<a href="/compress" class="sidebar-link" title="Картинки"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909M3.75 21h16.5A2.25 2.25 0 0023.25 18.75V5.25A2.25 2.25 0 0021 3H3.75A2.25 2.25 0 001.5 5.25v13.5A2.25 2.25 0 003.75 21z"/></svg></a>
|
||||||
|
<a href="/md" class="sidebar-link" title="Markdown"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"/></svg></a>
|
||||||
|
<a href="/placeholder" class="sidebar-link" title="Placeholder"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5M14.25 3.104c.251.023.501.05.75.082M19 14.5l-2.47 2.47a2.25 2.25 0 01-1.59.659H9.06a2.25 2.25 0 01-1.591-.659L5 14.5m14 0V17a2.25 2.25 0 01-2.25 2.25H7.25A2.25 2.25 0 015 17v-2.5"/></svg></a>
|
||||||
|
<a href="/parser" class="sidebar-link" title="Parser"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m9.86-2.06a4.5 4.5 0 00-6.364-6.364L6.257 6.514a4.5 4.5 0 001.242 7.244"/></svg></a>
|
||||||
|
<a href="/password" class="sidebar-link" title="Password"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z"/></svg></a>
|
||||||
|
<a href="/sanitizer" class="sidebar-link active" title="Sanitizer"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75m-3-7.036A11.959 11.959 0 013.598 6 11.99 11.99 0 003 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285z"/></svg></a>
|
||||||
|
<a href="/converter" class="sidebar-link" title="Converter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21L3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5"/></svg></a>
|
||||||
|
<a href="/formatter" class="sidebar-link" title="Formatter"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M17.25 6.75L22.5 12l-5.25 5.25m-10.5 0L1.5 12l5.25-5.25m7.5-3l-4.5 16.5"/></svg></a>
|
||||||
|
<a href="/editor" class="sidebar-link" title="Editor"><svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0115.75 21H5.25A2.25 2.25 0 013 18.75V8.25A2.25 2.25 0 015.25 6H10"/></svg></a>
|
||||||
|
<a href="/httpclient" class="sidebar-link" title="HTTP Client">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/redirects" class="sidebar-link" title="Redirect Analyzer">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="/svgeditor" class="sidebar-link" title="SVG Редактор">
|
||||||
|
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 00-5.78 1.128 2.25 2.25 0 01-2.4 2.245 4.5 4.5 0 008.4-2.245c0-.399-.078-.78-.22-1.128zm0 0a15.998 15.998 0 003.388-1.62m-5.043-.025a15.994 15.994 0 011.622-3.395m3.42 3.42a15.995 15.995 0 004.764-4.648l3.876-5.814a1.151 1.151 0 00-1.597-1.597L14.146 6.32a15.996 15.996 0 00-4.649 4.763m3.42 3.42a6.776 6.776 0 00-3.42-3.42"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-bottom">
|
||||||
|
<div class="theme-toggle" id="themeToggle" title="Переключить тему">
|
||||||
|
<div class="theme-toggle-knob">
|
||||||
|
<svg class="w-3 h-3 dark:hidden text-amber-500" fill="currentColor" viewBox="0 0 20 20"><path d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"/></svg>
|
||||||
|
<svg class="w-3 h-3 hidden dark:block text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="main-content">
|
||||||
|
<div class="relative z-10 max-w-3xl mx-auto px-4 py-8 sm:py-12">
|
||||||
|
|
||||||
|
<div class="text-center mb-8 fade-up">
|
||||||
|
<div class="inline-flex items-center gap-2 mb-3 px-3 py-1 rounded-full border dark:border-surface-600 border-gray-300 dark:bg-surface-800/60 bg-white/60 text-xs font-mono dark:text-gray-400 text-gray-500 tracking-wide">
|
||||||
|
<span class="w-1.5 h-1.5 rounded-full bg-accent animate-pulse"></span>
|
||||||
|
XSS Protection · Client-side
|
||||||
|
</div>
|
||||||
|
<h1 class="text-3xl sm:text-4xl font-extrabold tracking-tight dark:text-white text-gray-900">
|
||||||
|
HTML <span class="text-accent">Sanitizer</span>
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-sm dark:text-gray-500 text-gray-400 font-mono">очистка, извлечение текста, entities, минификация</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tool Grid -->
|
||||||
|
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2 mb-5 fade-up-delay">
|
||||||
|
<button class="tool-btn active px-3 py-2.5 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="sanitize">
|
||||||
|
<div class="text-xs font-semibold">Sanitize</div>
|
||||||
|
<div class="text-[10px] dark:text-gray-600 text-gray-400 mt-0.5">XSS cleanup</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn px-3 py-2.5 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="htmlToText">
|
||||||
|
<div class="text-xs font-semibold">HTML → Text</div>
|
||||||
|
<div class="text-[10px] dark:text-gray-600 text-gray-400 mt-0.5">strip tags</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn px-3 py-2.5 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="entities">
|
||||||
|
<div class="text-xs font-semibold">Entities</div>
|
||||||
|
<div class="text-[10px] dark:text-gray-600 text-gray-400 mt-0.5">encode / decode</div>
|
||||||
|
</button>
|
||||||
|
<button class="tool-btn px-3 py-2.5 rounded-xl text-center border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 cursor-pointer" data-tool="minify">
|
||||||
|
<div class="text-xs font-semibold">Minify</div>
|
||||||
|
<div class="text-[10px] dark:text-gray-600 text-gray-400 mt-0.5">compress html</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processing Card -->
|
||||||
|
<div class="dark:bg-surface-800/80 bg-white/85 border dark:border-surface-600 border-gray-200 rounded-2xl p-5 sm:p-7 backdrop-blur-sm fade-up-delay shadow-sm dark:shadow-none">
|
||||||
|
|
||||||
|
<div id="entityOpts" class="flex gap-2 mb-4 hidden">
|
||||||
|
<button class="sub-btn active flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors cursor-pointer" data-sub="encode">Encode</button>
|
||||||
|
<button class="sub-btn flex-1 px-3 py-2 rounded-lg text-xs font-medium border dark:border-surface-600 border-gray-200 dark:text-gray-400 text-gray-500 transition-colors cursor-pointer" data-sub="decode">Decode</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-xs font-medium dark:text-gray-500 text-gray-400 mb-1.5 font-mono uppercase tracking-wider">Input</label>
|
||||||
|
<textarea id="inputArea" rows="8" placeholder="Вставьте HTML для очистки..."
|
||||||
|
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none focus:border-accent/50 transition-colors"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button id="processBtn"
|
||||||
|
class="w-full py-3 rounded-xl font-semibold text-sm tracking-wide transition-all duration-300
|
||||||
|
bg-accent text-white border border-accent cursor-pointer
|
||||||
|
hover:bg-accent-bright hover:shadow-lg hover:shadow-accent/20 active:scale-[0.98]">
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div id="outputBlock" class="mt-5 hidden">
|
||||||
|
<div class="flex items-center justify-between mb-1.5">
|
||||||
|
<label class="text-xs font-medium dark:text-gray-500 text-gray-400 font-mono uppercase tracking-wider">Output</label>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span id="stats" class="text-[10px] font-mono dark:text-gray-600 text-gray-400"></span>
|
||||||
|
<button id="copyBtn" class="px-3 py-1 rounded-lg text-xs font-medium bg-accent/10 text-accent hover:bg-accent/20 transition-colors border border-accent/20">Копировать</button>
|
||||||
|
<button id="clearBtn" class="px-3 py-1 rounded-lg text-xs font-medium dark:bg-surface-700 bg-gray-100 dark:text-gray-400 text-gray-500 hover:text-danger transition-colors border dark:border-surface-600 border-gray-200">Очистить</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<textarea id="outputArea" rows="10" readonly
|
||||||
|
class="w-full px-4 py-3 rounded-xl dark:bg-surface-700 bg-gray-50 border dark:border-surface-600 border-gray-200 dark:text-gray-200 text-gray-700 focus:outline-none transition-colors"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const html = document.documentElement;
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme) { html.className = savedTheme; }
|
||||||
|
else if (!window.matchMedia('(prefers-color-scheme: dark)').matches) { html.className = ''; }
|
||||||
|
document.getElementById('themeToggle').addEventListener('click', () => {
|
||||||
|
const isDark = html.classList.toggle('dark');
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : '');
|
||||||
|
});
|
||||||
|
|
||||||
|
const TOOLS = {
|
||||||
|
sanitize: { btn: 'Очистить', ph: 'Вставьте HTML для очистки...' },
|
||||||
|
htmlToText: { btn: 'Извлечь текст', ph: 'Вставьте HTML...' },
|
||||||
|
entities: { btn: 'Конвертировать', ph: 'Вставьте текст или HTML entities...', hasSub: true },
|
||||||
|
minify: { btn: 'Минифицировать', ph: 'Вставьте HTML...' },
|
||||||
|
};
|
||||||
|
|
||||||
|
let activeTool = 'sanitize';
|
||||||
|
let entityMode = 'encode';
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.tool-btn[data-tool]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
activeTool = btn.dataset.tool;
|
||||||
|
const t = TOOLS[activeTool];
|
||||||
|
$('processBtn').textContent = t.btn;
|
||||||
|
$('inputArea').placeholder = t.ph;
|
||||||
|
$('entityOpts').classList.toggle('hidden', !t.hasSub);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sub-btn[data-sub]').forEach(btn => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
document.querySelectorAll('.sub-btn[data-sub]').forEach(b => b.classList.remove('active'));
|
||||||
|
btn.classList.add('active');
|
||||||
|
entityMode = btn.dataset.sub;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('processBtn').addEventListener('click', () => {
|
||||||
|
const input = $('inputArea').value;
|
||||||
|
if (!input.trim()) return;
|
||||||
|
let output = '';
|
||||||
|
try {
|
||||||
|
switch (activeTool) {
|
||||||
|
case 'sanitize': output = sanitizeHtml(input); break;
|
||||||
|
case 'htmlToText': output = htmlToText(input); break;
|
||||||
|
case 'entities': output = entityMode === 'encode' ? entityEncode(input) : entityDecode(input); break;
|
||||||
|
case 'minify': output = minifyHtml(input); break;
|
||||||
|
}
|
||||||
|
} catch (e) { output = 'Error: ' + e.message; }
|
||||||
|
$('outputArea').value = output;
|
||||||
|
$('outputBlock').classList.remove('hidden');
|
||||||
|
const diff = output.length - input.length;
|
||||||
|
$('stats').textContent = `${input.length} \u2192 ${output.length} (${diff > 0 ? '+' : ''}${diff})`;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('copyBtn').addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText($('outputArea').value).then(() => {
|
||||||
|
const b = $('copyBtn'); const o = b.textContent;
|
||||||
|
b.textContent = '\u0421\u043a\u043e\u043f\u0438\u0440\u043e\u0432\u0430\u043d\u043e!'; setTimeout(() => { b.textContent = o; }, 1500);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$('clearBtn').addEventListener('click', () => {
|
||||||
|
$('inputArea').value = ''; $('outputArea').value = ''; $('outputBlock').classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================== Sanitize ====================
|
||||||
|
function sanitizeHtml(html) {
|
||||||
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
const SAFE = new Set(['p','br','hr','b','i','em','strong','u','s','del','ins','a','img','ul','ol','li','dl','dt','dd','h1','h2','h3','h4','h5','h6','blockquote','pre','code','kbd','samp','var','table','thead','tbody','tfoot','tr','th','td','caption','figure','figcaption','span','div','section','article','aside','header','footer','main','nav','sub','sup','mark','small','abbr','time','details','summary']);
|
||||||
|
const ATTRS = new Set(['href','src','alt','title','class','id','width','height','target','rel','colspan','rowspan','datetime','lang','dir','loading','srcset','sizes']);
|
||||||
|
const KILL = new Set(['script','style','iframe','object','embed','applet','form','input','textarea','select','button','link','meta','base','noscript']);
|
||||||
|
|
||||||
|
(function clean(parent) {
|
||||||
|
for (const node of Array.from(parent.childNodes)) {
|
||||||
|
if (node.nodeType !== 1) continue;
|
||||||
|
const tag = node.tagName.toLowerCase();
|
||||||
|
if (KILL.has(tag)) { node.remove(); continue; }
|
||||||
|
if (!SAFE.has(tag)) {
|
||||||
|
while (node.firstChild) parent.insertBefore(node.firstChild, node);
|
||||||
|
node.remove(); continue;
|
||||||
|
}
|
||||||
|
for (const attr of Array.from(node.attributes)) {
|
||||||
|
if (attr.name.startsWith('on') || !ATTRS.has(attr.name)) { node.removeAttribute(attr.name); continue; }
|
||||||
|
if ((attr.name === 'href' || attr.name === 'src') && /^(javascript|vbscript|data:text\/html)/i.test(attr.value.trim())) {
|
||||||
|
node.removeAttribute(attr.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tag === 'a' && node.getAttribute('target')) node.setAttribute('rel', 'noopener noreferrer');
|
||||||
|
clean(node);
|
||||||
|
}
|
||||||
|
})(doc.body);
|
||||||
|
return doc.body.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== HTML → Text ====================
|
||||||
|
function htmlToText(html) {
|
||||||
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
||||||
|
function walk(node) {
|
||||||
|
if (node.nodeType === 3) return node.textContent;
|
||||||
|
if (node.nodeType !== 1) return '';
|
||||||
|
const tag = node.tagName.toLowerCase();
|
||||||
|
if (tag === 'script' || tag === 'style') return '';
|
||||||
|
const inner = Array.from(node.childNodes).map(walk).join('');
|
||||||
|
switch (tag) {
|
||||||
|
case 'br': return '\n';
|
||||||
|
case 'hr': return '\n---\n';
|
||||||
|
case 'p': case 'div': case 'section': case 'article': return '\n\n' + inner.trim() + '\n\n';
|
||||||
|
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': return '\n\n' + inner.trim() + '\n\n';
|
||||||
|
case 'li': return '\n - ' + inner.trim();
|
||||||
|
case 'ul': case 'ol': return '\n' + inner + '\n';
|
||||||
|
case 'blockquote': return '\n> ' + inner.trim().split('\n').join('\n> ') + '\n';
|
||||||
|
case 'pre': return '\n\n' + node.textContent + '\n\n';
|
||||||
|
case 'tr': return inner + '\n';
|
||||||
|
case 'td': case 'th': return inner + '\t';
|
||||||
|
default: return inner;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return walk(doc.body).replace(/\n{3,}/g, '\n\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Entities ====================
|
||||||
|
function entityEncode(text) {
|
||||||
|
return text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,''');
|
||||||
|
}
|
||||||
|
function entityDecode(text) {
|
||||||
|
const el = document.createElement('textarea');
|
||||||
|
el.innerHTML = text;
|
||||||
|
return el.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Minify ====================
|
||||||
|
function minifyHtml(html) {
|
||||||
|
return html.replace(/<!--[\s\S]*?-->/g,'').replace(/\s+/g,' ').replace(/>\s+</g,'><').replace(/\s+>/g,'>').replace(/<\s+/g,'<').trim();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2455
public/svgeditor.html
Normal file
2455
public/svgeditor.html
Normal file
File diff suppressed because it is too large
Load Diff
63
public/vendor/fonts.css
vendored
Normal file
63
public/vendor/fonts.css
vendored
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/jetbrains-400.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/jetbrains-500.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/jetbrains-600.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/jetbrains-700.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/manrope-400.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/manrope-500.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/manrope-600.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/manrope-700.ttf) format('truetype');
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 800;
|
||||||
|
font-display: swap;
|
||||||
|
src: url(/vendor/fonts/manrope-800.ttf) format('truetype');
|
||||||
|
}
|
||||||
BIN
public/vendor/fonts/jetbrains-400.ttf
vendored
Normal file
BIN
public/vendor/fonts/jetbrains-400.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/jetbrains-500.ttf
vendored
Normal file
BIN
public/vendor/fonts/jetbrains-500.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/jetbrains-600.ttf
vendored
Normal file
BIN
public/vendor/fonts/jetbrains-600.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/jetbrains-700.ttf
vendored
Normal file
BIN
public/vendor/fonts/jetbrains-700.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/manrope-400.ttf
vendored
Normal file
BIN
public/vendor/fonts/manrope-400.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/manrope-500.ttf
vendored
Normal file
BIN
public/vendor/fonts/manrope-500.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/manrope-600.ttf
vendored
Normal file
BIN
public/vendor/fonts/manrope-600.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/manrope-700.ttf
vendored
Normal file
BIN
public/vendor/fonts/manrope-700.ttf
vendored
Normal file
Binary file not shown.
BIN
public/vendor/fonts/manrope-800.ttf
vendored
Normal file
BIN
public/vendor/fonts/manrope-800.ttf
vendored
Normal file
Binary file not shown.
1213
public/vendor/highlight.min.js
vendored
Normal file
1213
public/vendor/highlight.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
10
public/vendor/hljs-github-dark.css
vendored
Normal file
10
public/vendor/hljs-github-dark.css
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||||
|
Theme: GitHub Dark
|
||||||
|
Description: Dark theme as seen on github.com
|
||||||
|
Author: github.com
|
||||||
|
Maintainer: @Hirse
|
||||||
|
Updated: 2021-05-15
|
||||||
|
|
||||||
|
Outdated base version: https://github.com/primer/github-syntax-dark
|
||||||
|
Current colors taken from GitHub's CSS
|
||||||
|
*/.hljs{color:#c9d1d9;background:#0d1117}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#ff7b72}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#d2a8ff}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#79c0ff}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#a5d6ff}.hljs-built_in,.hljs-symbol{color:#ffa657}.hljs-code,.hljs-comment,.hljs-formula{color:#8b949e}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#7ee787}.hljs-subst{color:#c9d1d9}.hljs-section{color:#1f6feb;font-weight:700}.hljs-bullet{color:#f2cc60}.hljs-emphasis{color:#c9d1d9;font-style:italic}.hljs-strong{color:#c9d1d9;font-weight:700}.hljs-addition{color:#aff5b4;background-color:#033a16}.hljs-deletion{color:#ffdcd7;background-color:#67060c}
|
||||||
10
public/vendor/hljs-github.css
vendored
Normal file
10
public/vendor/hljs-github.css
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
|
||||||
|
Theme: GitHub
|
||||||
|
Description: Light theme as seen on github.com
|
||||||
|
Author: github.com
|
||||||
|
Maintainer: @Hirse
|
||||||
|
Updated: 2021-05-15
|
||||||
|
|
||||||
|
Outdated base version: https://github.com/primer/github-syntax-light
|
||||||
|
Current colors taken from GitHub's CSS
|
||||||
|
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}
|
||||||
69
public/vendor/marked.min.js
vendored
Normal file
69
public/vendor/marked.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/vendor/opentype.min.js
vendored
Normal file
2
public/vendor/opentype.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
83
public/vendor/tailwind.js
vendored
Normal file
83
public/vendor/tailwind.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user