- drop contact-card--brand-ya (yandex chat) from contact-section ru/en - update cookie-policy contacts enumeration to drop Yandex (ru/en) - rebuild public/
816 lines
49 KiB
HTML
816 lines
49 KiB
HTML
<!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 />который не трогает ваши заметки
|
||
</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>
|