sync: full site from notebook (source of truth) + missing media
No files matched your search
@@ -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`.
|
||||
|
||||
В ответе пользователю — одна короткая строка или «готово», без списка правок, если он не просил отчёт.
|
||||
@@ -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
|
||||
@@ -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`).
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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` после сборки главной.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Термины: как говорят в дизайне (RU)
|
||||
|
||||
Короткий словарик: как формулируют коллеги в речи и текстах. Не норма русского языка, а устоявшийся профессиональный жаргон.
|
||||
|
||||
## Юзерфлоу / user flow
|
||||
|
||||
- **Как говорят:** «юзерфлоу», иногда «user flow» в латинице.
|
||||
- **По-русски формально:** пользовательский сценарий, поток пользователя, маршрут пользователя.
|
||||
- **Заметка:** даже в русскоязычной среде дизайнеры часто оставляют **user flow / юзерфлоу** — так короче и однозначно отсылает к артефакту (диаграмма или описание шагов).
|
||||
|
||||
---
|
||||
|
||||
*Файл можно дополнять новыми строками по мере появления устойчивых формулировок.*
|
||||
@@ -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`).
|
||||
|
||||
@@ -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` есть локальные несохраненные изменения, которые нужно оставить.
|
||||
@@ -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`.
|
||||
@@ -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**.
|
||||
@@ -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**`, без лишнего интерактива.
|
||||
@@ -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.
|
||||
@@ -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 — догенерируется недостающий файл.
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 254 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 776 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 760 KiB |
|
After Width: | Height: | Size: 773 KiB |
@@ -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"}
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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 @@
|
||||
|
||||
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 867 KiB |
|
After Width: | Height: | Size: 274 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 800 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 250 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 543 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 490 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 988 KiB |
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 261 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 244 KiB |
|
After Width: | Height: | Size: 83 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 107 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 421 KiB |