feat: компонент фильтр-чипов в дизайн-систему + фильтр проектов на /ai-dev/

This commit is contained in:
Nikolay Mamadaev committed 2026-10-07 13:05:10 +02:00
1 parent 52485a17c4
commit ee0dca11a2
9 files changed
+761 -527

No files matched your search

+28
View File
@@ -147,6 +147,34 @@
| `.hero__tags` | Контейнер тегов в Hero |
| `.hero__tag` | Тег (пилл с рамкой). Используйте для ключевых слов |
| `.portfolio-card__tag` | Тег внутри карточки портфолио |
| `.mp-card__tags` | Контейнер тегов в mp-card |
| `.mp-card__tag` | Тег внутри mp-card |
### Фильтр-чипы
Переключаемая группа тегов над списком карточек. Нажатый чип оставляет только элементы с
совпадающим тегом, остальные скрываются.
| Класс / атрибут | Назначение |
|---|---|
| `.filter-chips` | Контейнер чипов (`<ul>`). Атрибут `data-filter-chips` включает поведение |
| `.filter-chip` | Кнопка-чип. Атрибут `data-filter-value` = тег, который она включает |
| `.filter-chip__count` | Счётчик совпадений. Проставляется скриптом автоматически |
| `data-filter-scope` | Селектор контейнера с фильтруемыми элементами. По умолчанию `document` |
| `data-filter-item` | Помечает фильтруемый элемент |
| `data-filter-tag` | Теги элемента через пробел. Несколько тегов работают как OR |
Правила:
- Состояние держится в `aria-pressed="true|false"`, а не в классе. Оформление выбранного
чипа — `[aria-pressed="true"]`, поэтому оно не зависит от `:focus` и не залипает после клика.
- Значение `all` (или пустое) показывает все элементы. Такой чип обычно первый.
- Скрытие через атрибут `hidden` + `[data-filter-item][hidden] { display: none }` — сетка
не оставляет дыр. Плавное затухание потребовало бы анимации высоты, а для фильтра
мгновенный отклик важнее.
- JS: `initFilterChips()` в `public/js/modules/ui/filter-chips.js`, подключён в `main.js`.
Счётчики и стартовое состояние берутся из разметки, модуль сам их заполняет.
- Помечайте элементы `hidden`, а не удаляйте — иначе фильтр ломает раскладку при возврате.
### Карточки
+199 -265
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta
name="description"
content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты."
content="Николай Мамадаев, AI-разработник. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты."
/>
<title>AI-разработка · Николай Мамадаев</title>
<script>
@@ -124,7 +124,7 @@
<meta property="og:locale" content="ru_RU" />
<meta property="og:locale:alternate" content="en_US" />
<meta property="og:title" content="AI-разработка · Николай Мамадаев" />
<meta property="og:description" content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты." />
<meta property="og:description" content="Николай Мамадаев, AI-разработник. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты." />
<meta property="og:url" content="https://mamadaevv.site/ai-dev/" />
<meta property="og:image" content="https://mamadaevv.site/assets/og/og-default.png" />
<meta property="og:image:width" content="1200" />
@@ -134,7 +134,7 @@
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="AI-разработка · Николай Мамадаев" />
<meta name="twitter:description" content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты." />
<meta name="twitter:description" content="Николай Мамадаев, AI-разработник. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты." />
<meta name="twitter:image" content="https://mamadaevv.site/assets/og/og-default.png" />
<meta name="twitter:image:alt" content="AI-разработка · Николай Мамадаев" />
@@ -181,7 +181,7 @@
"@id": "https://mamadaevv.site/ai-dev/#page",
"url": "https://mamadaevv.site/ai-dev/",
"name": "AI-разработка · Николай Мамадаев",
"description": "Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты.",
"description": "Николай Мамадаев, AI-разработник. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты.",
"isPartOf": {
"@id": "https://mamadaevv.site/#website"
},
@@ -219,6 +219,10 @@
<link rel="stylesheet" href="/vendor/plyr/plyr.css" />
<style>
/* Локальные правила страницы. Всё остальное — компоненты дизайн-системы:
.contact-grid (сетка), .mp-card (карточка), .filter-chips (переключатель),
.mp-card__tags/.mp-card__tag (метаданные), .case-links-grid (ссылки),
.hero__tags/.hero__tag (список навыков). */
.ai-page {
max-width: 52rem;
margin: 0 auto;
@@ -236,24 +240,19 @@
.ai-page a:not(.btn) {
color: var(--accent);
}
/* Сетка карточек проектов: 2 колонки, схлопывается в одну на мобильном */
/* Сетка проектов: контракт .contact-grid, но 2 колонки и шире минимальная
колонка — карточкам нужен воздух. Сама сетка и её адаптив из системы. */
.ai-projects {
list-style: none;
margin: 1.25rem 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 1rem;
}
.ai-projects > li {
margin: 0;
margin-bottom: 0;
}
/* Обложка- заглушка внутри карточки. */
.ai-projects__media {
border-radius: var(--radius-md);
border-radius: var(--radius-sm);
overflow: hidden;
border: 1px solid var(--line);
margin-bottom: 0.7rem;
background: var(--surface);
background: var(--bg-elevated);
}
.ai-projects__media img {
display: block;
@@ -262,56 +261,19 @@
aspect-ratio: 16 / 10;
object-fit: cover;
}
.ai-projects__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.ai-projects__title {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
color: var(--text);
}
/* Стек проекта — моноширинный, как .filter-chip__count в системе. */
.ai-projects__stack {
margin: 0.3rem 0 0;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.78rem;
color: var(--text-muted);
}
.ai-projects__desc {
margin: 0.45rem 0 0;
font-size: 0.95rem;
color: var(--text);
}
.ai-projects__links {
list-style: none;
margin: 0.6rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.9rem;
font-size: 0.85rem;
}
/* Навыки — компактные чипы переиспользуют стили тегов карточек активности */
.ai-stack-list {
list-style: none;
margin: 1rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.ai-stack-list li {
margin: 0;
padding: 0.3rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.8rem;
font-size: 0.76rem;
line-height: 1.4;
color: var(--text-muted);
}
/* У mp-card--compact теги уходят вниз карточки (order 2) — оставляем
стек под заголовком, поэтому поднимаем его наравне с описанием. */
.mp-card--compact .ai-projects__stack {
order: 1;
}
</style>
</head>
<body>
@@ -425,228 +387,200 @@
</p>
<h2 id="roles">Что я делаю</h2>
<ul class="section__prose ai-stack-list">
<li>Продуктовая логика и API</li>
<li>Telegram-боты на aiogram 3</li>
<li>MCP-серверы</li>
<li>Плагины Figma и Obsidian</li>
<li>Автоматизация и агентные пайплайны</li>
<ul class="section__prose hero__tags" aria-label="Что я делаю">
<li class="hero__tag">Продуктовая логика и API</li>
<li class="hero__tag">Telegram-боты на aiogram 3</li>
<li class="hero__tag">MCP-серверы</li>
<li class="hero__tag">Плагины Figma и Obsidian</li>
<li class="hero__tag">Автоматизация и агентные пайплайны</li>
</ul>
<h2 id="projects">Проекты</h2>
<ol class="ai-projects">
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-01.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Personal Assistant</h3>
</div>
<p class="ai-projects__stack">Python · aiogram 3 · PostgreSQL</p>
<p class="ai-projects__desc">
Личный ассистент в Telegram: напоминания, планирование дел и работа с LLM
в одном диалоге.
</p>
<ul class="ai-projects__links">
<li><a href="../cases/ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<ul class="filter-chips" data-filter-chips data-filter-scope="#ai-projects" aria-label="Фильтр проектов по типу">
<li><button type="button" class="filter-chip" data-filter-value="all" aria-pressed="true">Все</button></li>
<li><button type="button" class="filter-chip" data-filter-value="bots" aria-pressed="false">Бots</button></li>
<li><button type="button" class="filter-chip" data-filter-value="mcp" aria-pressed="false">MCP</button></li>
<li><button type="button" class="filter-chip" data-filter-value="plugins" aria-pressed="false">Плагины</button></li>
<li><button type="button" class="filter-chip" data-filter-value="web" aria-pressed="false">Веб</button></li>
</ul>
<ol class="contact-grid ai-projects" id="ai-projects">
<li data-filter-item data-filter-tag="bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-01.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Personal Assistant</h3>
<p class="ai-projects__stack">Python · aiogram 3 · PostgreSQL</p>
<p class="mp-card__desc">Личный ассистент в Telegram: напоминания, планирование дел и работа с LLM в одном диалоге.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Боты</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-02.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Image Gen Bot</h3>
</div>
<p class="ai-projects__stack">Python · aiogram 3 · Redis · PostgreSQL</p>
<p class="ai-projects__desc">
Бот для генерации изображений: баланс, реферальная программа, админ-логи
в отдельные топики супергруппы.
</p>
<ul class="ai-projects__links">
<li><a href="../cases/ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-02.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Image Gen Bot</h3>
<p class="ai-projects__stack">Python · aiogram 3 · Redis · PostgreSQL</p>
<p class="mp-card__desc">Бот для генерации изображений: баланс, реферальная программа, админ-логи в отдельные топики супергруппы.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Боты</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-03.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">MCP Image Server</h3>
</div>
<p class="ai-projects__stack">JavaScript · MCP · OpenRouter</p>
<p class="ai-projects__desc">
Собственный MCP-сервер для генерации картинок. Подключается к агентам
как инструмент.
</p>
<ul class="ai-projects__links">
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/mcp-img-gen" target="_blank" rel="noopener noreferrer">Код</a></li>
</ul>
<li data-filter-item data-filter-tag="mcp bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-03.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">MCP Image Server</h3>
<p class="ai-projects__stack">JavaScript · MCP · OpenRouter</p>
<p class="mp-card__desc">Собственный MCP-сервер для генерации картинок. Подключается к агентам как инструмент.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">MCP</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-01.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">AI assistant</h3>
</div>
<p class="ai-projects__stack">TypeScript · Figma Plugin API</p>
<p class="ai-projects__desc">
Плагин для Figma, который исполняет скрипты прямо в редакторе.
ИИ-ассистент для любого тарифа.
</p>
<ul class="ai-projects__links">
<li><a href="../cases/ai-assistant-figma/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="plugins">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-01.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">AI assistant</h3>
<p class="ai-projects__stack">TypeScript · Figma Plugin API</p>
<p class="mp-card__desc">Плагин для Figma, который исполняет скрипты прямо в редакторе. ИИ-ассистент для любого тарифа.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагины</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-02.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Card Columns</h3>
</div>
<p class="ai-projects__stack">TypeScript · Obsidian API</p>
<p class="ai-projects__desc">
Вью для заметок в стиле канбан-доски: группировка по колонкам с фильтрами
по тегам.
</p>
<ul class="ai-projects__links">
<li><a href="../cases/card-columns-obsidian/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="plugins">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-02.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Card Columns</h3>
<p class="ai-projects__stack">TypeScript · Obsidian API</p>
<p class="mp-card__desc">Вью для заметок в стиле канбан-доски: группировка по колонкам с фильтрами по тегам.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагины</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-03.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Hermes Desktop Plugins</h3>
</div>
<p class="ai-projects__stack">JavaScript · TypeScript · Electron</p>
<p class="ai-projects__desc">
Плагины для рабочего стола Hermes: чип баланса API, звук завершения задачи
и контроль трат.
</p>
<ul class="ai-projects__links">
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/unorouter-balance" target="_blank" rel="noopener noreferrer">Код</a></li>
</ul>
<li data-filter-item data-filter-tag="plugins">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-03.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Hermes Desktop Plugins</h3>
<p class="ai-projects__stack">JavaScript · TypeScript · Electron</p>
<p class="mp-card__desc">Плагины для рабочего стола Hermes: чип баланса API, звук завершения задачи и контроль трат.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагины</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-01.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Social Post Writer</h3>
</div>
<p class="ai-projects__stack">Python · aiogram 3 · SQLite</p>
<p class="ai-projects__desc">
Бот, который превращает тезисы в готовый пост под нужную площадку и тон,
учитывая голос автора.
</p>
<ul class="ai-projects__links">
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/social-post-writer-bot" rel="noopener noreferrer">Код</a></li>
</ul>
<li data-filter-item data-filter-tag="bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-01.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Social Post Writer</h3>
<p class="ai-projects__stack">Python · aiogram 3 · SQLite</p>
<p class="mp-card__desc">Бот, который превращает тезисы в готовый пост под нужную площадку и тон, учитывая голос автора.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Боты</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="../assets/ai-dev/placeholder-02.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">AI Design Assistant</h3>
</div>
<p class="ai-projects__stack">HTML · CSS · GLSL · WebGL</p>
<p class="ai-projects__desc">
Этот сайт целиком: структура, дизайн-система, шейдеры, CI/CD.
Весь цикл создания вместе с ИИ-агентом.
</p>
<ul class="ai-projects__links">
<li><a href="../cases/site-portfolio/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="web">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="../assets/ai-dev/placeholder-02.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">AI Design Assistant</h3>
<p class="ai-projects__stack">HTML · CSS · GLSL · WebGL</p>
<p class="mp-card__desc">Этот сайт целиком: структура, дизайн-система, шейдеры, CI/CD. Весь цикл создания вместе с ИИ-агентом.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Веб</li>
</ul>
</div>
</article>
</li>
</ol>
<h2 id="stack">Стек</h2>
<ul class="section__prose ai-stack-list">
<li>Python</li>
<li>TypeScript</li>
<li>JavaScript</li>
<li>Go</li>
<li>HTML/CSS</li>
<li>GLSL</li>
<li>aiogram 3</li>
<li>FastAPI</li>
<li>PostgreSQL</li>
<li>Redis</li>
<li>SQLite</li>
<li>Docker</li>
<li>Coolify</li>
<li>Gitea</li>
<li>MCP</li>
<ul class="section__prose hero__tags" aria-label="Стек">
<li class="hero__tag">Python</li>
<li class="hero__tag">TypeScript</li>
<li class="hero__tag">JavaScript</li>
<li class="hero__tag">Go</li>
<li class="hero__tag">HTML/CSS</li>
<li class="hero__tag">GLSL</li>
<li class="hero__tag">aiogram 3</li>
<li class="hero__tag">FastAPI</li>
<li class="hero__tag">PostgreSQL</li>
<li class="hero__tag">Redis</li>
<li class="hero__tag">SQLite</li>
<li class="hero__tag">Docker</li>
<li class="hero__tag">Coolify</li>
<li class="hero__tag">Gitea</li>
<li class="hero__tag">MCP</li>
</ul>
<h2 id="links">Где я это выкладываю</h2>
<p class="section__prose">
Код открыт: <a href="https://github.com/mamadaevv" target="_blank" rel="noopener noreferrer">GitHub</a>
и <a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin" target="_blank" rel="noopener noreferrer">Gitea</a>.
Сервисы, которые работают прямо сейчас, живут на
<a href="https://secrets-4-me.dynv6.net" target="_blank" rel="noopener noreferrer">secrets-4-me</a>.
</p>
<ul class="case-links-grid">
<li>
<a class="case-link-card" href="https://github.com/mamadaevv" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">GitHub</span>
<span class="case-link-card__meta">Проекты и эксперименты с кодом</span>
</span>
</a>
</li>
<li>
<a class="case-link-card" href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">Gitea</span>
<span class="case-link-card__meta">Внутренние проекты и сервисы</span>
</span>
</a>
</li>
<li>
<a class="case-link-card" href="../cases/site-portfolio/" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">Кейс этого сайта</span>
<span class="case-link-card__meta">Как я делал сайт вместе с ИИ-агентом</span>
</span>
</a>
</li>
<li>
<a class="case-link-card" href="https://secrets-4-me.dynv6.net" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">secrets-4-me</span>
<span class="case-link-card__meta">Сервис, который работает сейчас</span>
</span>
</a>
</li>
</ul>
</div>
</main>
</body>
+20
View File
@@ -1032,6 +1032,26 @@
</ul>
<p class="sg-label">.mp-card__tags / .mp-card__tag</p>
<h3>Фильтр-чипы</h3>
<p class="section__prose">
Переключаемая группа тегов над списком карточек. Нажатый чип показывает только
те элементы, у которых этот тег есть в <code>data-filter-tag</code>. Остальные
скрываются атрибутом <code>hidden</code>, поэтому сетка не оставляет дыр.
</p>
<ul class="filter-chips" data-filter-chips data-filter-scope="#filter-demo" aria-label="Фильтр демо-карточек">
<li><button type="button" class="filter-chip" data-filter-value="all" aria-pressed="true">Все</button></li>
<li><button type="button" class="filter-chip" data-filter-value="bots" aria-pressed="false">Боты</button></li>
<li><button type="button" class="filter-chip" data-filter-value="plugins" aria-pressed="false">Плагины</button></li>
<li><button type="button" class="filter-chip" data-filter-value="web" aria-pressed="false">Веб</button></li>
</ul>
<ul class="mp-card__tags" id="filter-demo" style="display: grid; gap: 0.4rem; list-style: none; padding: 0; margin: 0.5rem 0">
<li class="filter-demo-item" data-filter-item data-filter-tag="bots web" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Бот на aiogram + веб-панель</li>
<li class="filter-demo-item" data-filter-item data-filter-tag="bots" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Telegram-бот генерации картинок</li>
<li class="filter-demo-item" data-filter-item data-filter-tag="plugins" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Плагин для Figma</li>
<li class="filter-demo-item" data-filter-item data-filter-tag="plugins web" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Плагин для Obsidian</li>
</ul>
<p class="sg-label">.filter-chips / .filter-chip[aria-pressed] + [data-filter-item][data-filter-tag]</p>
<h3>Карточка портфолио</h3>
<div class="sg-portfolio-wrap">
<a class="mp-card mp-card--link mp-card--stack-bottom" href="#components" tabindex="-1" aria-hidden="true">
+76
View File
@@ -3570,6 +3570,82 @@ body.home .section__title {
white-space: nowrap;
}
/* ==========================================================================
Фильтр-чипы — переключаемая группа тегов для фильтрации карточек.
Разметка: .filter-chips > button.filter-chip[aria-pressed] (или .filter-chip--all).
Партнёр: элементы с [data-filter-item] и [data-filter-tag~="<значение>"].
========================================================================== */
.filter-chips {
list-style: none;
margin: 0 0 1.25rem;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.45rem;
}
.filter-chip {
display: inline-flex;
align-items: center;
min-height: 2rem;
padding: 0.35rem 0.75rem;
border: 1px solid var(--line);
border-radius: 999px;
background: color-mix(in srgb, var(--surface) 78%, transparent);
color: var(--text-muted);
font-family: inherit;
font-size: 0.82rem;
font-weight: 600;
line-height: 1.2;
letter-spacing: 0.01em;
text-align: left;
cursor: pointer;
transition:
color 0.18s ease,
border-color 0.18s ease,
background-color 0.18s ease;
}
.filter-chip:hover {
color: var(--text);
border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
}
/* Выбранный чип — акцентная заливка. aria-pressed держит состояние явным,
поэтому оформление не зависит от :focus и переживает клик без залипания. */
.filter-chip[aria-pressed="true"] {
color: var(--accent-text);
border-color: var(--accent);
background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.filter-chip:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.filter-chip__count {
margin-left: 0.45rem;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.72rem;
font-weight: 500;
opacity: 0.7;
}
@media (prefers-reduced-motion: reduce) {
.filter-chip {
transition: none;
}
}
/* Скрытие несовпавших карточек: display:none, чтобы grid не оставлял дыр.
Плавное затухание вместо этого потребовало бы анимации высоты —
для фильтра мгновенный отклик важнее. */
[data-filter-item][hidden] {
display: none;
}
.mp-card__media {
position: relative;
z-index: 1;
+2
View File
@@ -25,6 +25,7 @@ import { initProjectsCarousel } from "./modules/ui/projects-carousel.js";
import { initCaseMediaSlider } from "./modules/cases/media-slider.js";
import { initSkillsAccordion } from "./modules/ui/skills-accordion.js";
import { initSkillsIntroMessages } from "./modules/ui/skills-intro.js";
import { initFilterChips } from "./modules/ui/filter-chips.js";
import { initContentEditMode } from "./modules/ui/content-edit/content-edit-mode.js";
import { initDeferredPortfolioCovers } from "./modules/ui/deferred-media.js";
import { initImageLoadPlaceholder } from "./modules/ui/image-load-placeholder.js";
@@ -54,6 +55,7 @@ initMiniProjectsExpand(miniProjectsMasonry);
initExperienceHoverGlow();
initContactCopy();
initSkillsIntroMessages();
initFilterChips();
initCaseNaturalImagesHalfSize();
initCaseSequentialImages();
initCaseTallMediaExpand();
+79
View File
@@ -0,0 +1,79 @@
/**
* Фильтр-чипы — переключение видимости элементов по тегу.
*
* Разметка (см. DESIGN.md → Компоненты → Фильтр-чипы):
* <ul class="filter-chips" data-filter-chips>
* <li><button class="filter-chip" type="button" data-filter-value="all" aria-pressed="true">Все</button></li>
* <li><button class="filter-chip" type="button" data-filter-value="bots" aria-pressed="false">Боты</button></li>
* </ul>
*
* Партнёрские элементы:
* <li data-filter-item data-filter-tag="bots mcp">…</li>
*
* Значение `all` (или пустое) показывает все элементы. Несколько тегов
* на элементе пишутся через пробел и работают как OR.
*
* Счётчики в чипах (`.filter-chip__count`) считаются автоматически:
* сколько элементов соответствует этому значению.
*/
export function initFilterChips() {
const root = document.querySelector("[data-filter-chips]");
if (!root) return;
const scope = document.querySelector(root.getAttribute("data-filter-scope")) || document;
const items = Array.from(scope.querySelectorAll("[data-filter-item]"));
if (!items.length) return;
const chips = Array.from(root.querySelectorAll("[data-filter-value]"));
if (!chips.length) return;
const ALL = "all";
function matches(item, value) {
if (value === ALL || !value) return true;
const tags = (item.getAttribute("data-filter-tag") || "").split(/\s+/);
return tags.includes(value);
}
function apply(value) {
items.forEach((item) => {
const show = matches(item, value);
if (show) {
item.removeAttribute("hidden");
} else {
item.setAttribute("hidden", "");
}
});
chips.forEach((chip) => {
chip.setAttribute("aria-pressed", chip.getAttribute("data-filter-value") === value ? "true" : "false");
});
}
function fillCounts() {
chips.forEach((chip) => {
const value = chip.getAttribute("data-filter-value");
const count = items.filter((item) => matches(item, value)).length;
let slot = chip.querySelector(".filter-chip__count");
if (!slot) {
slot = document.createElement("span");
slot.className = "filter-chip__count";
slot.setAttribute("aria-hidden", "true");
chip.appendChild(slot);
}
slot.textContent = String(count);
});
}
chips.forEach((chip) => {
chip.addEventListener("click", () => {
apply(chip.getAttribute("data-filter-value") || ALL);
});
});
fillCounts();
// Стартовое состояние берём из чипа, который уже нажат в разметке,
// иначе после bfcache-возврата фильтр сбросился бы на «все».
const initial = chips.find((chip) => chip.getAttribute("aria-pressed") === "true");
apply(initial ? initial.getAttribute("data-filter-value") || ALL : ALL);
}
+141
View File
@@ -0,0 +1,141 @@
// Проверка логики initFilterChips без браузера: собираем дерево из собранной
// страницы public/ai-dev/index.html и прогоняем по нему реальный модуль.
import fs from "node:fs";
const HTML = fs.readFileSync(process.argv[2] || "public/ai-dev/index.html", "utf8");
class El {
constructor() {
this.attrs = {};
this.children = [];
this._text = "";
this.listeners = {};
}
setAttribute(k, v) {
this.attrs[k] = String(v);
}
getAttribute(k) {
return this.attrs[k] ?? null;
}
removeAttribute(k) {
delete this.attrs[k];
}
hasAttribute(k) {
return k in this.attrs;
}
appendChild(c) {
this.children.push(c);
return c;
}
*walk() {
for (const c of this.children) {
yield c;
yield* c.walk();
}
}
querySelector(sel) {
const cls = sel.startsWith(".") ? sel.slice(1) : null;
for (const d of this.walk()) {
if (cls && (d.getAttribute("class") || "").split(/\s+/).includes(cls)) return d;
}
return null;
}
querySelectorAll(sel) {
const attr = sel.replace(/[[\]]/g, "");
const out = [];
for (const d of this.walk()) if (d.hasAttribute(attr)) out.push(d);
return out;
}
addEventListener(ev, fn) {
this.listeners[ev] = fn;
}
click() {
this.listeners.click?.();
}
get textContent() {
return this._text;
}
set textContent(v) {
this._text = String(v);
}
// Модуль ставит slot.className = "..." — в реальном DOM это атрибут class.
set className(v) {
this.attrs.class = String(v);
}
get className() {
return this.attrs.class || "";
}
}
const scopeSel = (HTML.match(/data-filter-scope="([^"]+)"/) || [, "#ai-projects"])[1];
const scopeId = scopeSel.replace(/^#/, "");
const chipsRoot = new El();
chipsRoot.setAttribute("data-filter-scope", scopeSel);
const scope = new El();
scope.setAttribute("id", scopeId);
const chipRe =
/<button[^>]*class="filter-chip"[^>]*data-filter-value="([^"]+)"[^>]*aria-pressed="([^"]*)"[^>]*>([^<]*)<\/button>/g;
for (const m of HTML.matchAll(chipRe)) {
const li = new El();
const b = new El();
b.setAttribute("class", "filter-chip");
b.setAttribute("data-filter-value", m[1]);
b.setAttribute("aria-pressed", m[2]);
b.textContent = m[3];
li.appendChild(b);
chipsRoot.appendChild(li);
}
const itemRe = /<(\w+)([^>]*\bdata-filter-item\b[^>]*)>/g;
for (const m of HTML.matchAll(itemRe)) {
const li = new El();
const tagm = m[2].match(/data-filter-tag="([^"]+)"/);
li.setAttribute("data-filter-item", "");
li.setAttribute("data-filter-tag", tagm ? tagm[1] : "");
scope.appendChild(li);
}
const mod = await import("../public/js/modules/ui/filter-chips.js");
// Подменяем document на наше дерево.
globalThis.document = {
querySelector: (s) => {
if (s === "[data-filter-chips]") return chipsRoot;
if (s === scopeSel) return scope;
return null;
},
createElement: () => new El(),
};
mod.initFilterChips();
const items = () => scope.querySelectorAll("[data-filter-item]");
const chips = () => chipsRoot.querySelectorAll("[data-filter-value]");
const visible = () => items().filter((i) => !i.hasAttribute("hidden")).length;
const pressed = () => chips().filter((c) => c.getAttribute("aria-pressed") === "true").map((c) => c.getAttribute("data-filter-value"));
const shownTags = () => items().filter((i) => !i.hasAttribute("hidden")).map((i) => i.getAttribute("data-filter-tag"));
console.log(`chips=${chips().length} items=${items().length}`);
console.log("counts in DOM:", chips().map((c) => {
const slot = c.children.find((k) => (k.getAttribute("class") || "").includes("filter-chip__count"));
return `${c.getAttribute("data-filter-value")}=${slot ? slot.textContent : "MISSING"}`;
}).join(" "));
console.log("initial:", visible(), pressed());
let fail = 0;
const tagOf = (i) => (i.getAttribute("data-filter-tag") || "").split(/\s+/);
const expect = {};
for (const c of chips()) {
const v = c.getAttribute("data-filter-value");
expect[v] = v === "all" ? items().length : items().filter((i) => tagOf(i).includes(v)).length;
}
console.log("expected:", JSON.stringify(expect));
for (const [val, want] of Object.entries(expect)) {
chips().find((c) => c.getAttribute("data-filter-value") === val).click();
const got = visible();
const ok = got === want && pressed().length === 1 && pressed()[0] === val;
if (!ok) fail++;
console.log(`${ok ? "OK " : "FAIL"} ${val.padEnd(8)} visible=${got} (want ${want}) pressed=${pressed()} tags=${JSON.stringify(shownTags())}`);
}
console.log(fail === 0 ? "\nALL PASS" : `\n${fail} FAILURES`);
process.exit(fail === 0 ? 0 : 1);
+196 -262
View File
@@ -5,12 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta
name="description"
content="Николай Мамадаев, AI-разработчик. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты."
content="Николай Мамадаев, AI-разработник. Делаю целые продукты: Telegram-боты, MCP-серверы, плагины для Figma и Obsidian, desktop-утилиты."
/>
<title>AI-разработка · Николай Мамадаев</title>
{{> head-links}}
{{> head-plyr}}
<style>
/* Локальные правила страницы. Всё остальное — компоненты дизайн-системы:
.contact-grid (сетка), .mp-card (карточка), .filter-chips (переключатель),
.mp-card__tags/.mp-card__tag (метаданные), .case-links-grid (ссылки),
.hero__tags/.hero__tag (список навыков). */
.ai-page {
max-width: 52rem;
margin: 0 auto;
@@ -28,24 +32,19 @@
.ai-page a:not(.btn) {
color: var(--accent);
}
/* Сетка карточек проектов: 2 колонки, схлопывается в одну на мобильном */
/* Сетка проектов: контракт .contact-grid, но 2 колонки и шире минимальная
колонка — карточкам нужен воздух. Сама сетка и её адаптив из системы. */
.ai-projects {
list-style: none;
margin: 1.25rem 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: 1rem;
}
.ai-projects > li {
margin: 0;
margin-bottom: 0;
}
/* Обложка- заглушка внутри карточки. */
.ai-projects__media {
border-radius: var(--radius-md);
border-radius: var(--radius-sm);
overflow: hidden;
border: 1px solid var(--line);
margin-bottom: 0.7rem;
background: var(--surface);
background: var(--bg-elevated);
}
.ai-projects__media img {
display: block;
@@ -54,56 +53,19 @@
aspect-ratio: 16 / 10;
object-fit: cover;
}
.ai-projects__head {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 0.75rem;
}
.ai-projects__title {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
color: var(--text);
}
/* Стек проекта — моноширинный, как .filter-chip__count в системе. */
.ai-projects__stack {
margin: 0.3rem 0 0;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.78rem;
color: var(--text-muted);
}
.ai-projects__desc {
margin: 0.45rem 0 0;
font-size: 0.95rem;
color: var(--text);
}
.ai-projects__links {
list-style: none;
margin: 0.6rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.4rem 0.9rem;
font-size: 0.85rem;
}
/* Навыки — компактные чипы переиспользуют стили тегов карточек активности */
.ai-stack-list {
list-style: none;
margin: 1rem 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
}
.ai-stack-list li {
margin: 0;
padding: 0.3rem 0.7rem;
border: 1px solid var(--line);
border-radius: 999px;
font-family: var(--font-mono, ui-monospace, monospace);
font-size: 0.8rem;
font-size: 0.76rem;
line-height: 1.4;
color: var(--text-muted);
}
/* У mp-card--compact теги уходят вниз карточки (order 2) — оставляем
стек под заголовком, поэтому поднимаем его наравне с описанием. */
.mp-card--compact .ai-projects__stack {
order: 1;
}
</style>
</head>
<body>
@@ -123,228 +85,200 @@
</p>
<h2 id="roles">Что я делаю</h2>
<ul class="section__prose ai-stack-list">
<li>Продуктовая логика и API</li>
<li>Telegram-боты на aiogram 3</li>
<li>MCP-серверы</li>
<li>Плагины Figma и Obsidian</li>
<li>Автоматизация и агентные пайплайны</li>
<ul class="section__prose hero__tags" aria-label="Что я делаю">
<li class="hero__tag">Продуктовая логика и API</li>
<li class="hero__tag">Telegram-боты на aiogram 3</li>
<li class="hero__tag">MCP-серверы</li>
<li class="hero__tag">Плагины Figma и Obsidian</li>
<li class="hero__tag">Автоматизация и агентные пайплайны</li>
</ul>
<h2 id="projects">Проекты</h2>
<ol class="ai-projects">
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Personal Assistant</h3>
</div>
<p class="ai-projects__stack">Python · aiogram 3 · PostgreSQL</p>
<p class="ai-projects__desc">
Личный ассистент в Telegram: напоминания, планирование дел и работа с LLM
в одном диалоге.
</p>
<ul class="ai-projects__links">
<li><a href="{{casesPrefix}}ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<ul class="filter-chips" data-filter-chips data-filter-scope="#ai-projects" aria-label="Фильтр проектов по типу">
<li><button type="button" class="filter-chip" data-filter-value="all" aria-pressed="true">Все</button></li>
<li><button type="button" class="filter-chip" data-filter-value="bots" aria-pressed="false">Бots</button></li>
<li><button type="button" class="filter-chip" data-filter-value="mcp" aria-pressed="false">MCP</button></li>
<li><button type="button" class="filter-chip" data-filter-value="plugins" aria-pressed="false">Плагины</button></li>
<li><button type="button" class="filter-chip" data-filter-value="web" aria-pressed="false">Веб</button></li>
</ul>
<ol class="contact-grid ai-projects" id="ai-projects">
<li data-filter-item data-filter-tag="bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Personal Assistant</h3>
<p class="ai-projects__stack">Python · aiogram 3 · PostgreSQL</p>
<p class="mp-card__desc">Личный ассистент в Telegram: напоминания, планирование дел и работа с LLM в одном диалоге.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Боты</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Image Gen Bot</h3>
</div>
<p class="ai-projects__stack">Python · aiogram 3 · Redis · PostgreSQL</p>
<p class="ai-projects__desc">
Бот для генерации изображений: баланс, реферальная программа, админ-логи
в отдельные топики супергруппы.
</p>
<ul class="ai-projects__links">
<li><a href="{{casesPrefix}}ai-platform-primo-rpa/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Image Gen Bot</h3>
<p class="ai-projects__stack">Python · aiogram 3 · Redis · PostgreSQL</p>
<p class="mp-card__desc">Бот для генерации изображений: баланс, реферальная программа, админ-логи в отдельные топики супергруппы.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Боты</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-03.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">MCP Image Server</h3>
</div>
<p class="ai-projects__stack">JavaScript · MCP · OpenRouter</p>
<p class="ai-projects__desc">
Собственный MCP-сервер для генерации картинок. Подключается к агентам
как инструмент.
</p>
<ul class="ai-projects__links">
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/mcp-img-gen" target="_blank" rel="noopener noreferrer">Код</a></li>
</ul>
<li data-filter-item data-filter-tag="mcp bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-03.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">MCP Image Server</h3>
<p class="ai-projects__stack">JavaScript · MCP · OpenRouter</p>
<p class="mp-card__desc">Собственный MCP-сервер для генерации картинок. Подключается к агентам как инструмент.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">MCP</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">AI assistant</h3>
</div>
<p class="ai-projects__stack">TypeScript · Figma Plugin API</p>
<p class="ai-projects__desc">
Плагин для Figma, который исполняет скрипты прямо в редакторе.
ИИ-ассистент для любого тарифа.
</p>
<ul class="ai-projects__links">
<li><a href="{{casesPrefix}}ai-assistant-figma/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="plugins">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">AI assistant</h3>
<p class="ai-projects__stack">TypeScript · Figma Plugin API</p>
<p class="mp-card__desc">Плагин для Figma, который исполняет скрипты прямо в редакторе. ИИ-ассистент для любого тарифа.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагины</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Card Columns</h3>
</div>
<p class="ai-projects__stack">TypeScript · Obsidian API</p>
<p class="ai-projects__desc">
Вью для заметок в стиле канбан-доски: группировка по колонкам с фильтрами
по тегам.
</p>
<ul class="ai-projects__links">
<li><a href="{{casesPrefix}}card-columns-obsidian/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="plugins">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Card Columns</h3>
<p class="ai-projects__stack">TypeScript · Obsidian API</p>
<p class="mp-card__desc">Вью для заметок в стиле канбан-доски: группировка по колонкам с фильтрами по тегам.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагины</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-03.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Hermes Desktop Plugins</h3>
</div>
<p class="ai-projects__stack">JavaScript · TypeScript · Electron</p>
<p class="ai-projects__desc">
Плагины для рабочего стола Hermes: чип баланса API, звук завершения задачи
и контроль трат.
</p>
<ul class="ai-projects__links">
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/unorouter-balance" target="_blank" rel="noopener noreferrer">Код</a></li>
</ul>
<li data-filter-item data-filter-tag="plugins">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-03.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Hermes Desktop Plugins</h3>
<p class="ai-projects__stack">JavaScript · TypeScript · Electron</p>
<p class="mp-card__desc">Плагины для рабочего стола Hermes: чип баланса API, звук завершения задачи и контроль трат.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагины</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">Social Post Writer</h3>
</div>
<p class="ai-projects__stack">Python · aiogram 3 · SQLite</p>
<p class="ai-projects__desc">
Бот, который превращает тезисы в готовый пост под нужную площадку и тон,
учитывая голос автора.
</p>
<ul class="ai-projects__links">
<li><a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin/social-post-writer-bot" rel="noopener noreferrer">Код</a></li>
</ul>
<li data-filter-item data-filter-tag="bots">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-01.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">Social Post Writer</h3>
<p class="ai-projects__stack">Python · aiogram 3 · SQLite</p>
<p class="mp-card__desc">Бот, который превращает тезисы в готовый пост под нужную площадку и тон, учитывая голос автора.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Боты</li>
</ul>
</div>
</article>
</li>
<li>
<div class="ai-projects__media">
<img
src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp"
alt=""
width="640"
height="400"
loading="lazy"
decoding="async"
/>
</div>
<div class="ai-projects__head">
<h3 class="ai-projects__title">AI Design Assistant</h3>
</div>
<p class="ai-projects__stack">HTML · CSS · GLSL · WebGL</p>
<p class="ai-projects__desc">
Этот сайт целиком: структура, дизайн-система, шейдеры, CI/CD.
Весь цикл создания вместе с ИИ-агентом.
</p>
<ul class="ai-projects__links">
<li><a href="{{casesPrefix}}site-portfolio/" target="_blank" rel="noopener noreferrer">Кейс</a></li>
</ul>
<li data-filter-item data-filter-tag="web">
<article class="mp-card mp-card--compact" data-glass="false">
<div class="mp-card__media ai-projects__media" aria-hidden="true">
<img class="mp-card__image" src="{{assetPrefix}}assets/ai-dev/placeholder-02.webp" alt="" width="640" height="400" loading="lazy" decoding="async" />
</div>
<div class="mp-card__body">
<h3 class="mp-card__title">AI Design Assistant</h3>
<p class="ai-projects__stack">HTML · CSS · GLSL · WebGL</p>
<p class="mp-card__desc">Этот сайт целиком: структура, дизайн-система, шейдеры, CI/CD. Весь цикл создания вместе с ИИ-агентом.</p>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Веб</li>
</ul>
</div>
</article>
</li>
</ol>
<h2 id="stack">Стек</h2>
<ul class="section__prose ai-stack-list">
<li>Python</li>
<li>TypeScript</li>
<li>JavaScript</li>
<li>Go</li>
<li>HTML/CSS</li>
<li>GLSL</li>
<li>aiogram 3</li>
<li>FastAPI</li>
<li>PostgreSQL</li>
<li>Redis</li>
<li>SQLite</li>
<li>Docker</li>
<li>Coolify</li>
<li>Gitea</li>
<li>MCP</li>
<ul class="section__prose hero__tags" aria-label="Стек">
<li class="hero__tag">Python</li>
<li class="hero__tag">TypeScript</li>
<li class="hero__tag">JavaScript</li>
<li class="hero__tag">Go</li>
<li class="hero__tag">HTML/CSS</li>
<li class="hero__tag">GLSL</li>
<li class="hero__tag">aiogram 3</li>
<li class="hero__tag">FastAPI</li>
<li class="hero__tag">PostgreSQL</li>
<li class="hero__tag">Redis</li>
<li class="hero__tag">SQLite</li>
<li class="hero__tag">Docker</li>
<li class="hero__tag">Coolify</li>
<li class="hero__tag">Gitea</li>
<li class="hero__tag">MCP</li>
</ul>
<h2 id="links">Где я это выкладываю</h2>
<p class="section__prose">
Код открыт: <a href="https://github.com/mamadaevv" target="_blank" rel="noopener noreferrer">GitHub</a>
и <a href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin" target="_blank" rel="noopener noreferrer">Gitea</a>.
Сервисы, которые работают прямо сейчас, живут на
<a href="https://secrets-4-me.dynv6.net" target="_blank" rel="noopener noreferrer">secrets-4-me</a>.
</p>
<ul class="case-links-grid">
<li>
<a class="case-link-card" href="https://github.com/mamadaevv" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">GitHub</span>
<span class="case-link-card__meta">Проекты и эксперименты с кодом</span>
</span>
</a>
</li>
<li>
<a class="case-link-card" href="http://x1qnvk08zec2u5cx6yh5iexh.155.212.186.39.sslip.io/admin" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">Gitea</span>
<span class="case-link-card__meta">Внутренние проекты и сервисы</span>
</span>
</a>
</li>
<li>
<a class="case-link-card" href="{{casesPrefix}}site-portfolio/" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">Кейс этого сайта</span>
<span class="case-link-card__meta">Как я делал сайт вместе с ИИ-агентом</span>
</span>
</a>
</li>
<li>
<a class="case-link-card" href="https://secrets-4-me.dynv6.net" target="_blank" rel="noopener noreferrer">
<span class="case-link-card__body">
<span class="case-link-card__title">secrets-4-me</span>
<span class="case-link-card__meta">Сервис, который работает сейчас</span>
</span>
</a>
</li>
</ul>
</div>
</main>
</body>
+20
View File
@@ -724,6 +724,26 @@
</ul>
<p class="sg-label">.mp-card__tags / .mp-card__tag</p>
<h3>Фильтр-чипы</h3>
<p class="section__prose">
Переключаемая группа тегов над списком карточек. Нажатый чип показывает только
те элементы, у которых этот тег есть в <code>data-filter-tag</code>. Остальные
скрываются атрибутом <code>hidden</code>, поэтому сетка не оставляет дыр.
</p>
<ul class="filter-chips" data-filter-chips data-filter-scope="#filter-demo" aria-label="Фильтр демо-карточек">
<li><button type="button" class="filter-chip" data-filter-value="all" aria-pressed="true">Все</button></li>
<li><button type="button" class="filter-chip" data-filter-value="bots" aria-pressed="false">Боты</button></li>
<li><button type="button" class="filter-chip" data-filter-value="plugins" aria-pressed="false">Плагины</button></li>
<li><button type="button" class="filter-chip" data-filter-value="web" aria-pressed="false">Веб</button></li>
</ul>
<ul class="mp-card__tags" id="filter-demo" style="display: grid; gap: 0.4rem; list-style: none; padding: 0; margin: 0.5rem 0">
<li class="filter-demo-item" data-filter-item data-filter-tag="bots web" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Бот на aiogram + веб-панель</li>
<li class="filter-demo-item" data-filter-item data-filter-tag="bots" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Telegram-бот генерации картинок</li>
<li class="filter-demo-item" data-filter-item data-filter-tag="plugins" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Плагин для Figma</li>
<li class="filter-demo-item" data-filter-item data-filter-tag="plugins web" style="padding: 0.5rem 0.7rem; border: 1px solid var(--line); border-radius: var(--radius-sm); font-size: 0.88rem">Плагин для Obsidian</li>
</ul>
<p class="sg-label">.filter-chips / .filter-chip[aria-pressed] + [data-filter-item][data-filter-tag]</p>
<h3>Карточка портфолио</h3>
<div class="sg-portfolio-wrap">
<a class="mp-card mp-card--link mp-card--stack-bottom" href="#components" tabindex="-1" aria-hidden="true">