sync: full site from notebook (source of truth) + missing media

This commit is contained in:
root committed 2026-07-24 20:20:51 +02:00
commit 5f18505943
505 files changed
+35927

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
# Apply content edits
Примени правки из режима content-edit на localhost.
## Обязательно
1. Прочитай и выполни **`.cursor/rules/content-edit-apply.mdc`**.
2. Прочитай **`texts/content-edits/pending.json`**.
3. Если `items` пуст — кратко сообщи, что очередь пуста, и остановись.
4. Иначе для каждого пункта с непустым `instruction` (по возрастанию `n`):
- правь только **`src/pages/…`** (не `public/`);
- основной блок: `target.selector` → `target.content` / `snippet` → `anchorId`;
- дополнительные блоки из `refs[]` — тот же порядок; метки `«…»` в instruction ↔ `refs[].label`;
- выполни `instruction`.
5. Русский текст после правок — скилл **`humanizer-ru`**.
6. Запусти **`npm run build:pages`**.
7. **`pending.json` не очищать** — очередь остаётся для проверки; пользователь сам удаляет пункты в dock или правит instruction и снова вызывает apply. По желанию обнови только `lastAppliedAt` (ISO).
## Не делать
- Не очищать `items` в `pending.json` после apply.
- Не править `public/cases/*.html` вручную (только через сборку).
- Не путать с режимом «правь в public» — см. `case-content-edit-in-public.mdc`.
Если в сообщении указана страница (slug или путь) — примени только её `item.page`.
В ответе пользователю — одна короткая строка или «готово», без списка правок, если он не просил отчёт.
File diff suppressed because it is too large. Load diff
+26
View File
@@ -0,0 +1,26 @@
# Dependencies
node_modules/
# Build outputs
dist/
build/
coverage/
.cache/
# IDE/editor local files (команды — в репо, см. .cursor/commands/)
.cursor/*
!.cursor/commands/
!.cursor/commands/**
.vscode/
gitverse/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# OS files
.DS_Store
Thumbs.db
+62
View File
@@ -0,0 +1,62 @@
# Подсказки для агента (Cursor)
## GitVerse / деплой статики
Полный чеклист: **`docs/gitverse-sync.md`** → раздел **«Инструкция для агента (Cursor)»**.
Деплой на VPS: **`docs/vps-deploy.md`**.
Кратко по запросу пользователя «выложить на GitVerse», «синк gitverse», «задеплоить»:
1. При правках `src/pages/**` или `src/partials/**` — `npm run build:pages`; при смене шрифтов/Plyr или `package.json` — `npm run build` (включает `vendor:copy`).
2. Затем `npm run sync-gitverse` (разрешения **network** и **git_write**).
3. Не запускать синк без явного запроса пользователя.
По запросу «задеплой в Coolify», «задеплой на VPS»:
1. При правках в `public/` — `npm run deploy:vps` (разрешения **all**).
2. По запросу «задеплой везде» — `npm run deploy` (VPS + GitVerse).
Не запускать деплой на VPS без явного запроса: скрипт перезаписывает `/data/static-site` на сервере.
## Git: корневой репозиторий
Этот репозиторий в корне проекта **только локальный** (рабочая копия, история коммитов для себя). **Не добавлять `origin` и не пушить** в удалённый репозиторий из агента, если пользователь явно не попросил настроить remote и не дал URL. Выгрузка наружу — отдельный сценарий через **`npm run sync-gitverse`** в папку `gitverse` (см. выше), а не `git push` из корня.
### Влить в `master`, оставаясь на текущей ветке
Запрос «влить в мастер» **не означает** `git checkout master`, если пользователь не просил переключиться.
- **Fast-forward** (ветка впереди `master`): на текущей ветке — `git branch -f master HEAD` или `git push . HEAD:master`.
- **Нужен merge-коммит** (`master` ушёл вперёд): `git checkout master` → `git merge <ветка>` → `git checkout -` (вернуться на прежнюю ветку).
- После влития **не меняй** активную ветку в IDE без запроса; при необходимости уточни, на какой ветке продолжать работу.
## Design System
Страница-каталог: **`src/pages/cases/design-system.html`** → после сборки `public/cases/design-system.html`.
При **глобальных** правках стилей (токены в `public/css/main.css`, компоненты главной, блоки лонгрида кейса) **обновляй Design System**: живой пример и краткое описание в соответствующей секции, при необходимости — `docs/case-page-blocks.md`.
## Контент кейса в public (по запросу)
Обычный путь: `src/pages/cases/` → `npm run build:pages`. Если пользователь явно просит править **собранную** страницу под `npm run serve`, см. **`.cursor/rules/case-content-edit-in-public.mdc`**: правки в `.case-article` в `public/cases/`, синхронизация в `src` по запросу.
## Русские тексты (humanizer-ru)
При генерации и правке **русскоязычного контента** для сайта (кейсы, главная, `texts/**`, callout, meta) — читать скилл **`humanizer-ru`** и вычитывать текст по нему перед отдачей пользователю. Подробности: **`.cursor/rules/humanizer-ru-text.mdc`**.
## Правки контента (content-edits, localhost)
Режим на странице при `npm run serve` → очередь в **`texts/content-edits/pending.json`**. Apply по запросу **«примени правки»**: читать этот файл, править **`src/pages/`**, **humanizer-ru**, **`npm run build:pages`**; **`pending.json` не очищать** (пользователь удаляет пункты сам). Подробности: **`docs/content-edit-mode.md`**, **`.cursor/rules/content-edit-apply.mdc`**.
## Vendor (шрифты, Plyr)
Локально в `public/`, без CDN. Обновление: **`docs/vendor-assets.md`**, команда `npm run vendor:copy`.
## Медиа на сайте (WebP / WebM)
По запросу «добавить/обновить картинку или видео» в разметке только **WebP** (изображения, постеры) и **WebM** (видео); исходник конвертировать скриптами, не ждать отдельного уточнения. Подробности: **`.cursor/rules/media-webp-webm-only.mdc`**, `docs/image-optimization.md`, `docs/video-conversion.md`.
## Остальное
Правила и таблица доков: **`.cursor/rules/read-docs-before-feature-dev.mdc`** (локально; папка `.cursor/` может быть в `.gitignore`).
+7
View File
@@ -0,0 +1,7 @@
FROM nginx:latest
# Fix mjs MIME type — without this plyr.min.mjs fails to load as ES module
RUN sed -i 's|application/javascript *js;|application/javascript js mjs;|' /etc/nginx/mime.types
COPY ./public /usr/share/nginx/html
RUN chown -R nginx:nginx /usr/share/nginx/html
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -e
HOST="root@155.212.186.39"
SSH_KEY="$HOME/.ssh/beget"
SRC_DIR="$(cd "$(dirname "$0")" && pwd)/public/"
echo "📦 Deploy mamadaevv.site..."
echo "Src: $SRC_DIR"
REMOTE_INFO=$(ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$HOST" '
CONTAINER=$(docker ps --format "{{.Names}}" | grep "s9mrcps8xw02r1dak0fsznix" | head -1)
VOLUME_PATH=$(docker inspect "$CONTAINER" 2>/dev/null | python3 -c "
import sys,json
mounts=json.load(sys.stdin)[0].get(\"Mounts\",[])
print([m[\"Source\"] for m in mounts if m[\"Destination\"]==\"/usr/share/nginx/html\"][0])
")
echo "$CONTAINER|$VOLUME_PATH"
')
IFS='|' read -r CONTAINER VOLUME_PATH <<< "$REMOTE_INFO"
echo "Container: $CONTAINER"
echo "Volume: $VOLUME_PATH"
# Копируем файлы
cd "$SRC_DIR"
tar cf - . | ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$HOST" \
"tar xf - -C $VOLUME_PATH"
# Фикс MIME для .mjs (plyr.min.mjs модуль)
ssh -i "$SSH_KEY" -o StrictHostKeyChecking=no "$HOST" \
"docker exec $CONTAINER sh -c \"
if ! grep -q 'application/javascript mjs' /etc/nginx/mime.types 2>/dev/null; then
sed -i 's|application/wasm|application/javascript mjs;\\n application/wasm|' /etc/nginx/mime.types
nginx -s reload && echo 'MIME fix applied'
fi
\"" 2>/dev/null || true
echo ""
echo "✅ Done! https://mamadaevv.site"
+199
View File
@@ -0,0 +1,199 @@
# Страница кейса: каркас и блоки контента
Инструкция завязана на эталон **`src/pages/cases/template-elements-demo.html`**: оттуда копируются готовые куски разметки. Текстовый дубль структуры для редактуры — `texts/cases/template-elements-demo.md`.
## Где править
| Что | Путь |
|-----|------|
| Разметка кейса | `src/pages/cases/<имя>.html` |
| Общие вставки (шапка кейса, футер, скрипты) | `src/partials/*.html` |
| Сборка в статику | `npm run build:pages` → `public/cases/<имя>.html` |
**`public/cases/*.html` не редактировать вручную** — они перезаписываются сборкой.
Локальные инструкции агенту без правки HTML в браузере: режим правок при `npm run serve` → [`docs/content-edit-mode.md`](content-edit-mode.md) → `texts/content-edits/*.json` → apply в `src` по запросу в чате.
## Каркас `body` (как в template)
Порядок важен для единообразия с остальными кейсами:
1. `{{> header-case}}` — шапка с ссылкой на главную (внутри partial используется `{{assetPrefix}}`).
2. `{{> fab-stack}}` — плавающие кнопки темы и т.п.
3. `<main id="case-main" class="section section--tight">` → внутри `<div class="section__inner case-article">` — **весь уникальный контент кейса**.
4. Нижние секции как на главной: `{{> projects-section}}`, `{{> contact-section}}`, `{{> footer-spacer}}`, `{{> footer}}`, `{{> cookie-notice}}`, `{{> nav-more-floating}}`.
5. Скрипты подключаются в `<head>` через `{{> head-links}}` (preload + `main.js`, `skill-connectors.js`).
`head` у кейсов: `meta`, `viewport`, `description`, `title`, затем `{{> head-links}}` (preconnect/preload шрифтов и CSS/JS, стили, скрипты с `defer`), при необходимости локальный `<style>` для `.case-article`.
## Пути к CSS и медиа
Файл страницы лежит в **`cases/`**, один уровень ниже корня сайта. В итоговом HTML пути от страницы:
- Стили: `../css/main.css`
- Картинки и видео проекта: `../assets/<папка-проекта>/файл.ext`
В **partials** для ссылок на корень сайта используй **`{{assetPrefix}}`** (при сборке из `cases/*` подставится `../`). В самой странице кейса чаще пишут литерал `../`, как в template.
## Текст и заголовки
Внутри `.case-article`:
- Вводная строка: `<p class="hero__eyebrow">…</p>`
- Заголовок: `<h1 class="case-page__h1">…</h1>`
- Подзаголовок (по желанию): `<p class="case-page__sub">…</p>`
- Обычные абзацы: `<p class="section__prose">…</p>`
- Секции: `<h2>`, при необходимости `<h3>`
Списки — обычные `<ul>` / `<ol>` без обязательных классов (стили задаёт вложенный CSS страницы для `.case-article`).
## Блоки (копировать из template)
### Сетка карточек ссылок
- Обертка: `<div class="case-links-grid">`.
- Одна карточка: `<a class="case-link-card" href="…" target="_blank" rel="noopener noreferrer">`.
- Внутри: `<span class="case-link-card__media">` (превью-картинка или favicon) и `<span class="case-link-card__body">` с `case-link-card__title`, `case-link-card__meta`, `case-link-card__url`.
- Вариант без тяжёлого превью: на `<a>` добавить модификатор **`case-link-card--favicon`**, в `media` — картинка с `https://www.google.com/s2/favicons?domain=…`.
### Изображения (`figure.case-media`)
| Модификатор | Поведение |
|-------------|-----------|
| `case-media--natural` | Натуральный масштаб (визуально ~половина логического размера, см. `initCaseNaturalImagesHalfSize` в `main.js`). **Без** `data-image-viewer`. |
| `case-media--content` | По ширине текстовой колонки; клик — полноэкранный просмотр, если на `<img>` есть **`data-image-viewer`** (обводка и hover как у `.case-video-shell`). |
| `case-media--wide` | Чуть шире колонки; тоже с **`data-image-viewer`** при необходимости просмотра. |
Общие атрибуты для картинок в кейсе: `loading="lazy"`, `decoding="async"`, осмысленный `alt`, при известных размерах — `width` и `height`.
Подпись: `<figcaption>…</figcaption>`. У блока с `.case-video-shell` подпись по центру под видео (как у кадров с `data-image-viewer`).
### Длинный скрин (`data-case-tall-media`)
Для **очень высокого** кадра (полный экран приложения, длинная страница): в свёрнутом виде показывается только верх, внизу превью — плотный градиент («туман»), чтобы было видно, что картинка **не целиком**. Рамка как у кликабельных кадров. Кнопка «Развернуть» / «Свернуть» с шевроном поверх нижней части превью (внутри рамки); после сворачивания — прокрутка к превью (`scrollIntoView` на clip).
**Ширина:** на `figure` с `data-case-tall-media` задай **`data-case-tall-width="content"`** (по ширине текстовой колонки) или **`data-case-tall-width="wide"`** (как у обычной «широкой» картинки: вынос за колонку). Допустим и старый вариант без атрибута: **`class="case-media case-media--wide"`** на `figure` — эквивалентно `wide`. Не смешивай `data-case-tall-width="content"` с классом `case-media--wide` на одном `figure`.
Стили: `.case-media-tall*` и блок ширины в `public/css/main.css`. Логика: [`public/js/modules/cases/tall-media-expand.js`](public/js/modules/cases/tall-media-expand.js) (`initCaseTallMediaExpand` уже вызывается из [`public/js/main.js`](public/js/main.js)).
**Как вставить в кейс**
1. На **`figure`** поставь **`class="case-media"`**, **`data-case-tall-media`**, **`data-case-tall-width="content"`** или **`wide`**.
2. Внутри — разметка ниже: уникальный **`id`** у `.case-media-tall__clip`, тот же id в **`aria-controls`** у кнопки; подписи «Развернуть»/«Свернуть» в span с **`data-case-tall-label-more`** / **`data-case-tall-label-less`** (у «Свернуть» изначально **`hidden`**).
3. На **`img`** оставь **`data-image-viewer`**, если нужен полноэкранный просмотр по клику.
4. Высоту превью (обрезки) задай на clip, например inline: **`style="--case-tall-max: min(60vh, 28rem)"`**. Если не задавать, в CSS по умолчанию `min(70vh, 32rem)`.
5. Файлы клади в **`public/assets/<папка-кейса>/…`**; в HTML страниц кейсов пути **`../assets/<папка>/файл.webp`** (относительно `public/cases/`).
**Сниппет** (подставь `УНИКАЛЬНЫЙ-ID`, путь к WebP, `alt`, при необходимости замени **`wide`** на **`content`**):
```html
<figure class="case-media" data-case-tall-media data-case-tall-width="wide">
<div class="case-media-tall" data-case-tall-root>
<div
class="case-media-tall__clip"
id="УНИКАЛЬНЫЙ-ID"
data-case-tall-clip
style="--case-tall-max: min(70vh, 32rem)"
>
<img
src="../assets/<папка-кейса>/<файл>.webp"
alt="…"
width="1200"
loading="lazy"
decoding="async"
data-image-viewer
/>
<div class="case-media-tall__fog" aria-hidden="true"></div>
</div>
<div class="case-media-tall__actions">
<button
type="button"
class="case-media-tall__btn btn btn--ghost btn--flat"
data-case-tall-toggle
aria-expanded="false"
aria-controls="УНИКАЛЬНЫЙ-ID"
>
<span class="case-media-tall__btn-chev" aria-hidden="true"></span>
<span data-case-tall-label-more>Развернуть</span>
<span data-case-tall-label-less hidden>Свернуть</span>
</button>
</div>
</div>
<figcaption>…</figcaption>
</figure>
```
Живой пример: [`src/pages/cases/template-elements-demo.html`](src/pages/cases/template-elements-demo.html) (секция «Длинный скрин»); второй кейс — [`src/pages/cases/ai-platform-primo-rpa.html`](src/pages/cases/ai-platform-primo-rpa.html).
### Галерея изображений сеткой (`case-media-gallery`)
Сетка из миниатюр с тем же полноэкранным просмотром, что у одиночных картинок (`data-image-viewer` на каждом `<img>`). Стили в `public/css/main.css` (блок `.case-media-gallery`).
**Разметка:**
- Обертка: `figure.case-media.case-media-gallery` (+ опционально `case-media-gallery--wide`).
- Внутри: `div.case-media-gallery__grid` → для каждой ячейки `div.case-media-gallery__item` → `<img … data-image-viewer />`.
- Число колонок на **широком** экране задаётся на `figure`, например: `style="--case-gallery-cols: 3"` (целое ≥ 1). Если атрибут не указан, по умолчанию в CSS задано `3`.
- **Ширина:** без модификатора — как обычный блок в колонке статьи; класс **`case-media-gallery--wide`** — выход за колонку по той же схеме, что у `case-media--wide` (viewport с боковыми отступами). На узких экранах (`max-width: 640px`) wide-галерея схлопывается в ширину колонки.
- **Адаптив сетки:** примерно до `48rem` — не больше двух колонок (`min(2, var(--case-gallery-cols))`); до `28rem` — одна колонка.
Пример целиком см. в [`src/pages/cases/template-elements-demo.html`](src/pages/cases/template-elements-demo.html) (два варианта: контентная ширина и wide).
**Сетка + отложенный `src`:** в ячейках можно задать реальный URL в **`data-src`** и плейсхолдер в `src` (крошечный `data:image/gif;base64,…`), чтобы [`initCaseSequentialImages`](public/js/modules/cases/sequential-images.js) подставлял картинки **по порядку в DOM** (в т.ч. строка за строкой в галерее). Пока у `img` есть `data-src`, просмотр по клику не открывается. Пример: [`src/pages/cases/lunacy.html`](src/pages/cases/lunacy.html).
### Последовательная подгрузка (`data-case-sequential-images`)
- Оберни фрагмент статьи с картинками (часто весь `.case-article`): **`data-case-sequential-images`** на контейнере.
- У каждого кадра: **`data-src="../assets/…/файл.webp"`** и временный **`src`** — прозрачный GIF 1×1 (тот же паттерн, что в Lunacy-кейсе).
- Логика: [`public/js/modules/cases/sequential-images.js`](public/js/modules/cases/sequential-images.js), вызов **`initCaseSequentialImages()`** в [`public/js/main.js`](public/js/main.js) **до** `initCaseImageViewer()`.
- Просмотр: [`public/js/modules/cases/image-viewer.js`](public/js/modules/cases/image-viewer.js) слушает клики **делегированием**; не открывает плейсхолдер и не открывает, пока не снят `data-src`.
### Callout
```html
<aside class="case-callout" data-callout>
<span class="case-callout__emoji case-callout__emoji--pulse" aria-hidden="true">📌</span>
<div class="case-callout__content">
<p>…</p>
</div>
</aside>
```
- **Ширина:** как у wide-медиа — блок шире колонки статьи (`min(100vw − 2×padding, content + 8rem)`), на экранах до 640px — ширина колонки.
- **Иконка:** в `.case-callout__emoji` — цветные emoji (📌 💡 ✨ ✅ ⭐), не текстовые символы (✎ ✓ ◆ →); рендер через emoji-шрифт в `main.css`.
- **Типографика:** текст в `.case-callout__content` чуть крупнее основного абзаца (lead).
- Живой пример: [`src/pages/cases/design-system.html`](src/pages/cases/design-system.html) (секция Callout).
### Видео
- Обертка: `<figure class="case-media">` (часто с inline-отступами как в template) → `<div class="case-video-shell">` → `<video controls preload="metadata" playsinline … data-case-video-player>`.
- Подпись `<figcaption>` под видео — по центру колонки (стили в `main.css`).
- Видео в карточке и в fullscreen — `object-fit: contain` (без обрезки); fullscreen через Plyr (`iosNative: false`), не нативные `controls` на `<video>`.
- Постер: WebP в `poster`.
- Источники: `<source type="video/webm">`, затем `video/mp4`.
Про конвертацию и постеры: `docs/video-conversion.md`.
## Оптимизация изображений
PNG/JPEG → WebP по папке: `docs/image-optimization.md`, скрипт `scripts/optimize-images.mjs`.
## Связь с JS
Подключаемые через `{{> head-links}}` сценарии уже инициализируют:
- последовательную подгрузку `data-src` внутри `[data-case-sequential-images]` (`initCaseSequentialImages`);
- просмотр картинок по клику (`data-image-viewer`, делегирование на документ);
- плеер видео кейса (`data-case-video-player`);
- натуральный размер для `.case-media--natural img`;
- сворачивание длинного скрина (`[data-case-tall-media]`, `initCaseTallMediaExpand`).
Новые интерактивные паттерны — по правилам из `docs/frontend-js-architecture.md` (модуль в `public/js/modules/cases/`, вызов из `main.js`).
## Чеклист нового кейса
1. Скопировать `template-elements-demo.html` → `src/pages/cases/<slug>.html`, заменить `meta`, `title`, контент.
2. Положить медиа в **`public/assets/<slug>/`** (см. `public/assets/README.md` и `docs/image-optimization.md`).
3. Проверить все пути `../assets/…` и `../css/…`.
4. Выполнить `npm run build:pages` и открыть `public/cases/<slug>.html` локально.
5. При необходимости завести текстовый слепок в `texts/cases/<slug>.md` и ссылку на кейс в `src/partials/projects-section.html` после сборки главной.
+41
View File
@@ -0,0 +1,41 @@
# Режим правок контента (localhost)
Инструмент для сбора **инструкций агенту** прямо на странице при локальном просмотре. HTML на диске не меняется до apply в `src`.
## Запуск
```bash
npm run serve
```
Открыть страницу на [http://localhost:4175](http://localhost:4175). Работает только на `localhost` / `127.0.0.1`.
Сервер: [`scripts/serve-dev.mjs`](../scripts/serve-dev.mjs) — BrowserSync по `public/`, API правок, **без** перезагрузки при записи в `texts/content-edits/`.
Старый однострочный BrowserSync: `npm run serve:static`.
## Как пользоваться
1. **FAB (карандаш)** — включить режим правок. Если в очереди есть правки, на кнопке виден **бейдж** с их числом (режим выключен).
2. В режиме правок слева внизу открывается **парящая панель**: слева список, справа поле «Инструкция агенту». Без выбора блока можно сразу писать **общую правку страницы**.
3. **Наведение** на объект в `main` — пунктирный контур; **клик** — правка конкретного блока. **Shift+клик** — вставить ссылку `«…»` на блок в текст (работает и для общей правки страницы).
4. Текст сохраняется **автоматически** (debounce ~400 ms) в [`texts/content-edits/pending.json`](../texts/content-edits/pending.json). Пустое поле — запись удаляется.
5. Клик по пункту в списке слева — скролл к объекту (если блок ещё на странице) и редактирование. Если блок уже удалён или селектор не находится — скролла нет, инструкция всё равно открывается. **Удалить** — убрать правку из очереди.
6. **Escape** или клик мимо блока — снять контур блока, вернуться к общей правке страницы. Повторный клик FAB — выключить режим.
7. В Cursor: **`/apply-edits`** (или **`/apply_edits`**, если остался старый алиас) или **«примени правки»** — агент читает `pending.json`, правит `src/pages/`, `humanizer-ru`, `npm run build:pages`. Очередь **не очищается** — проверяешь результат, при необходимости правишь задание и apply снова; готовые пункты удаляешь сам в dock.
Черновики (клик без текста) видны в списке по сниппету блока; в JSON попадают только пункты с непустой инструкцией.
## Формат JSON
См. [`texts/content-edits/README.md`](../texts/content-edits/README.md).
## Apply для агента
Правило: [`.cursor/rules/content-edit-apply.mdc`](../.cursor/rules/content-edit-apply.mdc).
Порядок поиска блока в `src`: `target.selector` → подстрока `target.content` / `snippet` → зона `anchorId`. Дополнительные блоки из instruction — в `item.refs[]` (метки `«…»` ↔ `refs[].label`).
## Legacy
Ручная правка собранного HTML в `public/` — только по явной просьбе, см. `.cursor/rules/case-content-edit-in-public.mdc`. Не путать с content-edits.
+13
View File
@@ -0,0 +1,13 @@
# Термины: как говорят в дизайне (RU)
Короткий словарик: как формулируют коллеги в речи и текстах. Не норма русского языка, а устоявшийся профессиональный жаргон.
## Юзерфлоу / user flow
- **Как говорят:** «юзерфлоу», иногда «user flow» в латинице.
- **По-русски формально:** пользовательский сценарий, поток пользователя, маршрут пользователя.
- **Заметка:** даже в русскоязычной среде дизайнеры часто оставляют **user flow / юзерфлоу** — так короче и однозначно отсылает к артефакту (диаграмма или описание шагов).
---
*Файл можно дополнять новыми строками по мере появления устойчивых формулировок.*
+89
View File
@@ -0,0 +1,89 @@
# Frontend JS Architecture
## Цель
- Держать `public/js/main.js` минимальным entrypoint.
- Делить код по фичам, а не по страницам.
- Снижать связность и дублирование.
## Структура
- `public/js/main.js` — только импорты и вызовы `init*`.
- `public/js/modules/theme/*` — тема и панель стилей.
- `public/js/modules/header/*` — шапка, скролл, overflow-навигация.
- `public/js/modules/experience/*` — masonry, expand, hover-эффекты.
- `public/js/modules/mini-projects/*` — masonry и раскрытие секции мини-проектов.
- `public/js/modules/cases/*` — медиалогика кейсов.
- `public/js/modules/ui/*` — локальные UI-фичи (копирование, cookie, hero и т.д.).
- `public/js/modules/shared/*` — переиспользуемые утилиты.
## Правила зависимостей
- Разрешено: `feature -> shared`.
- Нежелательно: `feature -> feature`.
- Запрещено: циклические зависимости.
- Если логика нужна в 2+ фичах, переносить ее в `shared`.
## Соглашения по именованию
- Инициализаторы: `initFeatureName`.
- Файлы с разделением ответственности:
- `*-core.js` — состояние, вычисления, чистая логика.
- `*-controls.js` — DOM и события.
- Утилиты: короткие имена по области (`math.js`, `media.js`, `dom.js`).
## Контракт для init-функций
- Функция должна быть безопасной на любой странице.
- Если нужного DOM нет, делать ранний `return`.
- Не бросать исключения при отсутствии опциональных API браузера.
- По возможности избегать глобального состояния.
## Как добавить новую фичу
1. Создать модуль в нужной feature-папке, например `public/js/modules/ui/new-feature.js`.
2. Экспортировать `initNewFeature`.
3. В `public/js/main.js`:
- добавить `import { initNewFeature } ...`;
- вызвать `initNewFeature()` в блоке инициализации.
4. Если часть кода переиспользуется, вынести в `public/js/modules/shared/*`.
## Минимальный шаблон фичи
```js
export function initNewFeature() {
const root = document.querySelector("[data-new-feature]");
if (!root) return;
// logic
}
```
## Чеклист перед PR
- Проверить базовый smoke на страницах с этой фичей.
- Проверить `prefers-reduced-motion`, если есть анимации.
- Проверить resize/scroll сценарии, если есть геометрические вычисления.
- Проверить доступность: `aria-*`, keyboard-only навигация, Escape/Focus.
## Source/Build/Public для HTML
- `src/partials/*.html` — source of truth для общих блоков (header, fab, projects, contact, footer, cookie и т.д.).
- `src/pages/**/*.html` — source of truth для страниц с include-маркерами `{{> partial-name }}`.
- `public/assets/**` — картинки, видео, иконки сайта (редактируются вручную; сборка страниц их не копирует).
- `public/**/*.html` — только build-артефакт; руками не редактируем.
Эталон HTML-блоков для лонгрида кейса и порядок partials: **`docs/case-page-blocks.md`** (исходник-пример: `src/pages/cases/template-elements-demo.html`).
### Сборка страниц
- Команда: `npm run build:pages`.
- Скрипт: `scripts/build-pages.mjs`.
- Скрипт подставляет partials и переменные пути:
- `{{assetPrefix}}` (`""` для корня, `../` для `cases/*`),
- `{{casesPrefix}}` (`cases/` для корня, `""` для `cases/*`).
### Правило перед выкладкой
- Перед публикацией на хостинг всегда запускать `npm run build:pages` (или `npm run build`).
+57
View File
@@ -0,0 +1,57 @@
# Синхронизация `public` -> `gitverse`
## Назначение
Скрипт `sync-gitverse` копирует содержимое `public` в папку `gitverse`, затем выполняет коммит и пуш в репозиторий внутри `gitverse`.
## Запуск
- Через npm:
- `npm run sync-gitverse`
- Напрямую:
- `node scripts/sync-gitverse.mjs`
- С явными путями:
- `node scripts/sync-gitverse.mjs <sourceDir> <targetDir>`
По умолчанию:
- `sourceDir = public`
- `targetDir = gitverse`
## Что делает скрипт
1. Проверяет, что исходная папка существует.
2. В целевой папке удаляет всё, кроме:
- `.git`
- `.gitverse`
3. Копирует содержимое `public` в `gitverse` (файлы `.png` пропускаются — в деплое используются WebP и остальные форматы).
4. В репозитории `gitverse` выполняет:
- `git add .`
- `git commit -m "sync public YYYY-MM-DD HH:mm"` (только если есть изменения)
- `git push`
5. После успешного пуша открывает в системном браузере по умолчанию: `https://mamadaevv.gitverse.site/me/`
Если изменений после копирования нет, коммит и пуш не делаются.
## Инструкция для агента (Cursor)
Когда пользователь просит **выложить на GitVerse**, **собрать/задеплоить gitverse**, **синхронизировать с gitverse** или аналогично:
1. **Сборка `public/`:** если менялись `src/pages/**` или `src/partials/**` — `npm run build:pages`; если шрифты/Plyr/`package.json` или `scripts/copy-vendor.mjs` — `npm run build` (сначала `vendor:copy`, затем страницы). См. [`docs/vendor-assets.md`](vendor-assets.md).
2. **Синк и пуш:** выполни `npm run sync-gitverse` из корня. Нужны разрешения **network** и **git_write** (SSH на GitVerse, пуш из папки `gitverse`).
3. **Ожидания:** скрипт не копирует файлы `.png` в `gitverse`. После **успешного** `git push` открывается системный браузер с `https://mamadaevv.gitverse.site/me/`. Если изменений в `gitverse` после копирования нет — коммит/пуш не выполняются, браузер не открывается.
4. **Не вызывай** `sync-gitverse` без запроса пользователя: скрипт очищает целевую папку (кроме `.git` и `.gitverse`) и пушит в remote.
5. **Корневой git репозиторий** этого проекта считается **только локальным**: не пушь из корня в `origin` и не настраивай remote без явной просьбы пользователя с URL. Внешняя выкладка — через `gitverse`, не через `git push` в корне.
6. Деплой на VPS — отдельный сценарий, см. **`docs/vps-deploy.md`**.
Подробности и ограничения — в разделах выше.
## Требования
- В `gitverse` уже должен быть инициализирован git-репозиторий (папка `.git`).
- Должен быть настроен доступ на push (SSH-ключ/права).
## Важно
- Скрипт удаляет в `gitverse` все файлы и папки, кроме `.git` и `.gitverse`.
- Не запускай скрипт, если в `gitverse` есть локальные несохраненные изменения, которые нужно оставить.
+42
View File
@@ -0,0 +1,42 @@
# Оптимизация картинок: PNG / JPEG → WebP рядом
Скрипт `scripts/optimize-images.mjs` обходит **вложенные** папки и для каждого `.png`, `.jpg`, `.jpeg` создаёт рядом `то же имя.webp`.
- Размер в пикселях **не меняется**.
- WebP с **качеством 90** (и `alphaQuality: 100` для прозрачного PNG) — сильно не жмёт, ориентир на визуально близкий результат к исходнику при меньшем весе файла.
- **Повторный запуск:** если `имя.webp` уже есть и **новее или того же возраста**, что и исходник, файл **пропускается** (чтобы не гонять лишний раз). После правки картинки снова запусти скрипт, либо удали `webp`, либо используй `--force`.
## Требования
- **Node.js**;
- пакет `sharp` (ставится вместе с `npm install` в корне репозитория).
## Запуск
Из корня репозитория (где `package.json`), укажи **папку с картинками** (относительно корня или абсолютный путь):
```bash
npm run optimize-images -- public/assets/cases/мой-кейс/img
```
либо:
```bash
node scripts/optimize-images.mjs public/assets/cases/мой-кейс/img
```
Пересоздать все WebP, даже если по дате «свежее»:
```bash
npm run optimize-images -- public/assets/cases/мой-кейс/img --force
```
В PowerShell, если в пути есть пробелы:
```powershell
npm run optimize-images -- "public\assets\cases\мой-кейс\img"
```
## Когда картинка не трогается
См. выше: если `foo.webp` есть и **не старше** `foo.png` / `foo.jpg`, `foo` пропускается, кроме режима `--force`.
+51
View File
@@ -0,0 +1,51 @@
# Локальный просмотр сайта
Сайт в режиме разработки обслуживается из папки `**public/**` через [BrowserSync](https://browsersync.io/): при изменении файлов страница в браузере обновляется сама.
## Требования
- Установлены **Node.js** и **npm** (идут вместе с Node).
## Первый раз в проекте
В корне репозитория (рядом с `package.json`):
```bash
npm install
npm run vendor:copy
```
(или `npm run build` — vendor + сборка HTML из `src/`.)
## Запуск
```bash
npm run serve
```
Скрипт [`scripts/serve-dev.mjs`](../scripts/serve-dev.mjs): статика из `public/`, API правок контента, перезагрузка **только** при изменениях в `public/**` (запись в `texts/content-edits/` вкладку не перезагружает).
Без API: `npm run serve:static`.
## Куда смотреть
- **Сайт:** [http://localhost:4175](http://localhost:4175) — откроется `public/index.html` и остальные пути относительно `public/`.
- **Панель BrowserSync UI:** [http://localhost:4176](http://localhost:4176) (опционально, для синхронизации и настроек).
## Что отслеживается
Любые изменения в `**public/**`**: HTML, CSS, JS, картинки. Папка `texts/content-edits/` **не** в watch — autosave правок не сбрасывает режим редактирования.
## API правок контента
Только с localhost:
- `GET /api/content-edits?page=cases/foo.html` — правки для страницы;
- `GET /api/content-edits` — весь `pending.json`;
- `PUT /api/content-edits?page=…` — тело: `{ page, items[] }` (мерж в общий файл).
Файл: **`texts/content-edits/pending.json`**. См. [`docs/content-edit-mode.md`](content-edit-mode.md).
## Остановка
В терминале: **Ctrl+C**.
+78
View File
@@ -0,0 +1,78 @@
# Карточки секции «Активность»: как подобрать настройки
Кратко о том, какие атрибуты и классы влияют на вид карточки и как не ломать сетку. Живой каталог всех вариантов — **[`public/cases/mini-projects-showcase.html`](../public/cases/mini-projects-showcase.html)** (исходник `src/pages/cases/mini-projects-showcase.html`). Разметка главной — `src/partials/mini-projects-section.html`, раскладка — `[public/js/modules/mini-projects/masonry.js](../public/js/modules/mini-projects/masonry.js)`, стили — `[public/css/main.css](../public/css/main.css)` (классы `mini-projects-*`, `mp-card*`).
## Как устроена сетка
- Каждая карточка — элемент `**li.mini-projects-grid__item**` внутри `**ol#mini-projects-list.mini-projects-grid**`.
- **Ширина в «клетках»** задаётся атрибутом `**data-mp-cells`** со значением от **1** до **3** — это максимум на карточку; в JS значение дополнительно ограничивается (`readDeclaredCells` в `masonry.js`).
- Фактическое число клеток в ряду (`rowCells`) зависит от ширины списка (контейнера сетки), не от всего экрана целиком:
- **≥ 1180px** — 6 клеток в ряду
- **≥ 880px** — 4
- **≥ 520px** — 2
- иначе — **1**
Пороги заданы в `rowCellsForWidth()` в `masonry.js`; при необходимости их можно сдвинуть под макет.
- Если `**data-mp-cells`** больше, чем `rowCells` (например, на телефоне ряд из одной клетки), скрипт подставит эффективную ширину `min(объявленное, rowCells)`, карточка займёт всю строку.
Итог: `**data-mp-cells`** выбирайте в диапазоне **1–3** относительно широкого ряда; самая широкая карточка занимает половину шестиклеточной строки или меньше.
## Когда какой модификатор карточки
Базовый блок — `**mp-card`**. Дальше — комбинации модификаторов.
### `mp-card--stack` (+ опционально `mp-card--link`)
- Превью **на всю ширину карточки**, **вплотную** к верхнему и боковым краям; под ним — текст с внутренними отступами.
- Удобно для **сильного визуала** (скрин, обложка) и **2–4 строк описания**.
- Ссылка наружу: обёртка `**<a class="mp-card mp-card--link mp-card--stack">`**. В `**h3.mp-card__title`** добавьте `**<span class="mp-card__ext">**` (иконка внешней ссылки появляется при ховере, как в блоке «Опыт работы»).
### `mp-card--horizontal` (+ опционально `mp-card--link`)
- Превью **слева**, текст **справа** (колонка превью ~22% ширины карточки, не шире **6rem**); на ширине **до ~519px** колонки складываются в столбик (превью сверху), см. медиазапрос в CSS.
- Подходит, когда картинка **не главный герой**, но нужна миниатюра; текст может быть длиннее, чем в узкой колонке `stack`.
- У горизонтальной карточки без ссылки превью и текст те же классы `**mp-card__media`** / `**mp-card__body`**, но без `**mp-card--link`** и `**mp-card__ext**`.
### `mp-card--horizontal` + `mp-card--hor-image` (+ `mp-card--link`)
- Как horizontal, но логотип/иконка: `**object-fit: contain**` в превью, без обрезки по краям. На узком экране превью не растягивается на всю ширину — до **6rem**.
- Превью с обводкой и скруглением как у `portfolio-card__image` (см. `main.css`, `.mp-card__image`).
- Пример на главной: Figma Community, `data-mp-cells="2"`, превью `assets/misc/figma_logo.webp`.
### `mp-card--compact`
- **Только текстовый блок** (`mp-card__body`), без `**mp-card__media`**.
- Имеет смысл для заметок, чеклистов, коротких описаний без скрина.
- Ссылка: `**mp-card mp-card--link mp-card--compact`**; без ссылки — обёртка `**article`** или `**div**` с теми же классами без `**mp-card--link**`.
### `mp-card--decor`
- **Не проект**, а визуальная заглушка в сетке (ровная подложка, паттерн).
- Внутри: `**span.mp-card__decor-pattern`**; без тела проекта и без ссылки.
- Задаётся тем же `**data-mp-cells`**, что и у обычных карточек, чтобы заполнить «дыру» в masonry.
## Теги типа проекта
- Список `**ul.mp-card__tags`** (можно `**aria-label="Тип проекта"`**).
- Элементы — `**li.mp-card__tag**` с коротким словом (плагин, скрипт, веб-сайт и т.д.).
- Обычно **один** тег; несколько допустимы, строка тегов переносится.
## Свёрнутый блок «Показать все»
- Карточки, которые не должны учитываться в **ограниченной высоте** до раскрытия: на `**li`** добавьте `**data-mp-extra`** и атрибут `**hidden`** (скрипт снимает `hidden` для измерения, высота списка в свёрнутом режиме считается только по карточкам **без** `data-mp-extra`).
- Кнопка с `**data-mp-toggle`** уже есть в разметке; логика — `[expand.js](../public/js/modules/mini-projects/expand.js)`.
## Практические подсказки по красоте раскладки
1. `**data-mp-cells="3"`** — самая широкая карточка в сетке (на десктопе помещается до двух таких в один ряд при `rowCells === 6`). Для акцента и крупного превью в `**stack`**.
2. `**2 + 2 + 2`** или `**3 + 3**` на шести клетках часто выглядят ровнее случайного набора — masonry всё равно упакует по высоте; меняйте порядок `**li**` в HTML.
3. **Горизонтальный шаблон** на `**data-mp-cells="2"`** даёт плотный блок «иконка + текст»; длинный текст чаще уместнее в `**stack`** с `**data-mp-cells="3"`**.
4. `**mp-card--compact`** и `**data-mp-cells="1"**` — узкая текстовая колонка; хорошо рядом с `**stack**` на **2–3** клетки.
5. После смены картинок или текста высота пересчитывается на `**resize`** и при `**load`** изображений; если верстка «прыгнула», проверьте размеры превью и порядок элементов в DOM.
## Мини-чеклист перед коммитом
- У каждой `**li`** задан осмысленный `**data-mp-cells`** (**1–3**).
- Для ссылки есть `**mp-card--link`** и `**mp-card__ext`** в заголовке.
- У `**stack`/`horizontal`** с картинкой есть `**mp-card__media`** + `**mp-card__image**` (например, `placeholder.webp` или свой файл).
- `**data-mp-extra**` только у карточек, которые должны скрываться до «Показать все».
- Декоративная ячейка — `**mp-card--decor**`, без лишнего интерактива.
+55
View File
@@ -0,0 +1,55 @@
# Локальные vendor-ассеты (шрифты и Plyr)
Сайт не использует Google Fonts и jsDelivr в рантайме. Файлы лежат в `public/` и попадают на хостинг через `sync-gitverse`.
## Что где
| Путь | Содержимое |
|------|------------|
| `public/assets/fonts/*.css` | Три пресета темы: `merriweather`, `geologica`, `mono` |
| `public/assets/fonts/files/*.woff2` | Файлы шрифтов (из `@fontsource/*`) |
| `public/vendor/plyr/` | `plyr.css`, `plyr.min.mjs` (из `plyr@3.8.4`) |
Подключение:
- Шрифты — inline `theme-init-script` и [`theme-fonts.js`](../public/js/modules/theme/theme-fonts.js).
- CSS Plyr — partial [`src/partials/head-plyr.html`](../src/partials/head-plyr.html) только на страницах с `video[data-case-video-player]`.
- JS Plyr — [`video-player.js`](../public/js/modules/cases/video-player.js).
## Обновление
```bash
npm install
npm run vendor:copy
```
Скрипт: [`scripts/copy-vendor.mjs`](../scripts/copy-vendor.mjs). Пересобирает `public/assets/fonts/` и `public/vendor/plyr/` из `node_modules`.
Полная сборка (vendor + HTML):
```bash
npm run build
```
Только HTML из `src/`:
```bash
npm run build:pages
```
## Добавить или изменить шрифт
1. Установить пакет `@fontsource/...` или `@fontsource-variable/...`.
2. Добавить источник в `FONT_BUNDLES` в `copy-vendor.mjs`.
3. `npm run vendor:copy`.
4. Проверить имена `font-family` в [`public/css/main.css`](../public/css/main.css) (`html[data-font=...]`).
## Обновить Plyr
1. Обновить версию в `package.json` (`dependencies.plyr`).
2. `npm install` → `npm run vendor:copy`.
3. При смене API — правки в `video-player.js`.
## Деплой
Перед `npm run sync-gitverse` выполни `npm run build`, чтобы в `public/` были актуальные шрифты, Plyr и HTML.
+35
View File
@@ -0,0 +1,35 @@
# Конвертация роликов: MP4 → WebM и превью WebP
Скрипт `scripts/convert-videos.mjs` для каждого `.mp4` в указанной папке создаёт рядом:
- `имя.webm` — видео VP9/Opus (без отдельной аудиодорожки в исходнике — видео без звука);
- `имя.webp` — превью, один кадр (таймкод: не дальше 1 с от начала и не дальше 10% длительности, масштаб по ширине до 1280 px).
## Требования
- **Node.js** (как и для остального проекта);
- **ffmpeg** и **ffprobe** в `PATH` (полная сборка с `libvpx`/`libvpx-vp9`, `libopus`, `libwebp`).
## Запуск
Из корня репозитория (где `package.json`), укажи **папку с роликами** (относительно корня или абсолютный путь):
```bash
npm run convert-videos -- public/assets/cases/мой-кейс/видео
```
либо:
```bash
node scripts/convert-videos.mjs public/assets/cases/мой-кейс/видео
```
В PowerShell путь в кавычках, если в нём пробелы:
```powershell
npm run convert-videos -- "public\assets\cases\мой-кейс\видео"
```
## Когда ролик не трогается
Если для `foo.mp4` уже существуют **оба** файла `foo.webm` и `foo.webp` в той же папке, этот ролик **пропускается**. Если не хватает только webm или только webp — догенерируется недостающий файл.
+59
View File
@@ -0,0 +1,59 @@
# Деплой на VPS (инкрементальный)
Скрипт `scripts/deploy-vps.mjs` загружает содержимое `public/` на VPS, передавая только изменённые файлы.
## Принцип работы
1. **Манифест** (`.deploy-manifest.json`) хранит `mtime` и `size` каждого файла с прошлого деплоя.
2. При запуске скрипт сканирует `public/`, сравнивает с манифестом:
- Файлы с изменившимся `mtime` или `size` — загружаются.
- Файлы, которых нет локально, но есть в манифесте — удаляются с сервера.
3. Изменённые файлы пакуются в `tar` и передаются **одним SSH-соединением** (без сжатия, чтобы не тратить CPU на уже сжатые медиа).
4. Манифест обновляется после успешного деплоя.
При первом запуске (манифест пуст) заливаются все файлы — ~3 минуты на ~400 файлов. Повторный деплой (1–10 изменившихся файлов) — 2–5 секунд.
## Запуск
```bash
npm run deploy:vps
# или напрямую
node scripts/deploy-vps.mjs [sourceDir]
```
По умолчанию `sourceDir = public`.
## Рецепты
### Только в Coolify
Запрос пользователя: «задеплой в Coolify», «задеплой на VPS».
```bash
npm run deploy:vps
```
Нужны разрешения: **all** (SSH-ключ `~/.ssh/beget`, доступ к файлам `public/`).
### Везде (VPS + GitVerse)
```bash
npm run deploy
```
Выполняет `deploy:vps`, затем `sync-gitverse`.
## Конфигурация
- **SSH-ключ:** `~/.ssh/beget`
- **Хост:** `root@155.212.186.39`
- **Целевая папка:** `/data/static-site`
- **Манифест:** `.deploy-manifest.json` в корне проекта (не коммитится)
## Сброс манифеста
Если нужно залить всё с нуля — удалить `.deploy-manifest.json`:
```bash
rm .deploy-manifest.json
```
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2026 Adam Argyle (https://codepen.io/argyleink/pen/wBzPvaE)
Fork of an original work CSS <color-input> Color Picker (https://codepen.io/argyleink/pen/dPGBYZg)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,6 @@
# CSS <color-input> Inline Color Picker
A Pen created on CodePen.
Original URL: [https://codepen.io/argyleink/pen/wBzPvaE](https://codepen.io/argyleink/pen/wBzPvaE).
@@ -0,0 +1,3 @@
<color-input></color-input>
<!-- https://github.com/argyleink/css-color-component -->
@@ -0,0 +1 @@
import "https://unpkg.com/hdr-color-input"
@@ -0,0 +1,63 @@
/* comment out for the default anchor variant */
color-input {
display:grid;
place-items: start;
&::part(panel) {
display: block;
position: static;
opacity: 1;
}
}
@layer demo.support {
* {
box-sizing: border-box;
margin: 0;
}
html {
block-size: 100%;
color-scheme: dark light;
scrollbar-color: color-mix(in srgb, CanvasText, #0000 50%) #0000;
}
body {
min-block-size: 100%;
font-family: system-ui, sans-serif;
display: grid;
place-content: center;
}
}
@layer demo.support {
* {
box-sizing: border-box;
margin: 0;
}
html {
block-size: 100%;
color-scheme: dark light;
scrollbar-color: color-mix(in srgb, CanvasText, #0000 50%) #0000;
}
body {
min-block-size: 100%;
font-family: system-ui, sans-serif;
display: grid;
place-content: center;
}
}
Binary file not shown.
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2026 Hyperplexed (https://codepen.io/Hyperplexed/pen/MWQeYLW)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,7 @@
# Magical Hover Effect (w/ Tutorial)
A Pen created on CodePen.
Original URL: [https://codepen.io/Hyperplexed/pen/MWQeYLW](https://codepen.io/Hyperplexed/pen/MWQeYLW).
A recreation of the amazing hover effect found here: https://linear.app/features
@@ -0,0 +1,108 @@
<div id="cards">
<div class="card">
<div class="card-content">
<div class="card-image">
<i class="fa-duotone fa-apartment"></i>
</div>
<div class="card-info-wrapper">
<div class="card-info">
<i class="fa-duotone fa-apartment"></i>
<div class="card-info-title">
<h3>Apartments</h3>
<h4>Places to be apart. Wait, what?</h4>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-content">
<div class="card-image">
<i class="fa-duotone fa-unicorn"></i>
</div>
<div class="card-info-wrapper">
<div class="card-info">
<i class="fa-duotone fa-unicorn"></i>
<div class="card-info-title">
<h3>Unicorns</h3>
<h4>A single corn. Er, I mean horn.</h4>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-content">
<div class="card-image">
<i class="fa-duotone fa-blender-phone"></i>
</div>
<div class="card-info-wrapper">
<div class="card-info">
<i class="fa-duotone fa-blender-phone"></i>
<div class="card-info-title">
<h3>Blender Phones</h3>
<h4>These absolutely deserve to exist.</h4>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-content">
<div class="card-image">
<i class="fa-duotone fa-person-to-portal"></i>
</div>
<div class="card-info-wrapper">
<div class="card-info">
<i class="fa-duotone fa-person-to-portal"></i>
<div class="card-info-title">
<h3>Adios</h3>
<h4>See you...</h4>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-content">
<div class="card-image">
<i class="fa-duotone fa-person-from-portal"></i>
</div>
<div class="card-info-wrapper">
<div class="card-info">
<i class="fa-duotone fa-person-from-portal"></i>
<div class="card-info-title">
<h3>I mean hello</h3>
<h4>...over here.</h4>
</div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-content">
<div class="card-image">
<i class="fa-duotone fa-otter"></i>
</div>
<div class="card-info-wrapper">
<div class="card-info">
<i class="fa-duotone fa-otter"></i>
<div class="card-info-title">
<h3>Otters</h3>
<h4>Look at me, imma cute lil fella.</h4>
</div>
</div>
</div>
</div>
</div>
</div>
<a id="source-link" class="link" href="https://linear.app/features" target="_blank">
<i class="fa-solid fa-link"></i>
<span class="roboto-mono">Source</span>
</a>
<a id="youtube-link" class="link" href="https://youtu.be/htGfnF1zN4g" target="_blank">
<i class="fa-brands fa-youtube"></i>
<span>5 min Tutorial</span>
</a>
@@ -0,0 +1,10 @@
document.getElementById("cards").onmousemove = e => {
for(const card of document.getElementsByClassName("card")) {
const rect = card.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
card.style.setProperty("--mouse-x", `${x}px`);
card.style.setProperty("--mouse-y", `${y}px`);
};
}
@@ -0,0 +1,247 @@
:root {
--bg-color: rgb(20, 20, 20);
--card-color: rgb(23, 23, 23);
}
body {
align-items: center;
background-color: var(--bg-color);
display: flex;
height: 100vh;
justify-content: center;
margin: 0px;
overflow: hidden;
padding: 0px;
}
#cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
max-width: 916px;
width: calc(100% - 20px);
}
#cards:hover > .card::after {
opacity: 1;
}
.card {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 10px;
cursor: pointer;
display: flex;
height: 260px;
flex-direction: column;
position: relative;
width: 300px;
}
.card:hover::before {
opacity: 1;
}
.card::before,
.card::after {
border-radius: inherit;
content: "";
height: 100%;
left: 0px;
opacity: 0;
position: absolute;
top: 0px;
transition: opacity 500ms;
width: 100%;
}
.card::before {
background: radial-gradient(
800px circle at var(--mouse-x) var(--mouse-y),
rgba(255, 255, 255, 0.06),
transparent 40%
);
z-index: 3;
}
.card::after {
background: radial-gradient(
600px circle at var(--mouse-x) var(--mouse-y),
rgba(255, 255, 255, 0.4),
transparent 40%
);
z-index: 1;
}
.card > .card-content {
background-color: var(--card-color);
border-radius: inherit;
display: flex;
flex-direction: column;
flex-grow: 1;
inset: 1px;
padding: 10px;
position: absolute;
z-index: 2;
}
/* -- ↓ ↓ ↓ extra card content styles ↓ ↓ ↓ -- */
h1, h2, h3, h4, span {
color: rgb(240, 240, 240);
font-family: "Rubik", sans-serif;
font-weight: 400;
margin: 0px;
}
i {
color: rgb(240, 240, 240);
}
.card-image {
align-items: center;
display: flex;
height: 140px;
justify-content: center;
overflow: hidden;
}
.card-image > i {
font-size: 6em;
opacity: 0.25;
}
.card-info-wrapper {
align-items: center;
display: flex;
flex-grow: 1;
justify-content: flex-start;
padding: 0px 20px;
}
.card-info {
align-items: flex-start;
display: flex;
gap: 10px;
}
.card-info > i {
font-size: 1em;
height: 20px;
line-height: 20px;
}
.card-info-title > h3 {
font-size: 1.1em;
line-height: 20px;
}
.card-info-title > h4 {
color: rgba(255, 255, 255, 0.5);
font-size: 0.85em;
margin-top: 8px;
}
/* -- ↓ ↓ ↓ some responsiveness ↓ ↓ ↓ -- */
@media(max-width: 1000px) {
body {
align-items: flex-start;
overflow: auto;
}
#cards {
max-width: 1000px;
padding: 10px 0px;
}
.card {
flex-shrink: 1;
width: calc(50% - 4px);
}
}
@media(max-width: 500px) {
.card {
height: 180px;
}
.card-image {
height: 80px;
}
.card-image > i {
font-size: 3em;
}
.card-info-wrapper {
padding: 0px 10px;
}
.card-info > i {
font-size: 0.8em;
}
.card-info-title > h3 {
font-size: 0.9em;
}
.card-info-title > h4 {
font-size: 0.8em;
margin-top: 4px;
}
}
@media(max-width: 320px) {
.card {
width: 100%;
}
}
/* -- ↓ ↓ ↓ YouTube link styles ↓ ↓ ↓ -- */
#youtube-link {
bottom: 10px;
}
#youtube-link > i {
color: rgb(239, 83, 80);
}
#source-link {
bottom: 60px;
}
#source-link > i {
color: rgb(94, 106, 210);
}
.link {
align-items: center;
backdrop-filter: blur(3px);
background-color: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.1);
cursor: pointer;
display: inline-flex;
gap: 5px;
left: 10px;
padding: 10px 20px;
position: fixed;
text-decoration: none;
z-index: 100;
}
.link:hover {
background-color: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.link > i, .link > span {
height: 20px;
line-height: 20px;
}
.link > span {
color: white;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 776 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 773 KiB

+1
View File
@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
+73
View File
@@ -0,0 +1,73 @@
**Николай Мамадаев**
**Product Designer / Senior UX/UI Designer**
Калининград, Россия · Удалённо
 
 Телефон: +7 (960) 294-24-05
 
 Telegram: @mamadaevv
 
 Email: [nik@mamadaevv.ru
 ](mailto:nik@mamadaevv.ru "mailto:nik@mamadaevv.ru")[
 Сайт-портфолио: ](https://github.com/mamadaevv "https://github.com/mamadaevv")[https://mamadaevv.site](https://mamadaevv.site "https://mamadaevv.site")
**Профиль**
Продуктовый дизайнер с 10+ годами опыта в UX/UI, проектировании сложных интерфейсов, исследованиях и развитии дизайн-систем для web, desktop и mobile. Работаю на стыке дизайна и продукта: перевожу бизнес-задачи, продуктовые гипотезы и сложный контент в понятные сценарии с логичной информационной архитектурой и визуально привлекательными интерфейсами. Сильные стороны — Figma, пользовательские исследования, прототипирование, дизайн-системы, работа с метриками и плотная связка с разработкой.
**Образование**
**СЛПТ (Сокольский лесопромышленный политехнический техникум)**
 
 2006–2009 · Эксплуатация и обслуживание электрического и электромеханического оборудования (по отраслям)
**Языки**
- Английский (B1)
**Ключевые навыки**
**Проектирование**
 
 UX/UI дизайн, product design, проектирование интерфейсов, информационная архитектура, адаптивный дизайн, работа со сложным контентом и аналитическими сценариями
**Исследования и аналитика**
 
 Интервью, опросы, usability testing, проверка гипотез, работа с продуктовыми метриками
**Инструменты**
 
 Figma (компоненты, auto-layout, прототипирование, библиотеки, подготовка к разработке, плагины), дизайн-системы и UI-kit
**Процессы и коммуникация**
 
 Авторский надзор, консистентность интерфейсов, взаимодействие со стейкхолдерами, разработчиками и продуктовыми командами
**Опыт работы**
**Primo RPA**
***Ведущий UI/UX дизайнер***
Февраль 2023 — Ноябрь 2025 · Москва / удалённо
- Отвечал за развитие UX/UI нескольких продуктов: Studio, Orchestrator, Idea Hub и AI Server.
- Проектировал и улучшал графические интерфейсы, проводил пользовательские исследования, создавал интерактивные прототипы и поддерживал UI-kit.
- Участвовал в обновлении визуального языка продуктов компании; в веб-сервисах помог перейти к единой дизайн-системе и более целостному пользовательскому опыту.
- Работал в связке с разработкой и сопровождал внедрение решений до реализации.
**Icons8**
***UI/UX дизайнер***
Июль 2022 — Январь 2023
- Проектировал интерфейсы для редактора Lunacy, оформлял user flows и передавал решения в разработку.
- Создавал сложные прототипы, дорабатывал иконки и предлагал улучшения функциональности продукта.
**MANU Video Game Maker**
***Team Lead Product Team***
Январь 2019 — Декабрь 2021 · Москва
- Выполнил масштабный редизайн продукта и заложил новую концепцию интерфейса для сложных сценариев.
- Спроектировал поведение интерфейса для более чем сотни пользовательских сценариев и разработал дизайн-систему.
- Проводил исследования пользователей, создавал персоны и карты эмпатии, работал с аналитикой и продуктовыми метриками.
- Проектировал информационную архитектуру сайта вместе с командой и курировал создание пользовательской документации.
**Aegees**
***UX/UI дизайнер***
Октябрь 2017 — Декабрь 2018 · Сочи
- Занимался разработкой дизайна кроссплатформенного мессенджера для платформ Android, iOS, MacOS, Windows и Linux.
- Контролировал процесс работы с зарубежными иллюстраторами по подготовке графических материалов.
**Orion Innovation (ex MERA)**
***UI/UX дизайнер***
Июль 2016 — Октябрь 2017
- Создавал wireframes, финальный UI, сценарии, карты экранов и дизайн-концепции в составе продуктовой команды.
- Проводил качественные usability tests, презентовал решения клиентам и сопровождал реализацию в разработке.
**SpaceshipApps**
***UX/UI дизайнер***
Июнь 2015 — Июнь 2016 · Москва
- Разрабатывал дизайн для мобильного приложения социальной игры с квестами в реальном мире, приложения для записи к специалистам, сервиса заказа еды, голосовой игры с ботом, сайта и админки для сервиса регистрации авторского права, а также мобильной «белой доски» для коллаборативной работы.
**Дополнительно**
**AI и новые инструменты**
- Активно использую AI-инструменты для ресёрча, генерации идей, ускорения рутинных задач и поиска продуктовых решений. Для визуальных экспериментов — Recraft, GPT, YandexART.
- Собрал приватный AI-плагин для дизайнерской рутины. Регулярно использую Perplexity PRO для анализа, структурирования информации и проработки идей.
**Pet-проекты и профессиональная среда**
- Делаю pet-проекты с no-code и автоматизациями, использую n8n, REST API и PostgreSQL в собственных сценариях и связках сервисов.
- Веду профессиональные активности вне основной работы: Telegram-каналы, Figma Community, дизайнерские наработки и эксперименты на стыке дизайна, автоматизации и AI.
+2481
View File
File diff suppressed because it is too large. Load diff
+29
View File
@@ -0,0 +1,29 @@
{
"name": "my-site",
"private": true,
"scripts": {
"vendor:copy": "node scripts/copy-vendor.mjs",
"build:pages": "node scripts/build-pages.mjs",
"build": "npm run vendor:copy && npm run build:pages",
"serve": "node scripts/serve-dev.mjs",
"serve:static": "browser-sync start --server \"public\" --files \"public/**/*\" --no-notify --no-open --no-ghost-mode --port 4175 --ui-port 4176",
"convert-videos": "node scripts/convert-videos.mjs",
"optimize-images": "node scripts/optimize-images.mjs",
"deploy:vps": "node scripts/deploy-vps.mjs",
"deploy": "npm run deploy:vps && npm run sync-gitverse",
"sync-gitverse": "node scripts/sync-gitverse.mjs"
},
"devDependencies": {
"@fontsource-variable/geologica": "^5.2.8",
"@fontsource/alice": "^5.2.8",
"@fontsource/merriweather": "^5.2.11",
"@fontsource/pt-mono": "^5.2.8",
"@fontsource/pt-sans": "^5.2.8",
"@fontsource/source-code-pro": "^5.2.7",
"browser-sync": "^3.0.4",
"sharp": "^0.34.0"
},
"dependencies": {
"plyr": "^3.8.4"
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"model": "black-forest-labs/flux.2-klein-4b",
"messages": [
{
"role": "user",
"content": "Минималистичный AI-промпт для портретов в стиле high-fashion. Мягкий студийный свет, чистый фон и акцент на естественной красоте. Такой визуал идеально подходит для тренировки моделей освещения и теста фотореализма. Подходит для: fashion-портретов, нейрофотосессий, коммерческих стоков. Стиль: минимализм, чистая палитра, editorial precision. 👉 Узнай больше AI-идей и вдохновляющих промптов в моём телеграм-канале «Нейропольза» — переходи по ссылке. Minimalist AI portrait prompt with diffused studio light and fashion-magazine aesthetics. Perfect for clean editorial visuals and AI-photo training."
}
],
"modalities": ["image"],
"image_config": { "aspect_ratio": "9:16" },
"_provider": "PLZ",
"_cost": 1
}
+33
View File
@@ -0,0 +1,33 @@
# Николай Мамадаев
Lead Product Designer
## Контакты
- Telegram: [t.me/mamadaevv](https://t.me/mamadaevv)
- VK: [vk.com/mamadaevv](https://vk.com/mamadaevv)
## Опыт работы
Суммарно более 10 лет
1. [Primo RPA](https://primo-rpa.ru/) (02.2023 – 11.2025)
2. [Icons8](https://icons8.com/) (07.2022 – 01.2023)
3. [MANU Video Game Maker](https://manu.co/) (01.2019 – 12.2021)
4. [Aegees](https://aegees.com/) (10.2017 – 12.2018)
5. [Orion Innovation](https://orioninc.com/) (07.2016 – 10.2017)
6. [SpaceshipApps](https://www.spaceshipapps.com/) (06.2015 – 06.2016)
## Навыки
Figma и другие редакторы, Отрисовка UI по гайдлайнам,
Дизайн-системы, Прототипирование, User Flows, UX-исследования,
Юзабилити-тестирование, JTBD, CJM, Продуктовое мышление,
Приоритизация, Аналитика продуктовых метрик, Менторство,
AI-инструменты, HTML/CSS/JS, Вайбкодинг, Автоматизации, n8n
## Активность
- Telegram: [t.me/designnotpizza](https://www.threads.com/@nik.mamadaev)
- Figma: [figma.com/@mamadaevv](https://www.figma.com/@mamadaevv)
- GitHub: [github.com/mamadaevv](https://github.com/mamadaevv)
+15
View File
@@ -0,0 +1,15 @@
# Статические ассеты (`public/assets`)
Картинки, видео и прочие файлы для сайта лежат **здесь**, в подпапках по проекту (`template-elements-demo/`, `ai-platform-primo-rpa/`, `shared/icons/` и т.д.).
Сборка страниц (`npm run build:pages`) **не копирует** ассеты из других каталогов — только HTML из `src/pages` + partials. Этот каталог поддерживается вручную (или отдельными скриптами вроде оптимизации изображений).
## WebP из PNG/JPEG
Из корня репозитория:
```bash
npm run optimize-images -- public/assets/имя-папки
```
Подробности: `docs/image-optimization.md`.
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 867 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 490 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Loaded 100 of 505 files, more files were not shown because too many files have changed in this diff. Show more