817 lines
43 KiB
HTML
817 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" 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="Case Study: Card Columns, a view plugin for Obsidian Bases. It displays notes as cards arranged in columns, with tag filtering using AND/OR."
|
||
/>
|
||
<title>Card Columns, a plugin for Obsidian Bases: a case study</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">Skip to case content</a>
|
||
<header class="header" data-theme-picker>
|
||
<div class="header__inner">
|
||
<a class="logo" href="../../" aria-label="Home">NM</a>
|
||
<div class="header__cluster">
|
||
<nav class="nav" aria-label="Page sections">
|
||
<a href="#contact">Contact</a>
|
||
</nav>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="fab-stack" aria-label="Quick actions">
|
||
<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="Style settings">
|
||
<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="Theme & appearance" 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">Accent color</h3>
|
||
<div class="colorpicker" data-colorpicker>
|
||
<div class="colorpicker__plane-box">
|
||
<canvas class="colorpicker__plane" width="320" height="180" data-sl-canvas aria-label="Accent saturation and lightness"></canvas>
|
||
<div class="colorpicker__knob" data-sl-knob></div>
|
||
</div>
|
||
<div class="colorpicker__hue-wrap">
|
||
<label class="colorpicker__hue-sr" for="theme-hue-slider">Accent hue</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">Theme</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="System"><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="Light"><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="Dark"><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">Corner radius</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="Soft radius"><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="Medium radius"><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="Strong radius"><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">Typography</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">Page background</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="Show background gradient"><span class="theme-mesh-bg-seg__label">On</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="Hide background gradient"><span class="theme-mesh-bg-seg__label">Off</span></button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="fab fab--primary" href="#contact" aria-label="Contact">
|
||
<span class="fab-primary__ico" aria-hidden="true"></span>
|
||
</a>
|
||
</div>
|
||
<div class="content-edit-fab-stack" aria-label="Content Edits" 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="Content Editing Mode"
|
||
title="Edit Mode"
|
||
>
|
||
<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">
|
||
A Kanban view for Obsidian Bases<br />
|
||
that leaves your notes untouched
|
||
</h1>
|
||
<p class="case-page__sub">
|
||
Card Columns: Notes as cards in columns,<br />
|
||
filtered by AND/OR tags with just two clicks
|
||
</p>
|
||
|
||
<!-- МЕДИА: hero-спойлер, финальный экран плагина. Раскомментировать при вставке скрина.
|
||
<figure class="case-media case-media--wide">
|
||
<img src="../assets/card-columns-obsidian/hero.webp" alt="Card Columns: card columns on top of the .base file" width="1920" height="1080" loading="eager" decoding="async" data-image-viewer />
|
||
<figcaption>"Columns" view on top of the note database</figcaption>
|
||
</figure>
|
||
-->
|
||
|
||
<ul class="case-brief">
|
||
<li>Role: Product Design and Development</li>
|
||
<li><span>Platform:</span> Obsidian Bases</li>
|
||
<li><span>Status</span>: Working plugin, open source (MIT)</li>
|
||
<li><span>Team: Solo</span>, side project</li>
|
||
<li><span>Status:</span> Draft response, to be replaced</li>
|
||
</ul>
|
||
|
||
<div class="case-todo">
|
||
<span class="case-todo__label">Question for the author</span>
|
||
<p class="case-todo__q">
|
||
How long did it take from the initial idea to a working version? Weeks? A month of weekends?
|
||
</p>
|
||
<p>
|
||
Draft answer: I put it together in three weeks in the evenings, then spent another month refining it while
|
||
working on my own projects. Replace with the actual time.
|
||
</p>
|
||
</div>
|
||
|
||
<p class="section__prose">
|
||
Obsidian now has Bases, a separate file type that displays notes not as text, but
|
||
in various formats: tables, cards, and trees. The standard card view didn’t work for me: it’s
|
||
flat, with no grouping, and it’s hard to make sense of when there are hundreds of notes. I wanted to see them
|
||
arranged in columns, like a Kanban board, but without the extra overhead. That’s how the Card Columns view plugin came about.
|
||
</p>
|
||
|
||
<div class="case-block" id="kontekst">
|
||
<h2>Context</h2>
|
||
<p class="section__prose">
|
||
A “Bases” is neither a note nor a separate database. It’s a query to the vault: you select
|
||
notes using a filter, and the framework displays them in the chosen view. Obsidian itself has
|
||
a built-in card view, but it’s one-dimensional: no grouping by properties, no
|
||
proper filtering. I decided to create my own view that integrates into the
|
||
Bases toggle alongside the default ones.
|
||
</p>
|
||
<p class="section__prose">
|
||
The main limitation was in the framework itself: the plugin doesn’t store data, but rather renders what
|
||
the Bases query returns (Sort, Filter, Properties, Search from the toolbar). This means that all the logic
|
||
for data retrieval remains with Bases, and my task is solely to display and navigate the results.
|
||
</p>
|
||
<!-- МЕДИА: общий скрин/видео обзора — доска колонок поверх .base файла.
|
||
<figure class="case-media case-media--wide">
|
||
<img src="../assets/card-columns-obsidian/overview.webp" alt="View of "Card Columns" in the .base file: notes on the columns" width="1920" height="1080" loading="lazy" decoding="async" data-image-viewer />
|
||
<figcaption>Species Overview</figcaption>
|
||
</figure>
|
||
-->
|
||
</div>
|
||
|
||
<div class="case-block" id="pochemu-svoi">
|
||
<h2>Why not use a ready-made solution?</h2>
|
||
<p class="section__prose">
|
||
Before writing any code, I checked what was already available. Three options, none of them worked:
|
||
</p>
|
||
<p class="section__prose">
|
||
<strong>The default Bases card view.</strong>
|
||
It displays a continuous stream of cards without grouping.
|
||
When there are over a hundred notes, it becomes a feed where you have to scroll and squint at the titles.
|
||
I ruled that out first.
|
||
</p>
|
||
<p class="section__prose">
|
||
<strong>Kanban plugins from the community.</strong>
|
||
Most come with their own storage system:
|
||
the board exists separately from the vault, and notes are duplicated. This results in a second database
|
||
that needs to be synchronized. I didn’t need that.
|
||
</p>
|
||
<p class="section__prose">
|
||
<strong>The neighboring bases-kanban.</strong>
|
||
The closest in spirit: it hooks into Bases to create
|
||
a Kanban board. But it writes to the notes: dragging a card changes the frontmatter. For a view plugin,
|
||
this is a risk, which I’ll discuss below.
|
||
</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>
|
||
The decision to forgo drag-and-drop isn’t a shortcoming—it’s a design choice. A view plugin should not
|
||
write to user data. This decision shaped everything else.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<div class="case-todo">
|
||
<span class="case-todo__label">Question for the author</span>
|
||
<p class="case-todo__q">
|
||
Did you actually install and test other people’s plugins, or did you just go by the descriptions? Which
|
||
ones did you look at specifically?
|
||
</p>
|
||
<p>
|
||
Preliminary answer: I installed three of them, ran them for a week on my database, then uninstalled them. I looked at the source code for bases-kanban
|
||
but didn’t install it. Replace with actual information; include the plugin names.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="case-block" id="zadacha">
|
||
<h2>Task</h2>
|
||
<p class="section__prose">
|
||
Design a view for Bases that displays notes as cards arranged in columns and allows you to
|
||
quickly select the ones you need. Strict constraint: the plugin does not write anything to the notes and does not store
|
||
its own state, and sorting, filtering, properties, and search from the standard Bases toolbar must
|
||
work as before.
|
||
</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>
|
||
The main risk is creating a view that conflicts with the standard Bases toolbar. If that happens,
|
||
the user loses the sorting and search capabilities—the very features that make Bases worth using. This could not
|
||
be allowed to happen.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<div class="case-block" id="reshenie">
|
||
<h2>Solutions</h2>
|
||
<p class="section__prose">
|
||
The plugin adds a “Columns” view to Bases. Cards are built from the results of a Bases query;
|
||
grouping is defined by a property, and filtering is done via clickable tags. Next, there are five options
|
||
that determine the result. Each one includes something I decided against.
|
||
</p>
|
||
<!-- МЕДИА: скриншот вида с колонками и карточками внутри.
|
||
<figure class="case-media case-media--wide">
|
||
<img src="../assets/card-columns-obsidian/board.webp" alt="Columns of cards grouped by property" width="1920" height="1080" loading="lazy" decoding="async" data-image-viewer />
|
||
<figcaption>View with columns</figcaption>
|
||
</figure>
|
||
-->
|
||
|
||
<h3>Grouping by any property</h3>
|
||
<p class="section__prose">
|
||
Columns are generated using the “Group by” option in the Bases toolbar, by tag, status, category, or any
|
||
frontmatter property. Change the property in the query, and the board reloads automatically. This way, a single
|
||
database covers different perspectives: projects by status, notes by tags, tasks by
|
||
priority.
|
||
</p>
|
||
|
||
<h3>Tag filter by AND and OR</h3>
|
||
<p class="section__prose">
|
||
This is exactly what the plugin was designed for. Below the toolbar, there are tag icons; when clicked, they
|
||
filter cards across all columns at once:
|
||
</p>
|
||
<ul class="section__prose">
|
||
<li>Left-click on a tag to show only that tag (resets the other filters).</li>
|
||
<li>Left-click on the next tag to show notes with ANY of the selected tags (OR mode).</li>
|
||
<li>Ctrl+Click or right-click to switch tags without resetting the selection.</li>
|
||
<li>The OR/AND button: toggle between “any of” and “all of” for multiple tags.</li>
|
||
<li>All: Clear all filters.</li>
|
||
</ul>
|
||
<p class="section__prose">
|
||
The combination of “OR—add with a left-click + exact toggle via Ctrl” turned out to be intuitive:
|
||
quick selection doesn’t disrupt what’s already selected. I immediately rejected the alternative—moving the filter to a modal window—
|
||
because it requires an extra click and loses the context of what’s already been filtered.
|
||
</p>
|
||
<p class="section__prose">
|
||
The choice here isn’t about the feature itself, but where it lives. I chose the pills below the toolbar instead of
|
||
a separate filter window because filtering by tags is an action performed at every step of working
|
||
with the database. A modal window turns it into an interrupted workflow: open, configure, close, check.
|
||
The pill-shaped icons keep the result right in front of you, and the filter status is always visible.
|
||
</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" />
|
||
Your browser does not support video.
|
||
</video>
|
||
</div>
|
||
<figcaption>Filtering by tags: OR and AND</figcaption>
|
||
</figure>
|
||
-->
|
||
|
||
<h3>Cards and Covers</h3>
|
||
<p class="section__prose">
|
||
Properties such as chips, text, tags, links, Booleans, and dates are displayed for each note. You can
|
||
show a cover: from the first image in the note or from a separate property. You have
|
||
full control over the cover, including aspect ratio (Auto, 1:1, 3:2, 4:3, 16:9), border, fit
|
||
(cover/contain), orientation, and position on the card (above the title, below it, at the end).
|
||
</p>
|
||
|
||
<h3>Solution: Settings have been moved out of the code</h3>
|
||
<p class="section__prose">
|
||
At first, the parameters were hardcoded for my database. This was a mistake: the view would stop working
|
||
for someone else’s scenario. I moved everything to the gear menu, dividing it into four sections so that the view
|
||
could adapt to different databases, rather than just my single use case. More than 15 parameters.
|
||
</p>
|
||
<details class="case-details">
|
||
<summary>Show the full list of settings</summary>
|
||
<p class="section__prose"><strong>General</strong></p>
|
||
<ul class="section__prose">
|
||
<li>Where to open the card: active panel, pop-up window, new tab, split to the right / down.</li>
|
||
<li>Card width (px).</li>
|
||
<li>Columns per group (1–6), grid within a column.</li>
|
||
<li>Zebra columns (alternating background colors for easy scanning).</li>
|
||
<li>Masonry layout (cards fill vertical gaps).</li>
|
||
</ul>
|
||
<p class="section__prose"><strong>Title</strong></p>
|
||
<ul class="section__prose">
|
||
<li>Title wrapping, bold title, font size (px).</li>
|
||
</ul>
|
||
<p class="section__prose"><strong>Properties</strong></p>
|
||
<ul class="section__prose">
|
||
<li>Chip layout: stack (vertical) or grid (compact in a row).</li>
|
||
<li>Wrap multi-line values, font size.</li>
|
||
<li>Date and date-time formats + locale (Moment.js).</li>
|
||
</ul>
|
||
<p class="section__prose"><strong>Cover</strong></p>
|
||
<ul class="section__prose">
|
||
<li>Source: none / first image in the post / cover property.</li>
|
||
<li>Style (borderless / with border), aspect ratio, orientation, fit, position on the card.</li>
|
||
</ul>
|
||
</details>
|
||
|
||
<div class="case-todo">
|
||
<span class="case-todo__label">Question for the author</span>
|
||
<p class="case-todo__q">
|
||
Is it true about the hard-coded parameters? If not, replace it with the real reason for moving them
|
||
to the settings: user requests, multiple databases with different structures, whatever.
|
||
</p>
|
||
<p>
|
||
Preliminary answer: Yes, the first version was tailored to my database; the settings were added when
|
||
I needed to make the plugin available externally. I’ll check and fix it.
|
||
</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>
|
||
I implemented the split preview just like in Obsidian itself: left-click reuses the panel, Ctrl+click opens
|
||
a new tab within the split. Without this, on a wide screen, it’s inconvenient to
|
||
view cards next to the board.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<h3>Works within the standard toolbar</h3>
|
||
<p class="section__prose">
|
||
The view hooks into the BasesView framework, so sorting, filtering, properties, and search from
|
||
the Bases toolbar remain functional. The plugin only renders the query results; the data is not
|
||
duplicated and does not depend on a separate storage.
|
||
</p>
|
||
<!-- МЕДИА: скриншот настроек (gear-меню) с секциями.
|
||
<figure class="case-media case-media--content">
|
||
<img src="../assets/card-columns-obsidian/settings.webp" alt="Plugin Settings Window: General, Title, Properties, Cover" width="1200" height="675" loading="lazy" decoding="async" data-image-viewer />
|
||
<figcaption>View Settings</figcaption>
|
||
</figure>
|
||
-->
|
||
</div>
|
||
|
||
<div class="case-block" id="rezultat">
|
||
<h2>The Result and How I Tested It</h2>
|
||
<p class="section__prose">
|
||
This view solved my problem: a Bases query now reads like a board, rather than a flat stream
|
||
of cards. Grouping by property + filtering by AND/OR tags—exactly what the default
|
||
view was missing. Everything runs on the Bases query, so moving the vault and syncing don’t
|
||
break anything.
|
||
</p>
|
||
|
||
<h3>Metrics: number of steps to filter</h3>
|
||
<p class="section__prose">
|
||
The main task I used to measure success: filter notes by two tags and find the right
|
||
card. In the standard card view, this involves scrolling through the feed and scanning the titles with your eyes. In
|
||
Columns, two clicks on the pills and the column is right in front of you.
|
||
</p>
|
||
|
||
<div class="case-todo">
|
||
<span class="case-todo__label">Question for the author</span>
|
||
<p class="case-todo__q">
|
||
Can you run this test for real? Time 10 repetitions of the task: the default
|
||
view versus Columns. Or count the exact number of steps.
|
||
</p>
|
||
<p>
|
||
Preliminary answer: on a sample of 25 notes, filtering by two tags took about 40 seconds and
|
||
required scrolling through the feed; in Columns, two clicks, about 5 seconds. These are rough estimates—
|
||
replace them with actual measurements.
|
||
</p>
|
||
</div>
|
||
|
||
<h3>Test: Long-term usability in daily work</h3>
|
||
<p class="section__prose">
|
||
The second criterion is more subjective, but for a tool tailored to my needs, it’s more important: will I still be
|
||
using the plugin a month from now? I am—it’s become an integral part of my work with the database and doesn’t require
|
||
me to remember how to use it.
|
||
</p>
|
||
|
||
<div class="case-todo">
|
||
<span class="case-todo__label">Question for the author</span>
|
||
<p class="case-todo__q">
|
||
Are there any external indicators: installs, GitHub stars, reviews, or questions in the issue tracker? Any
|
||
number confirming that you’re not the only one using the plugin.
|
||
</p>
|
||
<p>
|
||
Preliminary answer: The plugin currently needs to be installed manually; it hasn’t been approved for the community repository yet, so
|
||
there are only a handful of installs and no stars so far. If there’s actual data, include it here—
|
||
it’s more convincing than any self-report.
|
||
</p>
|
||
</div>
|
||
<p class="section__prose">
|
||
What I’d do differently. I deliberately chose not to implement drag-and-drop of cards between columns,
|
||
since that changes a property in the note, and for a view plugin, that poses a separate data integrity risk.
|
||
But I see a demand for this among users, and I might add it as an optional mode. For
|
||
now, since the plugin is maintained manually (it hasn’t been reviewed by the community directory yet), this is the only thing
|
||
preventing users from using it right away.
|
||
</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>
|
||
Main conclusion: a view for Bases only works when it respects
|
||
the framework’s toolbar. I added a view, not a second storage system, and the notes
|
||
remained regular notes.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<section class="section section--surface" data-reveal>
|
||
<div class="section__inner">
|
||
<h2 class="section__title">See also</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 — Open a Case"
|
||
>
|
||
<div class="mp-card__body">
|
||
<h3 class="mp-card__title">AI Assistant</h3>
|
||
<ul class="mp-card__tags" aria-label="Project Type">
|
||
<li class="mp-card__tag">Figma plugin</li>
|
||
</ul>
|
||
<p class="mp-card__desc">A Figma plugin with AI features.</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 — Open a Case"
|
||
>
|
||
<div class="mp-card__body">
|
||
<h3 class="mp-card__title">Card Columns</h3>
|
||
<ul class="mp-card__tags" aria-label="Project Type">
|
||
<li class="mp-card__tag">Obsidian Plugin</li>
|
||
</ul>
|
||
<p class="mp-card__desc">A plugin for displaying cards in columns in 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="This website — Open a Case"
|
||
>
|
||
<div class="mp-card__body">
|
||
<h3 class="mp-card__title">This website</h3>
|
||
<ul class="mp-card__tags" aria-label="Project Type">
|
||
<li class="mp-card__tag">Development with AI IDE</li>
|
||
</ul>
|
||
<p class="mp-card__desc">Website development using 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">Contact</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">I'll reply today</span></span></a></li>
|
||
<li><a class="contact-card contact-card--link contact-card--brand-whatsapp" href="https://wa.me/qr/E2AWW5ZHE3ANJ1" 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/whatsapp.svg" alt="" width="24" height="24" loading="eager" decoding="async" /></span><span class="contact-card__body"><span class="contact-card__name">WhatsApp</span><span class="contact-card__hint">Stable connection</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="Copy address" aria-label="Copy email address to clipboard"><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">Email</span><span class="contact-card__hint" data-contact-copy-hint>Copy address</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">Website design and development — Nikolay Mamadaev</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 policy</a></span> ·<span class="footer__chunk footer__chunk--lang"><span class="footer__sep" aria-hidden="true">
|
||
</span><nav class="lang-switch" aria-label="Choose language" data-lang-switch>
|
||
<a class="lang-switch__btn" data-lang="ru" href="/">Русский</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">
|
||
This site uses cookies to ensure a smooth user experience. For more information, see the
|
||
<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>Accept</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>
|