Files
site-portfolio/public/cases/card-columns-obsidian/index.html
T
root fe9248c212 fix(contacts): remove Yandex card from contacts block
- drop contact-card--brand-ya (yandex chat) from contact-section ru/en
- update cookie-policy contacts enumeration to drop Yandex (ru/en)
- rebuild public/
2026-09-16 12:50:10 +02:00

816 lines
49 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru" data-theme="dark" data-radius="medium" data-font="merriweather">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta
name="description"
content="Кейс: Card Columns, плагин-вид для Obsidian Bases. Показывает заметки как карточки, разложенные по колонкам, с фильтрацией тегов по И/ИЛИ."
/>
<title>Card Columns, плагин для Obsidian Bases: кейс</title>
<script>
(function () {
var FONT_CSS = {
merriweather: "/assets/fonts/merriweather.css",
geologica: "/assets/fonts/geologica.css",
mono: "/assets/fonts/mono.css",
};
try {
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
var d = document.documentElement;
var m = t.mode;
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
var eff =
m === "system"
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: m;
d.dataset.theme = eff;
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
if (h !== null) d.style.setProperty("--theme-h", String(h));
if (s !== null) d.style.setProperty("--theme-s", String(s));
if (l !== null) {
var L = Number(l);
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
}
var hVal = h !== null ? h : 210;
var sVal = s !== null ? s : 90;
var lVal = l !== null ? Math.min(70, Math.max(20, Number(l))) : 40;
var H = (((hVal % 360) + 360) % 360) / 360;
var S = Math.min(100, Math.max(0, sVal)) / 100;
var Lv = Math.min(100, Math.max(0, lVal)) / 100;
var r, g, b;
if (S === 0) { r = g = b = Lv; }
else {
var q = Lv < 0.5 ? Lv * (1 + S) : Lv + S - Lv * S;
var p = 2 * Lv - q;
var hue2rgb = function(p, q, t) {
if (t < 0) t += 1; if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
};
r = hue2rgb(p, q, H + 1/3);
g = hue2rgb(p, q, H);
b = hue2rgb(p, q, H - 1/3);
}
var lum = 0.2126 * r + 0.7152 * g + 0.0722 * b;
d.style.setProperty("--on-accent", lum > 0.58 ? "hsl(220 14% 12%)" : "hsl(0 0% 98%)");
var rad = t.radius;
if (rad === "soft" || rad === "medium" || rad === "strong") {
d.dataset.radius = rad;
} else {
d.dataset.radius = "medium";
}
var f = t.font;
if (f === "onest") f = "geologica";
if (f === "anonymous") f = "mono";
if (f !== "merriweather" && f !== "mono" && f !== "geologica") f = "merriweather";
d.dataset.font = f;
var mb = t.meshBg;
if (mb === "off") {
d.dataset.meshBg = "off";
} else {
d.dataset.meshBg = "on";
}
var href = FONT_CSS[f];
if (href && !document.querySelector('link[data-theme-font="' + f + '"]')) {
var link = document.createElement("link");
link.rel = "stylesheet";
link.href = href;
link.setAttribute("data-theme-font", f);
document.head.appendChild(link);
}
} catch (e) {}
})();
</script>
<link rel="icon" type="image/png" sizes="32x32" href="/assets/favicon/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/favicon/apple-touch-icon.png" />
<link rel="manifest" href="/assets/favicon/site.webmanifest" />
<link rel="preload" href="/assets/fonts/merriweather.css" as="style" />
<link rel="preload" href="/css/main.css" as="style" />
<link rel="preload" href="/js/skill-connectors.js" as="script" />
<link rel="modulepreload" href="/js/main.js" />
<link rel="stylesheet" href="/css/main.css" />
<script src="/js/skill-connectors.js" defer></script>
<script type="module" src="/js/main.js"></script>
<link rel="stylesheet" href="/vendor/plyr/plyr.css" />
<style>
.case-article {
max-width: 48rem;
margin: 0 auto;
}
.case-article h2 {
margin: 2rem 0 0.75rem;
font-size: clamp(1.15rem, 2vw, 1.35rem);
font-weight: 700;
color: var(--text);
}
.case-article h3 {
font-size: 1.05rem;
font-weight: 600;
color: var(--text);
}
.case-article .section__prose {
margin-bottom: 0.75rem;
color: var(--text);
}
.case-article img,
.case-article video {
display: block;
max-width: 100%;
height: auto;
margin: 0.75rem 0 1rem;
}
.case-article ul,
.case-article ol {
margin: 0.5rem 0 1rem 1.25rem;
padding: 0;
}
.case-article li {
margin-bottom: 0.35rem;
}
.case-article a:not(.btn) {
color: var(--accent);
}
.case-article .case-brief {
list-style: none;
margin: 1.5rem 0 0;
padding: 1.1rem 1.25rem;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
gap: 0.85rem 1.5rem;
font-size: 0.9rem;
border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
border-radius: var(--radius-md, 12px);
}
.case-article .case-brief li {
margin: 0;
}
.case-article .case-brief span {
display: block;
margin-bottom: 0.15rem;
color: var(--text-muted);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.case-article .case-todo {
margin: 1rem 0 1.25rem;
padding: 0.9rem 1.1rem;
border-left: 2px solid var(--accent);
background: color-mix(in srgb, var(--accent) 8%, transparent);
border-radius: 0 var(--radius-sm, 8px) var(--radius-sm, 8px) 0;
font-size: 0.92rem;
}
.case-article .case-todo p {
margin: 0 0 0.5rem;
}
.case-article .case-todo p:last-child {
margin-bottom: 0;
}
.case-article .case-todo__q {
font-weight: 600;
color: var(--accent);
}
.case-todo__label {
display: block;
margin-bottom: 0.35rem;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
opacity: 0.85;
}
.case-article details.case-details {
margin: 0.75rem 0 1rem;
padding: 0.6rem 0.9rem;
border: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
border-radius: var(--radius-sm, 8px);
}
.case-article details.case-details > summary {
cursor: pointer;
font-weight: 600;
color: var(--accent);
padding: 0.25rem 0;
}
.case-article details.case-details[open] > summary {
margin-bottom: 0.5rem;
}
.case-block {
margin-top: 3rem;
padding-top: 2.5rem;
border-top: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
}
.case-block:first-of-type {
margin-top: 0;
padding-top: 0;
border-top: 0;
}
</style>
<script>
(function () {
try {
var t = JSON.parse(localStorage.getItem("siteThemeV1") || "{}");
var d = document.documentElement;
var m = t.mode;
if (m !== "light" && m !== "dark" && m !== "system") m = "system";
var eff =
m === "system"
? window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: m;
d.dataset.theme = eff;
var h = typeof t.h === "number" ? t.h : typeof t.hue === "number" ? t.hue : null;
var s = typeof t.s === "number" ? t.s : typeof t.sat === "number" ? t.sat : null;
var l = typeof t.l === "number" ? t.l : typeof t.light === "number" ? t.light : null;
if (h !== null) d.style.setProperty("--theme-h", String(h));
if (s !== null) d.style.setProperty("--theme-s", String(s));
if (l !== null) {
var L = Number(l);
if (Number.isFinite(L)) d.style.setProperty("--theme-l", String(Math.min(70, Math.max(20, L))));
}
var rad = t.radius;
if (rad === "soft" || rad === "medium" || rad === "strong") {
d.dataset.radius = rad;
} else {
d.dataset.radius = "medium";
}
var f = t.font;
if (f === "onest") f = "geologica";
if (f === "anonymous") f = "mono";
if (f === "merriweather" || f === "mono" || f === "geologica") {
d.dataset.font = f;
} else {
d.dataset.font = "merriweather";
}
var mb = t.meshBg;
if (mb === "off") {
d.dataset.meshBg = "off";
} else {
d.dataset.meshBg = "on";
}
} catch (e) {}
})();
</script>
</head>
<body data-current-case="card-columns">
<a class="skip-link" href="#case-main">К содержимому кейса</a>
<header class="header" data-theme-picker>
<div class="header__inner">
<a class="logo" href="../../" aria-label="На главную">НМ</a>
<div class="header__cluster">
<nav class="nav" aria-label="Разделы страницы">
<a href="#contact">Контакты</a>
</nav>
</div>
</div>
</header>
<div class="fab-stack" aria-label="Быстрые действия">
<div class="theme-panel-fab-wrap">
<button type="button" class="fab fab--secondary glass-neutral" data-theme-panel-btn aria-expanded="false" aria-controls="theme-panel" aria-label="Настройки стиля">
<span class="theme-panel-fab__icon theme-panel-fab__icon--open-hide" aria-hidden="true"></span>
<svg class="theme-panel-fab__icon theme-panel-fab__icon--close" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M6 6L18 18M18 6L6 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
<div id="theme-panel" class="theme-picker__panel glass-neutral" role="dialog" aria-label="Тема и оформление" hidden data-theme-panel>
<div class="theme-picker__stack">
<section class="theme-picker__section" aria-labelledby="theme-accent-title">
<h3 id="theme-accent-title" class="theme-picker__section-title">Акцентный цвет</h3>
<div class="colorpicker" data-colorpicker>
<div class="colorpicker__plane-box">
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Насыщенность и светлость акцента"></canvas>
<div class="colorpicker__knob" data-sl-knob></div>
</div>
<div class="colorpicker__hue-wrap">
<label class="colorpicker__hue-sr" for="theme-hue-slider">Оттенок акцента</label>
<input id="theme-hue-slider" class="colorpicker__hue" type="range" min="0" max="360" value="210" data-hue-slider aria-valuemin="0" aria-valuemax="360" />
</div>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-mode-title">
<h3 id="theme-mode-title" class="theme-picker__section-title">Тема</h3>
<div class="radius-seg theme-mode-seg" role="radiogroup" aria-labelledby="theme-mode-title">
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="system" aria-label="Как в системе"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--system"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="light" aria-label="Светлая тема"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--light"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-theme-mode="dark" aria-label="Тёмная тема"><span class="radius-seg__viz" aria-hidden="true"><span class="theme-mode-seg__ico theme-mode-seg__ico--dark"></span></span></button>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-radius-title">
<h3 id="theme-radius-title" class="theme-picker__section-title">Скругление углов</h3>
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-radius-title">
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="soft" aria-label="Слабое скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--soft"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="medium" aria-label="Среднее скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--medium"></span></span></button>
<button type="button" class="radius-seg__btn" role="radio" aria-checked="false" data-radius-option="strong" aria-label="Сильное скругление"><span class="radius-seg__viz" aria-hidden="true"><span class="radius-seg__arc radius-seg__arc--strong"></span></span></button>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-typography-title">
<h3 id="theme-typography-title" class="theme-picker__section-title">Типографика</h3>
<div class="radius-seg" role="radiogroup" aria-labelledby="theme-typography-title">
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="geologica" aria-label="Sans"><span class="theme-font-seg__label">Sans</span></button>
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="merriweather" aria-label="Sans Serif"><span class="theme-font-seg__label">Sans Serif</span></button>
<button type="button" class="radius-seg__btn theme-font-seg__btn" role="radio" aria-checked="false" data-font-option="mono" aria-label="Mono"><span class="theme-font-seg__label">Mono</span></button>
</div>
</section>
<section class="theme-picker__section" aria-labelledby="theme-mesh-bg-title">
<h3 id="theme-mesh-bg-title" class="theme-picker__section-title">Фон страницы</h3>
<div class="radius-seg radius-seg--two" role="radiogroup" aria-labelledby="theme-mesh-bg-title">
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="on" aria-label="Показать фоновый градиент"><span class="theme-mesh-bg-seg__label">Вкл</span></button>
<button type="button" class="radius-seg__btn theme-mesh-bg-seg__btn" role="radio" aria-checked="false" data-mesh-bg-option="off" aria-label="Скрыть фоновый градиент"><span class="theme-mesh-bg-seg__label">Выкл</span></button>
</div>
</section>
</div>
</div>
</div>
<a class="fab fab--primary" href="#contact" aria-label="Связаться">
<span class="fab-primary__ico" aria-hidden="true"></span>
</a>
</div>
<div class="content-edit-fab-stack" aria-label="Правки контента" data-content-edit-ui>
<button
type="button"
class="fab fab--secondary glass-neutral content-edit-fab"
data-content-edit-mode-toggle
hidden
aria-pressed="false"
aria-label="Режим правок контента"
title="Режим правок"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 20h4l10.5-10.5a2.12 2.12 0 0 0 0-3L16.5 4.5a2.12 2.12 0 0 0-3 0L3 15v5z"
stroke="currentColor"
stroke-width="1.75"
stroke-linejoin="round"
/>
</svg>
<span class="content-edit-fab__badge" hidden aria-hidden="true">0</span>
</button>
</div>
<main id="case-main" class="section section--tight">
<div class="section__inner case-article">
<p class="hero__eyebrow">Obsidian Bases · Plugin</p>
<h1 class="case-page__h1">
Канбан-вид для Obsidian Bases,<br />который не&nbsp;трогает ваши заметки
</h1>
<p class="case-page__sub">
Card Columns: заметки как карточки по колонкам,<br />
отбор по тегам И/ИЛИ за два клика
</p>
<!-- МЕДИА: hero-спойлер, финальный экран плагина. Раскомментировать при вставке скрина.
<figure class="case-media case-media--wide">
<img src="../assets/card-columns-obsidian/hero.webp" alt="Card Columns: колонки карточек поверх .base файла" width="1920" height="1080" loading="eager" decoding="async" data-image-viewer />
<figcaption>Вид Columns поверх базы заметок</figcaption>
</figure>
-->
<ul class="case-brief">
<li><span>Роль</span>Продуктовый дизайн и разработка</li>
<li><span>Платформа</span>Obsidian Bases</li>
<li><span>Статус</span>Рабочий плагин, open source (MIT)</li>
<li><span>Команда</span>Соло, пет-проект</li>
<li><span>Срок</span>Черновой ответ, заменить</li>
</ul>
<div class="case-todo">
<span class="case-todo__label">Вопрос к автору</span>
<p class="case-todo__q">
Сколько заняла работа от первой идеи до рабочей версии? Недели? Месяц выходных?
</p>
<p>
Черновой ответ: собрал за три недели вечером, потом ещё месяц доводил по ходу
собственной работы. Заменить на реальное.
</p>
</div>
<p class="section__prose">
В Obsidian появились Bases, отдельный тип файла, который показывает заметки не текстом, а
в разных видах: таблицей, карточками, деревом. Стандартный вид карточек мне не зашёл: он
плоский, без группировки, и в нём не разобраться, когда заметок сотни. Я хотел видеть их
раскладку по колонкам, как канбан, но без лишнего веса. Так появился плагин-вид Card Columns.
</p>
<div class="case-block" id="kontekst">
<h2>Контекст</h2>
<p class="section__prose">
Bases, это не заметка и не отдельная база данных. Это запрос к vault-у: выбираешь
заметки по фильтру, и фреймворк рисует их в выбранном виде. У самого Obsidian есть
встроенный вид карточек, но он одноплановый: нет группировки по свойствам, нет
нормальной фильтрации. Я решил сделать свой вид, который встраивается в переключатель
Bases наравне со штатными.
</p>
<p class="section__prose">
Главное ограничение было в самом фреймворке: плагин не хранит данные, а рисует то, что
выдал Bases-запрос (Sort, Filter, Properties, Search из тулбара). Значит, вся логика
выборки остаётся на Bases, а моя задача, только представление и навигация по нему.
</p>
<!-- МЕДИА: общий скрин/видео обзора — доска колонок поверх .base файла.
<figure class="case-media case-media--wide">
<img src="../assets/card-columns-obsidian/overview.webp" alt="Вид Card Columns поверх .base файла: заметки по колонкам" width="1920" height="1080" loading="lazy" decoding="async" data-image-viewer />
<figcaption>Обзор вида</figcaption>
</figure>
-->
</div>
<div class="case-block" id="pochemu-svoi">
<h2>Почему не готовое</h2>
<p class="section__prose">
Прежде чем писать код, я посмотрел, что уже есть. Три пути, все мимо:
</p>
<p class="section__prose">
<strong>Штатный вид карточек Bases.</strong> Рисует ровный поток карточек без группировки.
Когда заметок за сотню, это лента, в которой нужно скроллить и вглядываться в заголовки.
Отпал первым.
</p>
<p class="section__prose">
<strong>Канбан-плагины из комьюнити.</strong> Большинство тянет за собой свою систему
хранения: доска живёт отдельно от vault-а, заметки дублируются. Получается вторая база,
которую надо синхронизировать. Мне это было не нужно.
</p>
<p class="section__prose">
<strong>Соседний bases-kanban.</strong> Ближайший по духу: цепляется к Bases, делает
канбан. Но он пишет в заметки: перетаскивание карточки меняет frontmatter. Для view-плагина
это риск, о котором ниже.
</p>
<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>
Отказ от drag-and-drop, это не недоделка, а позиция. Плагин представления не должен
писать в пользовательские данные. Это решение определило всё остальное.
</p>
</div>
</aside>
<div class="case-todo">
<span class="case-todo__label">Вопрос к автору</span>
<p class="case-todo__q">
Ты реально ставил и тестировал чужие плагины или ориентировался по описаниям? Какие
именно смотрел?
</p>
<p>
Черновой ответ: поставил три штуки, погонял неделю на своей базе, удалил. bases-kanban
смотрел в исходниках, не ставил. Заменить на реальное, названия плагинов вписать.
</p>
</div>
</div>
<div class="case-block" id="zadacha">
<h2>Задача</h2>
<p class="section__prose">
Спроектировать вид для Bases, который показывает заметки карточками по колонкам и позволяет
быстро отбирать нужное. Жёсткое ограничение: плагин ничего не пишет в заметки и не хранит
своего состояния, а сортировка, фильтр, свойства и поиск из штатного тулбара Bases должны
работать как раньше.
</p>
<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>
Главный риск, сделать вид, который конфликтует со штатным тулбаром Bases. Тогда
пользователь теряет сортировку и поиск, ради которых Bases и берут. Этого нельзя
было допустить.
</p>
</div>
</aside>
</div>
<div class="case-block" id="reshenie">
<h2>Решения</h2>
<p class="section__prose">
Плагин добавляет в Bases вид «Columns». Карточки строятся из результатов Bases-запроса,
группировка задаётся свойством, а отбор, кликабельными тегами. Дальше, пять выборов,
которые определили результат. В каждом есть то, от чего я отказался.
</p>
<!-- МЕДИА: скриншот вида с колонками и карточками внутри.
<figure class="case-media case-media--wide">
<img src="../assets/card-columns-obsidian/board.webp" alt="Колонки карточек с группировкой по свойству" width="1920" height="1080" loading="lazy" decoding="async" data-image-viewer />
<figcaption>Вид с колонками</figcaption>
</figure>
-->
<h3>Группировка по любому свойству</h3>
<p class="section__prose">
Колонки формируются из Group by в тулбаре Bases, по тегу, статусу, категории или любому
frontmatter-свойству. Меняешь свойство в запросе, и доска пересобирается сама. Так одна
и та же база закрывает разные разрезы: проекты по статусу, заметки по тегам, задачи по
приоритету.
</p>
<h3>Фильтр тегов по И и ИЛИ</h3>
<p class="section__prose">
Это то, ради чего плагин и задумывался. Под тулбаром, пилюли тегов, по клику они
отсеивают карточки по всем колонкам сразу:
</p>
<ul class="section__prose">
<li>Левый клик по тегу, показать только его (сбрасывает остальные фильтры).</li>
<li>Левый клик по следующему тегу, показать заметки с ЛЮБЫМ из выбранных (режим ИЛИ).</li>
<li>Ctrl+Клик или правый клик, переключить тег, не сбрасывая выбор.</li>
<li>Кнопка OR/AND, переключить между «любой из» и «все перечисленные» для нескольких тегов.</li>
<li>All, снять все фильтры.</li>
</ul>
<p class="section__prose">
Связка «ИЛИ-добавление левым кликом + точный toggle через Ctrl» получилась интуитивной:
быстрый отбор не сбивает то, что уже выбрано. Альтернативу, вынести фильтр в модалку,
отверг сразу: лишний клик и потеря контекста того, что уже отфильтровано.
</p>
<p class="section__prose">
Выбор здесь не в самой фиче, а в том, где она живёт. Я выбрал пилюли под тулбаром вместо
отдельного окна фильтров, потому что отбор по тегам, это действие на каждом шаге работы
с базой. Модалка превращает его в прерванный сценарий: открыл, настроил, закрыл, проверил.
Пилюли оставляют результат перед глазами, а состояние фильтра видно всегда.
</p>
<!-- МЕДИА: короткое видео фильтрации по тегам (OR / AND).
<figure class="case-media" style="margin: 0.85rem 0 1.2rem">
<div class="case-video-shell">
<video controls preload="metadata" playsinline poster="../assets/card-columns-obsidian/filter-poster.webp" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
<source src="../assets/card-columns-obsidian/filter.webm" type="video/webm" />
<source src="../assets/card-columns-obsidian/filter.mp4" type="video/mp4" />
Ваш браузер не поддерживает видео.
</video>
</div>
<figcaption>Фильтрация по тегам: OR и AND</figcaption>
</figure>
-->
<h3>Карточки и обложки</h3>
<p class="section__prose">
По каждой заметке рисуются свойства как чипы, текст, теги, ссылки, булевы, даты. Можно
показать обложку: из первой картинки в заметке или из отдельного свойства. У обложки
полный контроль, соотношение сторон (Auto, 1:1, 3:2, 4:3, 16:9), рамка, вписывание
(cover/contain), ориентация и место на карточке (над заголовком, под ним, после всего).
</p>
<h3>Решение: настройки вынесены из кода</h3>
<p class="section__prose">
Сначала параметры были захардкожены под мою базу. Это ошибка: вид перестал бы работать
для чужого сценария. Вынес всё в gear-меню, разбив на четыре секции, чтобы вид
подстраивался под разные базы, а не под мой единственный случай. Больше 15 параметров.
</p>
<details class="case-details">
<summary>Показать полный список настроек</summary>
<p class="section__prose"><strong>General</strong></p>
<ul class="section__prose">
<li>Куда открывать карточку: активная панель, всплывающее окно, новая вкладка, сплит вправо / вниз.</li>
<li>Ширина карточки (px).</li>
<li>Колонок в группе (1–6), сетка внутри колонки.</li>
<li>Зебра колонок (чередование фона для сканирования).</li>
<li>Masonry-раскладка (карточки заполняют вертикальные пустоты).</li>
</ul>
<p class="section__prose"><strong>Title</strong></p>
<ul class="section__prose">
<li>Перенос заголовка, жирный заголовок, размер шрифта (px).</li>
</ul>
<p class="section__prose"><strong>Properties</strong></p>
<ul class="section__prose">
<li>Раскладка чипов: стек (вертикально) или сетка (компактно в строку).</li>
<li>Перенос многострочных значений, размер шрифта.</li>
<li>Формат даты и даты-времени + локаль (Moment.js).</li>
</ul>
<p class="section__prose"><strong>Cover</strong></p>
<ul class="section__prose">
<li>Источник: нет / первая картинка в заметке / свойство-обложка.</li>
<li>Стиль (без рамки / с рамкой), соотношение сторон, ориентация, вписывание, позиция на карточке.</li>
</ul>
</details>
<div class="case-todo">
<span class="case-todo__label">Вопрос к автору</span>
<p class="case-todo__q">
Это правда про захардкоженные параметры? Если нет, замени на реальную причину выноса
в настройки: просьбы пользователей, несколько баз с разной структурой, что угодно.
</p>
<p>
Черновой ответ: да, первая версия была под мою базу, настройки появились, когда
понадобилось открыть плагин наружу. Проверить и поправить.
</p>
</div>
<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>
Сплит-превью сделал как в самом Obsidian: левый клик переиспользует панель, Ctrl+Клик,
новую вкладку внутри сплита. Без этого на широком экране карточки неудобно
просматривать рядом с доской.
</p>
</div>
</aside>
<h3>Работает внутри штатного тулбара</h3>
<p class="section__prose">
Вид цепляется к BasesView-фреймворку, поэтому сортировка, фильтр, свойства и поиск из
тулбара Bases остаются рабочими. Плагин только рисует результат запроса, данные не
дублируются и не зависят от отдельного хранилища.
</p>
<!-- МЕДИА: скриншот настроек (gear-меню) с секциями.
<figure class="case-media case-media--content">
<img src="../assets/card-columns-obsidian/settings.webp" alt="Окно настроек плагина: General, Title, Properties, Cover" width="1200" height="675" loading="lazy" decoding="async" data-image-viewer />
<figcaption>Настройки вида</figcaption>
</figure>
-->
</div>
<div class="case-block" id="rezultat">
<h2>Результат и как я это мерял</h2>
<p class="section__prose">
Вид закрыл мою боль: Bases-запрос теперь читается как доска, а не как плоская лента
карточек. Группировка по свойству + отбор по тегам И/ИЛИ, то, чего не хватало штатному
виду. Всё крутится на запросе Bases, так что перенос vault-а и синхронизация ничего не
ломают.
</p>
<h3>Замер: сколько действий на отбор</h3>
<p class="section__prose">
Главная задача, по которой я мерял успех: отобрать заметки по двум тегам и найти нужную
карточку. В штатном виде карточек это скролл ленты с поиском по заголовкам глазами. В
Columns, два клика по пилюлям и колонка перед глазами.
</p>
<div class="case-todo">
<span class="case-todo__label">Вопрос к автору</span>
<p class="case-todo__q">
Можешь прогнать этот замер по-настоящему? Засечь время на 10 повторах задачи: штатный
вид против Columns. Или посчитать точное число действий.
</p>
<p>
Черновой ответ: на выборке из 25 заметок отбор по двум тегам занимал около 40 секунд и
требовал скроллить ленту; в Columns, два клика, примерно 5 секунд. Цифры примерные,
заменить на замеренные.
</p>
</div>
<h3>Замер: живучесть в ежедневной работе</h3>
<p class="section__prose">
Второй критерий субъективнее, но для инструмента под себя он важнее: продолжаю ли я
пользоваться плагином через месяц. Пользуюсь, он встроился в работу с базой и не требует
вспоминать, как им управлять.
</p>
<div class="case-todo">
<span class="case-todo__label">Вопрос к автору</span>
<p class="case-todo__q">
Есть ли внешние сигналы: установки, звёзды на GitHub, отзывы, вопросы в issue? Любая
цифра, подтверждающая, что плагином пользуются не только ты.
</p>
<p>
Черновой ответ: плагин пока ставится вручную, через каталог комьюнити не прошёл, так
что установок единицы и звёзд пока нет. Если есть реальные данные, вписать их сюда,
это сильнее любого-self-report.
</p>
</div>
<p class="section__prose">
Что сделал бы иначе. Drag-and-drop карточек между колонками не стал делать сознательно,
это меняет свойство в заметке, и для view-плагина это отдельный риск целостности данных.
Но вижу запрос на это у пользователей, и, возможно, добавлю как опциональный режим. А
пока плагин живёт вручную (через community-каталог ещё не прошёл), это единственное, что
мешает им пользоваться сходу.
</p>
<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>
Главный вывод: вид для Bases получается рабочим только тогда, когда он уважает
тулбар фреймворка. Я добавил представление, а не вторую систему хранения, и заметки
остались обычными заметками.
</p>
</div>
</aside>
</div>
</div>
</main>
<section class="section section--surface" data-reveal>
<div class="section__inner">
<h2 class="section__title">Смотрите также</h2>
<ol class="mini-projects-grid" data-mini-projects-list>
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="ai-assistant">
<a
class="mp-card mp-card--link mp-card--compact"
href="../ai-assistant-figma/"
aria-label="AI assistant — открыть кейс"
>
<div class="mp-card__body">
<h3 class="mp-card__title">AI assistant</h3>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагин для Figma</li>
</ul>
<p class="mp-card__desc">Плагин для Figma с AI-функциями.</p>
</div>
</a>
</li>
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="card-columns">
<a
class="mp-card mp-card--link mp-card--compact"
href="../card-columns-obsidian/"
aria-label="Card Columns — открыть кейс"
>
<div class="mp-card__body">
<h3 class="mp-card__title">Card Columns</h3>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Плагин для Obsidian</li>
</ul>
<p class="mp-card__desc">Плагин для колоночного отображения карточек в Obsidian.</p>
</div>
</a>
</li>
<li class="mini-projects-grid__item" data-mp-cells="3" data-mp-cells-sm="2" data-related-slug="site-with-ai-ide">
<a
class="mp-card mp-card--link mp-card--compact"
href="../site-with-ai-ide/"
aria-label="Этот сайт — открыть кейс"
>
<div class="mp-card__body">
<h3 class="mp-card__title">Этот сайт</h3>
<ul class="mp-card__tags" aria-label="Тип проекта">
<li class="mp-card__tag">Разработка с AI IDE</li>
</ul>
<p class="mp-card__desc">Разработка сайта с использованием AI IDE.</p>
</div>
</a>
</li>
</ol>
</div>
</section>
<section id="contact" class="section section--surface section--footerish" data-reveal>
<div class="section__inner">
<h2 class="section__title">Контакты</h2>
<ul class="contact-grid">
<li><a class="contact-card contact-card--link contact-card--brand-tg" href="https://t.me/mamadaevv" rel="noopener noreferrer"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"><img src="../../assets/shared/icons/tg.svg" alt="" width="24" height="24" loading="eager" decoding="async" /></span><span class="contact-card__body"><span class="contact-card__name">Telegram</span><span class="contact-card__hint">Отвечу сегодня</span></span></a></li>
<li><a class="contact-card contact-card--link contact-card--brand-vk" href="https://vk.com/mamadaevv" rel="noopener noreferrer"><span class="contact-card__corner" aria-hidden="true"></span><span class="contact-card__icon" aria-hidden="true"><img src="../../assets/shared/icons/vk.svg" alt="" width="24" height="24" loading="eager" decoding="async" /></span><span class="contact-card__body"><span class="contact-card__name">ВКонтакте</span><span class="contact-card__hint">Стабильный коннект</span></span></a></li>
<li><button type="button" class="contact-card contact-card--action contact-card--brand-email" data-contact-copy data-email-b64="bmlrQG1hbWFkYWV2di5zaXRl" data-hint-default="Скопировать адрес" aria-label="Скопировать адрес электронной почты в буфер обмена"><span class="contact-card__corner contact-card__corner--copy" aria-hidden="true"></span><span class="contact-card__icon contact-card__icon--accent" aria-hidden="true"></span><span class="contact-card__body"><span class="contact-card__name">Почта</span><span class="contact-card__hint" data-contact-copy-hint>Скопировать адрес</span></span></button></li>
</ul>
</div>
</section>
<footer class="footer">
<div class="footer__inner">
<span class="footer__chunk footer__chunk--date">© 2015–2026<span class="footer__sep" aria-hidden="true"> · </span></span>
<span class="footer__chunk footer__chunk--main">Дизайн и разработка сайта — Николай Мамадаев</span>
<span class="footer__chunk footer__chunk--meta footer__attr"><span class="footer__sep" aria-hidden="true"> · </span><a href="../../cases/design-system/">Design System</a><span class="footer__sep" aria-hidden="true"> · </span><a href="../../cookie-policy/">Политика cookie</a></span>
<span class="footer__chunk footer__chunk--lang"><span class="footer__sep" aria-hidden="true"> · </span><nav class="lang-switch" aria-label="Выбор языка" data-lang-switch>
<a class="lang-switch__btn" data-lang="ru" href="/">RU</a>
<a class="lang-switch__btn" data-lang="en" href="/en/">EN</a>
</nav>
<script>
(function () {
var p = location.pathname.replace(/^\/+/, "");
var isEn = p.indexOf("en/") === 0;
var base = isEn ? p.slice(3) : p;
var links = document.querySelectorAll("[data-lang-switch] [data-lang]");
for (var i = 0; i < links.length; i++) {
var b = links[i];
var l = b.getAttribute("data-lang");
b.setAttribute("href", (l === "en" ? "/en/" : "/") + base);
if ((l === "en") === isEn) b.setAttribute("aria-current", "true");
}
})();
</script>
</span>
</div>
</footer>
<aside class="cookie-notice glass-neutral" hidden data-cookie-notice>
<p class="cookie-notice__text">
Сайт использует cookie для комфортной работы. Подробнее — в
<a href="../../cookie-policy/" data-cookie-policy-link>cookie policy</a>.
</p>
<button type="button" class="btn btn--secondary btn--flat cookie-notice__accept" data-cookie-accept>
<span>Принять</span>
<span class="cookie-notice__accept-icon" aria-hidden="true">
<img src="../../assets/shared/icons/сookie.svg" alt="" width="20" height="20" loading="eager" decoding="async" />
</span>
</button>
</aside>
<div id="nav-more-menu-floating" class="nav-more__floating" hidden data-nav-more-menu-floating aria-hidden="true"></div>
<script>
document.addEventListener('DOMContentLoaded',function(){
var s=document.body.dataset.currentCase;
if(!s)return;
document.querySelectorAll('[data-related-slug="'+s+'"]').forEach(function(el){el.hidden=true;});
});
</script>
</body>
</html>