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