Files
site-portfolio/public/cases/card-columns-obsidian/index.html
T

719 lines
44 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: группировка заметок по свойствам, фильтрация тегов AND/OR и перенос карточек между колонками."
/>
<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="modulepreload" href="/js/modules/ui/page-mesh-shader.js" />
<link rel="stylesheet" href="/css/main.css" />
<link rel="canonical" href="https://mamadaevv.site/cases/card-columns-obsidian/" />
<link rel="alternate" hreflang="ru" href="https://mamadaevv.site/cases/card-columns-obsidian/" />
<link rel="alternate" hreflang="en" href="https://mamadaevv.site/en/cases/card-columns-obsidian/" />
<link rel="alternate" hreflang="x-default" href="https://mamadaevv.site/cases/card-columns-obsidian/" />
<meta name="author" content="Николай Мамадаев" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#0f1115" />
<!-- Open Graph -->
<meta property="og:type" content="article" />
<meta property="og:site_name" content="Николай Мамадаев — продуктовый дизайнер" />
<meta property="og:locale" content="ru_RU" />
<meta property="og:locale:alternate" content="en_US" />
<meta property="og:title" content="Card Columns для Obsidian Bases: кейс о канбан-виде" />
<meta property="og:description" content="Как я создал Card Columns для Obsidian Bases: группировка заметок по свойствам, фильтрация тегов AND/OR и перенос карточек между колонками." />
<meta property="og:url" content="https://mamadaevv.site/cases/card-columns-obsidian/" />
<meta property="og:image" content="https://mamadaevv.site/assets/og/og-default.png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:image:alt" content="Card Columns для Obsidian Bases: кейс о канбан-виде" />
<!-- Twitter / X -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="Card Columns для Obsidian Bases: кейс о канбан-виде" />
<meta name="twitter:description" content="Как я создал Card Columns для Obsidian Bases: группировка заметок по свойствам, фильтрация тегов AND/OR и перенос карточек между колонками." />
<meta name="twitter:image" content="https://mamadaevv.site/assets/og/og-default.png" />
<meta name="twitter:image:alt" content="Card Columns для Obsidian Bases: кейс о канбан-виде" />
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@graph": [
{
"@type": "Person",
"@id": "https://mamadaevv.site/#person",
"name": "Николай Мамадаев",
"url": "https://mamadaevv.site",
"jobTitle": "Продуктовый дизайнер",
"description": "Продуктовый дизайнер с опытом более 10 лет. Проектирую цифровые продукты, интерфейсы и дизайн-системы.",
"knowsAbout": [
"UX/UI-дизайн",
"продуктовый дизайн",
"дизайн-системы",
"прототипирование",
"Figma",
"no-code"
],
"sameAs": [
"https://t.me/mamadaevv",
"https://vk.com/mamadaevv",
"https://github.com/mamadaevv"
]
},
{
"@type": "WebSite",
"@id": "https://mamadaevv.site/#website",
"url": "https://mamadaevv.site/",
"name": "Николай Мамадаев — продуктовый дизайнер",
"inLanguage": "ru",
"author": {
"@id": "https://mamadaevv.site/#person"
},
"publisher": {
"@id": "https://mamadaevv.site/#person"
}
},
{
"@type": "CreativeWork",
"@id": "https://mamadaevv.site/cases/card-columns-obsidian/#page",
"url": "https://mamadaevv.site/cases/card-columns-obsidian/",
"name": "Card Columns для Obsidian Bases: кейс о канбан-виде",
"description": "Как я создал Card Columns для Obsidian Bases: группировка заметок по свойствам, фильтрация тегов AND/OR и перенос карточек между колонками.",
"isPartOf": {
"@id": "https://mamadaevv.site/#website"
},
"author": {
"@id": "https://mamadaevv.site/#person"
},
"inLanguage": "ru",
"creator": {
"@id": "https://mamadaevv.site/#person"
},
"datePublished": "2026-09-29",
"about": "UX/UI-дизайн и проектирование интерфейсов"
},
{
"@type": "BreadcrumbList",
"@id": "https://mamadaevv.site/cases/card-columns-obsidian/#breadcrumbs",
"itemListElement": [
{
"@type": "ListItem",
"position": 1,
"name": "Николай Мамадаев — продуктовый дизайнер",
"item": "https://mamadaevv.site/"
},
{
"@type": "ListItem",
"position": 2,
"name": "Кейсы",
"item": "https://mamadaevv.site/cases/"
},
{
"@type": "ListItem",
"position": 3,
"name": "Card Columns для Obsidian Bases: кейс о канбан-виде",
"item": "https://mamadaevv.site/cases/card-columns-obsidian/"
}
]
}
]
}
</script>
<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-block {
margin-top: 3rem;
padding-top: 2.5rem;
border-top: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
}
.case-block#kartochki,
.case-block#rezultat {
border-top: 0;
}
.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">
<svg class="liquid-glass-defs" aria-hidden="true" width="0" height="0" focusable="false" style="position:absolute;width:0;height:0;overflow:hidden;pointer-events:none">
<filter id="liquid-glass-displacement" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<feImage href="../../assets/liquid-glass/displacement.png" xlink:href="../../assets/liquid-glass/displacement.png" preserveAspectRatio="none" result="lgMap"/>
<feDisplacementMap in="SourceGraphic" in2="lgMap" scale="14" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<filter id="liquid-glass-displacement-btn" x="-20%" y="-20%" width="140%" height="140%" color-interpolation-filters="sRGB">
<feImage href="../../assets/liquid-glass/displacement.png" xlink:href="../../assets/liquid-glass/displacement.png" preserveAspectRatio="none" result="lgMapBtn"/>
<feDisplacementMap in="SourceGraphic" in2="lgMapBtn" scale="6" xChannelSelector="R" yChannelSelector="G"/>
</filter>
</svg>
<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 />
быстрый отбор тегов AND/OR
</p>
<figure class="case-media case-media--wide">
<img src="../../assets/card-columns-obsidian/overview.webp" alt="База ссылок в Obsidian, сгруппированная по тегам в колонки Card Columns" width="2076" height="1288" loading="eager" decoding="async" data-image-viewer />
<figcaption>Links: 196 результатов в режиме Columns</figcaption>
</figure>
<p class="section__prose">
Я разработал Card Columns, дополнительный вид карточек для Obsidian Bases. Штатный Cards показывал
заметки сеткой, а мне был нужен вид с колонками. В готовых плагинах я не нашёл подходящего сценария,
поэтому сделал свой. Плагин опубликован в&nbsp;<a href="https://community.obsidian.md/plugins/card-columns" target="_blank" rel="noopener noreferrer">каталоге Obsidian Community</a>.
Код открыт по лицензии MIT.
</p>
<div class="case-block" id="kontekst">
<h2>Ещё один вид карточек для Bases</h2>
<p class="section__prose">
В Obsidian Bases я настраиваю запрос через интерфейс и выбираю, как показать результат: таблицей,
списком или карточками. Card Columns добавляет ещё один карточный вид: раскладывает заметки по
колонкам на основе выбранного свойства.
</p>
<p class="section__prose">
Сортировку, фильтры, поиск и набор свойств задаю в штатном тулбаре Bases. Columns использует
эту выборку и меняет способ её отображения. При просмотре заметки остаются без изменений.
Перетаскивание карточки обновляет свойство, по которому построены колонки.
</p>
</div>
<div class="case-block" id="filtry">
<h2>Сначала сгруппировать, потом отобрать</h2>
<p class="section__prose">
В тулбаре Bases выбираю Group by: тег, статус, категорию или другое свойство.
Если группу не задать, Columns группирует по тегам. Меняю Group by, и колонки перестраиваются.
</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>
Мне нужна была одна карточка сразу в нескольких столбцах. Например, ссылка с тегами «Инструменты»
и «AI» появляется в обеих колонках. В базе фильмов одна и та же карточка может попасть в «Комедии»
и «Семейные». Готовые плагины, которые я пробовал, такого сценария не решали. Поэтому я сделал Columns.
Так заметку не нужно дублировать: она видна в каждой подходящей группе.
</p>
</div>
</aside>
<p class="section__prose">
Пилюли тегов стоят под тулбаром, рядом с доской. Обычный клик оставляет один тег, а&nbsp;следующий
добавляет ещё один по OR. Ctrl+клик или правый клик включает и выключает отдельный тег, не
сбрасывая остальные. В режиме OR достаточно совпадения с&nbsp;одним выбранным тегом. AND оставляет
заметки, где есть все выбранные теги. All сбрасывает отбор.
</p>
<p class="section__prose">
Фильтр по тегам уточняет уже полученную выборку. Штатный Filter задаёт основные условия через тулбар,
а пилюли позволяют быстро выбрать нужную комбинацию, не уходя с доски.
</p>
<figure class="case-media case-media--wide">
<div class="case-video-shell">
<video controls preload="metadata" playsinline poster="../../assets/card-columns-obsidian/filter-use.webp" aria-label="Демонстрация фильтрации тегов в Card Columns" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
<source src="../../assets/card-columns-obsidian/filter-use.webm" type="video/webm" />
Ваш браузер не поддерживает видео.
</video>
</div>
<figcaption>Фильтрация тегов: OR и AND</figcaption>
</figure>
</div>
<div class="case-block" id="kartochki">
<h2>Карточка показывает нужные поля</h2>
<p class="section__prose">
Какие поля попадут на карточку, выбираю в штатном списке Properties. Первое свойство становится
заголовком, остальные показываются чипами: текст, теги, ссылки, булевы значения и&nbsp;даты.
Grid собирает свойства с короткими значениями в компактную сетку, Stack располагает их друг
под другим. Для длинных значений можно включить перенос, а даты показывать в&nbsp;нужном формате.
Так карточка показывает выбранные для конкретной базы поля, не меняя сами заметки.
</p>
<p class="section__prose">
Если для базы нужны обложки, Columns возьмёт первую картинку из заметки или значение указанного
свойства frontmatter. В настройках задаю пропорции (Auto, 1:1, 3:2, 4:3 или 16:9), способ вписывания
и место на карточке.
Карточку можно открыть в&nbsp;активной панели, модальном окне, новой вкладке или сплите.
В&nbsp;сплите обычный клик открывает заметку в&nbsp;панели рядом с доской, а&nbsp;Ctrl+клик добавляет вкладку
в&nbsp;эту панель. Доска остаётся на месте, и&nbsp;контекст выборки не теряется.
</p>
<h3>Интерфейс в стиле Obsidian</h3>
<p class="section__prose">
Интерфейс Columns я проектировал и собирал прямо в коде, без Figma и других редакторов.
Ориентировался на дизайн-систему Obsidian, чтобы вид воспринимался частью приложения.
</p>
</div>
<div class="case-block" id="reshenie">
<h2>Перетаскивание меняет свойство заметки</h2>
<p class="section__prose">
В версии 0.9.0 карточки можно перемещать между колонками. При переносе Columns через API
Obsidian <code>processFrontMatter()</code> меняет frontmatter-свойство, по которому построена
группировка. На статусной доске перенос меняет статус заметки. Если свойство хранит список,
плагин заменяет перенесённое значение и сохраняет остальные значения.
</p>
<p class="section__prose">
Перетаскивание включено по умолчанию, но в General его можно отключить. Тогда Columns останется
видом для обзора, без правок свойств перетаскиванием.
</p>
<h3>Настройки сгруппированы по типу</h3>
<p class="section__prose">
Разделы General, Title, Properties и Cover собраны по типу параметров, чтобы нужное было проще найти.
В General задаю ширину карточек, число колонок в группе (от 1 до 6) и их порядок. Можно оставить
автоматический порядок или задать его вручную; пустые колонки при этом остаются на месте. Например,
колонка Done не исчезнет, даже если в ней пока нет карточек.
</p>
<p class="section__prose">
Сетка раскладывает карточки рядом, masonry заполняет пустоты по вертикали, а зебра помогает
следить за колонками на широком экране. Для заголовков, чипов, дат и обложек есть отдельные
параметры. Так один и тот же вид подходит разным базам.
</p>
<figure class="case-media case-media--wide">
<div class="case-video-shell">
<video controls preload="metadata" playsinline poster="../../assets/card-columns-obsidian/view-configure.webp" aria-label="Демонстрация настроек вида Columns" data-case-video-player style="display: block; width: 100%; height: auto; margin: 0">
<source src="../../assets/card-columns-obsidian/view-configure.webm" type="video/webm" />
Ваш браузер не поддерживает видео.
</video>
</div>
<figcaption>Настройки вида Columns</figcaption>
</figure>
</div>
<div class="case-block" id="publikaciya">
<h2>От разработки к публикации</h2>
<p class="section__prose">
Разработку веду с помощью Hermes Agent. Исходный код хранится на VPS агента, а собранная версия
синхронизируется на мой ноутбук. Там я проверяю плагин в своём vault.
</p>
<p class="section__prose">
Если всё работает, агент через настроенные скрипты отправляет изменения в Gitea и GitHub.
Публикацию новой версии в каталоге Obsidian Community я запускаю сам.
</p>
</div>
<div class="case-block" id="rezultat">
<h2>Результат: доска внутри Obsidian Bases</h2>
<p class="section__prose">
Card Columns оставляет заметки в Bases, но меняет способ их обзора: я задаю группировку,
отбираю теги и открываю карточки рядом с доской. Когда нужно сменить статус, перетаскивание
обновляет свойство, по которому построены колонки.
</p>
<p class="section__prose">
К концу сентября 2026 года плагин набрал 846 органических загрузок в каталоге Obsidian Community.
Там указана версия 0.9.0. На <a href="https://github.com/mamadaevv/card-columns" target="_blank" rel="noopener noreferrer">GitHub</a>
у Card Columns 4 звезды и два обращения: сообщение об ошибке закрыто, а запрос на выбор свойства
обложки пока открыт.
Некоторые предложенные доработки уже вошли в релизы. Плагин работает с Obsidian 1.8.0 и новее
на компьютерах и мобильных устройствах, лицензия MIT.
</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>
Для меня главный результат в том, что карточки стало удобнее представлять и просматривать.
Я выбираю нужные поля и группировку, а одна заметка может появиться в нескольких подходящих
колонках. Перетаскивание отдельно меняет свойство, и его можно отключить.
</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-portfolio">
<a
class="mp-card mp-card--link mp-card--compact"
href="../site-portfolio/"
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="en" href="/en/">English</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 l = links[i].getAttribute("data-lang");
links[i].setAttribute("href", (l === "en" ? "/en/" : "/") + base);
}
})();
</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>