Updated Readme + bug fixes

This commit is contained in:
Kazi Aidah Haque 2026-03-22 00:58:01 +06:00
parent 093e3dfb0a
commit 6fc61d0827
21 changed files with 1196 additions and 187 deletions

171
README.md
View file

@ -1,92 +1,135 @@
### Currently a mess, might mess up your vault.
### Please do not try until this note has been removed.
I am actively trying to fix all the issues. Thank you.
![SideCards Downloads](https://img.shields.io/badge/dynamic/json?logo=obsidian&style=for-the-badge&label=downloads&labelColor=26233a&color=483699&query=%24%5B%22sidecards%22%5D.downloads&url=https%3A%2F%2Fraw.githubusercontent.com%2Fobsidianmd%2Fobsidian-releases%2Fmaster%2Fcommunity-plugin-stats.json) ![Stars](https://img.shields.io/github/stars/Kazi-Aidah/sidecards?style=for-the-badge&color=c4a7e7&labelColor=26233a) ![Version](https://img.shields.io/github/manifest-json/v/Kazi-Aidah/sidecards?style=for-the-badge&color=9ccfd8&labelColor=26233a) ![Last update](https://img.shields.io/github/last-commit/Kazi-Aidah/sidecards?style=for-the-badge&color=9fc387&labelColor=26233a)
# SideCards
Quickly create cards in your Obsidian sidebar; color, tag, and drag them straight into your notes.
<img alt="SideCards Banner" src="assets/SideCards-banner.png" />
---
# SideCards
## Create & Manage Cards Instantly
Quickly create cards in your Obsidian Sidebar;
color, tag, and drag them straight into your notes.
### Quick Card Creation
Type directly into the sidebar input box to create cards instantly. Cards can also be created from the homepage or via the quick-add command.
<img alt="SideCardsBanner" src="https://github.com/user-attachments/assets/503eae84-0d7d-4385-9253-7b48d7bef43e" />
### Inline Editing
Edit card content directly: no modals, no interruptions.
<i> Theme: [Underwater](https://github.com/Seniblue/Underwater) </i>
![Inline editing](assets/inline-editing.png)
## Features
### Drag to Editor
Drag any card into your markdown editor to insert its content.
### **Core Card System:**
![Drag into editor](assets/drag-into-editor.gif)
Quick Card Creation - Instant note-taking with a dedicated input box <img alt="image" src="https://github.com/user-attachments/assets/8a4c9ca1-e84e-40f9-8f68-21dd68a590ed" />
### Essentials
- Real-time Auto-save: Changes are saved automatically as you type.
Visual Card Interface - Clean, card-based layout for easy scanning
Inline Editing - Edit card content directly without opening modals <img alt="image" src="https://github.com/user-attachments/assets/9b9e85c6-97a0-4096-9cb7-caf2cb762bea" />
- Note Conversion: Convert any card to a full markdown note with one click.
Drag to Editor - Drag cards into any markdown editor to insert content <img alt="image" src="https://github.com/user-attachments/assets/d9058648-9e47-4f7a-900f-b2f5066756e6" />
- Bidirectional Sync: Changes made in notes automatically update the cards and vice versa.
* Real-time Auto-save - Changes are saved automatically as you type.
### **Visual Customization:**
10 Customizable Colors - Color-code your cards for visual organization <img alt="image" src="https://github.com/user-attachments/assets/323c47b3-d623-4191-a073-8782f6a38e85" />
Multiple Card Styles - Choose from 3 distinct visual styles and adjust border radius, opacity, and thickness. <img alt="image" src="https://github.com/user-attachments/assets/3c8a5c77-89e9-4e6d-a434-17bce57c01e9" />
Timestamp Display - Optional creation timestamps with customizable formatting <img alt="image" src="https://github.com/user-attachments/assets/0fcde655-d688-4c84-84b0-83622ff1fecf" />
Clean UI Options - Hide scrollbars, filters and other buttons for a minimalist look <img alt="image" src="https://github.com/user-attachments/assets/dc962adc-d1c0-4ce6-8e62-b20f24223fad" />
## Integration with Your Vault
Seamless Note Conversion - Convert cards to full markdown notes with one click <img alt="image" src="https://github.com/user-attachments/assets/896cba69-7c47-4fd7-b8b6-93b2644ce187" />
Bidirectional Sync - Changes in notes automatically reflect in cards
- Folder Integration: Auto-import markdown files from a specified storage folder.
Folder Integration - Auto-import markdown files from specified folders
<img alt="image" src="https://github.com/user-attachments/assets/f53035ca-5ab6-4fe5-a619-71cfba0f483c" />
## Style Your Cards
Frontmatter Support - Comprehensive metadata storage in YAML frontmatter <img alt="image" src="https://github.com/user-attachments/assets/dcf31698-ef5e-4165-b439-a5ffa95b1b12" />
### 10 Custom Colors
Color-code your cards for visual organization. Enable two row color swatches in two rows for a more compact layout.
## Advanced Features
- Custom Keyboard Shortcuts - Configure your preferred keys for saving and line breaks
- Context Menu Actions - Rich right-click menu for card operations
- Archive System - Archive cards to declutter without deleting
- Auto-open Option - Automatically open sidebar when Obsidian starts
- Bulk Operations - Reload all cards or apply sorting across entire collection
- Custom Filter Creation - Build your own filters beyond the standard daily/weekly/monthly views
<img alt="image" src="https://github.com/user-attachments/assets/d56fa01e-dd9d-45c0-a2b0-cb3d423a2310" /> <img width="752" height="439" alt="image" src="https://github.com/user-attachments/assets/1eb02b3e-800f-4578-a41b-71a21ec1affc" />
![](assets/two-row-color-dots.png)
### Auto-color Rules
Automatically assign colors based on text content or tag matches.
![Auto-color settings](assets/settings-autocolor.png)
- Custom Color Naming - If card-color frontmatter is absent, card-color-name will work instead.
<img alt="card color name" src="https://github.com/user-attachments/assets/a291de80-79c1-4932-a35d-ff08fa9e0fef" />
### Multiple Card Styles
Choose from 3 visual styles and adjust border radius, opacity, and border thickness to customize it to your liking!
![Card styling settings](assets/settings-card-styling.png)
# Installation
It is currently not available in the Obsidian Community Plugins or BRAT.
You will have to clone this repo and put it in your `.obsidian/plugins` folder.
## Categories
By default, SideCards includes **Today**, **Tomorrow**, and **Archived** categories.
- **Today** shows cards created or modified today.
- **Tomorrow** is for cards you want to postpone as theyll automatically move to Today when the day changes.
- **Archived** stores all archived cards.
### Custom Categories
Build your own categories with custom labels, colors, and icons.
![Categories settings](assets/settings-categories.png)
Icons currently appear only in the context menu and category list, not on the button itself.
## Card Status System
Assign statuses with custom names, colors, and text colors.
![Status menu](assets/status-menu.png)
### Color Inheritance
Optionally inherit the status color into the card background.
## Expiry
Set expiry times with optional auto-archive and a time-left pill display.
![](assets/card-expiry.png)
## Homepage
### Dedicated Homepage View
A full-page view with a card editor, pinned notes, and recent notes.
![Banner](assets/SideCards-banner.png)
You can replace your default new tab with this homepage.
![Pinned and recent notes](assets/pinned-recent-notes.png)
Notes can be pinned by right-clicking a tab and selecting “Pin tab to SideCards Homepage,” or by using a command. Icons are supported via the [Iconic plugin](https://github.com/gfxholo/iconic/).
## Other Features
### Auto-pair Brackets
Automatically pairs `(`, `[`, `{`, `` ` ``, `=`, `%`, and `"` while typing.
### Formatting Shortcuts
Bold, italic, highlight, and comment wrapping work inside card editors.
## Installation
*SideCards is not yet available in Obsidian Community Plugins or BRAT.*
1. **Clone the repository** and place the plugin folder in your vaults `.obsidian/plugins/` directory. The folder structure should look like this:
```
sidecard
╰─ main.js
╰─ styles.css
╰─ manifest.json
sidecards
─ main.js
─ styles.css
─ manifest.json
```
into
Inside your vault, it should end up like:
```
.obsidian
╰─ plugins
╰─ sidecard
╰─ main.js
╰─ styles.css
╰─ manifest.json
.your-vault
└─ .obsidian
└─ plugins
└─ sidecards
├─ main.js
├─ styles.css
└─ manifest.json
```
2. **Enable the plugin** in Obsidian.
3. **Set a storage folder** on first launch. This is where your cards will be saved as notes.
### Questions or Suggestions?
Create a new issue [here](https://github.com/Kazi-Aidah/sidecards/issues) to report bugs or request features!

BIN
assets/SideCards-banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 434 KiB

BIN
assets/card-expiry.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
assets/drag-into-editor.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

BIN
assets/inline-editing.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
assets/status-menu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

578
main.js
View file

@ -386,8 +386,7 @@ function handleKeyWrap(event, editorEl, editor, enabled = true) {
"`": ["`", "`"],
"%": ["%", "%"],
"=": ["=", "="],
'"': ['"', '"'],
"'": ["'", "'"]
'"': ['"', '"']
};
const pair = wrapMap[key];
if (!pair)
@ -467,11 +466,12 @@ function getWordRangeAtCaret(selection) {
// src/views/components/Card.ts
var _CardComponent = class {
constructor(container, card, store, app, plugin) {
constructor(container, card, store, app, plugin, settingsOverride) {
this.container = container;
this.store = store;
this.app = app;
this.plugin = plugin;
this.settingsOverride = settingsOverride;
this.unsubscribe = [];
this.isEditing = false;
this.ignoreNextClick = false;
@ -648,9 +648,76 @@ var _CardComponent = class {
var _a;
const currentRender = ++this.renderCount;
this.stopExpiryTick();
if (this.isEditing) {
const existingContent = this.el.querySelector(".sc-content");
if (existingContent) {
existingContent.setAttribute("contenteditable", "true");
existingContent.textContent = this.card.content;
existingContent.addClass("is-editing");
setTimeout(() => {
existingContent.focus();
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(existingContent);
range.collapse(false);
sel == null ? void 0 : sel.removeAllRanges();
sel == null ? void 0 : sel.addRange(range);
}, 0);
existingContent.addEventListener("focusin", () => {
this.app.keymap.pushScope(this.scope);
this.app.workspace.activeEditor = this.owner;
});
existingContent.addEventListener("blur", () => {
this.app.keymap.popScope(this.scope);
if (this.app.workspace.activeEditor === this.owner) {
this.app.workspace.activeEditor = null;
}
if (this.isEditing) {
void (async () => {
const newContent = existingContent.textContent || "";
if (newContent !== this.card.content) {
await this.store.update(this.card.id, { content: newContent });
}
this.isEditing = false;
if (_CardComponent.activeEditor === this) {
_CardComponent.activeEditor = null;
}
void this.render();
})();
}
});
existingContent.addEventListener("keydown", (e) => {
var _a2;
if (handleKeyWrap(e, existingContent, this.editor, ((_a2 = this.plugin.settings) == null ? void 0 : _a2.autoPairBrackets) !== false)) {
e.preventDefault();
e.stopPropagation();
return;
}
if (this.applyFormattingHotkey(e, existingContent))
return;
const settings = this.store.settings;
const normalizeKey = (v) => String(v || "").toLowerCase().replace(/[\s+_]+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "");
const saveKey = normalizeKey(settings.saveKey || "enter");
let pressed = "";
if (e.ctrlKey)
pressed += "ctrl-";
if (e.shiftKey)
pressed += "shift-";
if (e.altKey)
pressed += "alt-";
if (e.key && e.key.toLowerCase() === "enter")
pressed += "enter";
if (pressed === saveKey) {
e.preventDefault();
existingContent.blur();
}
});
return;
}
}
this.el.empty();
this.el.dataset.id = this.card.id;
this.el.draggable = false;
this.el.draggable = true;
const statusDef = this.card.status ? (this.store.settings.cardStatuses || []).find((s) => s.name === this.card.status.name) : null;
const useStatusColor = this.store.settings.inheritStatusColor && statusDef;
const effectiveColor = useStatusColor && statusDef ? statusDef.colorIndex ? `var(--card-color-${statusDef.colorIndex})` : statusDef.color : this.card.color;
@ -714,6 +781,8 @@ var _CardComponent = class {
if (this.card.expiresAt && this.store.settings.showExpiryTimeLeft) {
const pill = container.createDiv("sc-expiry-pill");
pill.textContent = this.formatExpiryTimeLeft(this.card.expiresAt);
if (this.card.status)
pill.style.marginBottom = "4px";
}
if (this.card.status) {
const pill = container.createDiv("sc-status-pill");
@ -903,10 +972,14 @@ var _CardComponent = class {
}
}
renderFooter(container) {
var _a, _b, _c, _d, _e, _f;
const settings = this.store.settings;
const hasTags = this.card.tags && this.card.tags.length > 0;
if (settings.groupTags) {
if (settings.showTimestamps && settings.timestampBelowTags) {
const groupTags = (_b = (_a = this.settingsOverride) == null ? void 0 : _a.groupTags) != null ? _b : settings.groupTags;
const showTimestamps = (_d = (_c = this.settingsOverride) == null ? void 0 : _c.showTimestamps) != null ? _d : settings.showTimestamps;
const showTags = (_f = (_e = this.settingsOverride) == null ? void 0 : _e.showTags) != null ? _f : true;
const hasTags = showTags && this.card.tags && this.card.tags.length > 0;
if (groupTags) {
if (showTimestamps && settings.timestampBelowTags) {
const ts = container.createDiv("sc-timestamp sc-timestamp--block");
ts.textContent = this.formatTimestamp(this.card.created);
}
@ -922,17 +995,28 @@ var _CardComponent = class {
});
});
}
if (settings.showTimestamps && !settings.timestampBelowTags) {
if (showTimestamps && !settings.timestampBelowTags) {
const ts = container.createDiv(`sc-timestamp ${hasTags ? "sc-timestamp--inline-spaced" : "sc-timestamp--inline"}`);
ts.textContent = this.formatTimestamp(this.card.created);
}
} else {
if (settings.showTimestamps) {
if (showTimestamps) {
container.createDiv("sc-timestamp").textContent = this.formatTimestamp(this.card.created);
}
}
}
setupListeners() {
this.el.addEventListener("dragstart", (e) => {
if (!e.dataTransfer)
return;
e.dataTransfer.effectAllowed = "copyMove";
const payload = JSON.stringify({ id: this.card.id, content: this.card.content });
e.dataTransfer.setData("text/x-card-sidebar", payload);
try {
e.dataTransfer.setData("text/plain", this.card.content);
} catch (e2) {
}
});
this.el.addEventListener("click", (e) => {
if (this.ignoreNextClick) {
this.ignoreNextClick = false;
@ -1105,7 +1189,9 @@ var _CardComponent = class {
const unbind = this.store.eventBus.on("card:updated", (updated) => {
if (updated.id === this.card.id) {
this.card = updated;
void this.render();
if (!this.isEditing) {
void this.render();
}
}
});
this.unsubscribe.push(unbind);
@ -1679,6 +1765,31 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
c.style.transform = "";
});
}
let dragStartX = 0;
let dragStartY = 0;
let dragPending = false;
const DRAG_THRESHOLD = 5;
const onNativeDragStart = () => {
if (!dragPending && !active)
return;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
ghost == null ? void 0 : ghost.remove();
ghost = null;
if (draggedEl) {
draggedEl.style.opacity = "";
draggedEl.style.pointerEvents = "";
}
clearTransforms();
draggedEl = null;
active = false;
dragPending = false;
snapRects = /* @__PURE__ */ new Map();
lastFlipRects = /* @__PURE__ */ new Map();
originalOrder = [];
currentTargetIndex = -1;
draggedIndex = -1;
};
const onMouseDown = (e) => {
if (getSortMode() !== "manual")
return;
@ -1691,16 +1802,28 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
originalOrder = getCards();
draggedIndex = originalOrder.indexOf(card);
currentTargetIndex = draggedIndex;
dragStartX = e.clientX;
dragStartY = e.clientY;
dragPending = true;
snapRects = /* @__PURE__ */ new Map();
originalOrder.forEach((c) => snapRects.set(c, c.getBoundingClientRect()));
const rect = snapRects.get(card);
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
card.style.opacity = "0";
card.style.pointerEvents = "none";
ghost = card.cloneNode(true);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
};
function activateDrag(e) {
if (!draggedEl)
return;
dragPending = false;
active = true;
const rect = snapRects.get(draggedEl);
draggedEl.style.opacity = "0";
draggedEl.style.pointerEvents = "none";
ghost = draggedEl.cloneNode(true);
ghost.classList.add("sc-drag-ghost");
const cs = getComputedStyle(card);
const cs = getComputedStyle(draggedEl);
ghost.style.background = cs.background;
ghost.style.backgroundColor = cs.backgroundColor;
ghost.style.borderColor = cs.borderColor;
@ -1716,8 +1839,8 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
ghost.style.pointerEvents = "none";
ghost.style.width = rect.width + "px";
ghost.style.height = rect.height + "px";
ghost.style.left = rect.left + "px";
ghost.style.top = rect.top + "px";
ghost.style.left = e.clientX - offsetX + "px";
ghost.style.top = e.clientY - offsetY + "px";
ghost.style.margin = "0";
ghost.style.boxShadow = "0 8px 24px rgba(0,0,0,0.25)";
ghost.style.opacity = "1";
@ -1725,12 +1848,17 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
ghost.style.transform = "";
ghost.style.transition = "";
document.body.appendChild(ghost);
active = true;
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
e.preventDefault();
};
}
const onMouseMove = (e) => {
if (dragPending) {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
if (Math.hypot(dx, dy) >= DRAG_THRESHOLD) {
activateDrag(e);
} else {
return;
}
}
if (!active || !ghost || !draggedEl)
return;
ghost.style.left = e.clientX - offsetX + "px";
@ -1743,11 +1871,18 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
}
};
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
if (dragPending) {
dragPending = false;
draggedEl = null;
snapRects = /* @__PURE__ */ new Map();
originalOrder = [];
return;
}
if (!active || !draggedEl)
return;
active = false;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
ghost == null ? void 0 : ghost.remove();
ghost = null;
draggedEl.style.opacity = "";
@ -1772,8 +1907,10 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
draggedIndex = -1;
};
container.addEventListener("mousedown", onMouseDown);
container.addEventListener("dragstart", onNativeDragStart);
return () => {
container.removeEventListener("mousedown", onMouseDown);
container.removeEventListener("dragstart", onNativeDragStart);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
ghost == null ? void 0 : ghost.remove();
@ -1785,6 +1922,7 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
ghost = null;
draggedEl = null;
active = false;
dragPending = false;
snapRects = /* @__PURE__ */ new Map();
lastFlipRects = /* @__PURE__ */ new Map();
originalOrder = [];
@ -1792,6 +1930,142 @@ function attachDragToReorder(container, plugin, getSortMode, onReorder, onPlaceh
draggedIndex = -1;
};
}
function attachPinnedListDragToReorder(container, getPaths, onReorder) {
let ghost = null;
let draggedEl = null;
let draggedPath = "";
let offsetY = 0;
let active = false;
let indicator = null;
let dropIndex = -1;
function getItems() {
return Array.from(container.querySelectorAll(":scope > .sc-home-file-item"));
}
function getItemEl(el) {
if (!(el instanceof HTMLElement))
return null;
return el.closest(".sc-home-file-item");
}
function getOrCreateIndicator() {
if (!indicator) {
indicator = document.createElement("div");
indicator.className = "sc-pinned-drop-indicator";
indicator.style.cssText = "position:absolute;left:0;right:0;height:2px;background:var(--interactive-accent);border-radius:2px;pointer-events:none;z-index:100;display:none;";
}
return indicator;
}
function computeDropIndex(clientY) {
const items = getItems().filter((i) => i !== draggedEl);
if (items.length === 0)
return 0;
for (let i = 0; i < items.length; i++) {
const r = items[i].getBoundingClientRect();
if (clientY < r.top + r.height / 2)
return i;
}
return items.length;
}
function positionIndicator(idx) {
const ind = getOrCreateIndicator();
const items = getItems().filter((i) => i !== draggedEl);
const containerRect = container.getBoundingClientRect();
let top;
if (items.length === 0) {
top = 0;
} else if (idx >= items.length) {
const last = items[items.length - 1].getBoundingClientRect();
top = last.bottom - containerRect.top;
} else {
const target = items[idx].getBoundingClientRect();
top = target.top - containerRect.top;
}
ind.style.display = "block";
ind.style.top = top - 1 + "px";
if (!ind.parentElement) {
container.style.position = "relative";
container.appendChild(ind);
}
}
const onMouseDown = (e) => {
var _a;
if (e.button !== 0)
return;
const item = getItemEl(e.target);
if (!item)
return;
draggedEl = item;
draggedPath = (_a = item.dataset.path) != null ? _a : "";
if (!draggedPath)
return;
const rect = item.getBoundingClientRect();
offsetY = e.clientY - rect.top;
ghost = item.cloneNode(true);
ghost.classList.add("sc-drag-ghost");
const cs = getComputedStyle(item);
ghost.style.cssText = `
position:fixed;z-index:9999;pointer-events:none;
width:${rect.width}px;height:${rect.height}px;
left:${rect.left}px;top:${rect.top}px;
background:${cs.background};background-color:${cs.backgroundColor};
border-radius:${cs.borderRadius};padding:${cs.padding};
font-size:${cs.fontSize};color:${cs.color};
box-shadow:0 4px 16px rgba(0,0,0,0.2);opacity:0.9;cursor:grabbing;
`;
document.body.appendChild(ghost);
item.style.opacity = "0.3";
active = true;
dropIndex = computeDropIndex(e.clientY);
positionIndicator(dropIndex);
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
e.preventDefault();
};
const onMouseMove = (e) => {
if (!active || !ghost)
return;
ghost.style.top = e.clientY - offsetY + "px";
dropIndex = computeDropIndex(e.clientY);
positionIndicator(dropIndex);
};
const onMouseUp = () => {
if (!active)
return;
active = false;
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
ghost == null ? void 0 : ghost.remove();
ghost = null;
if (indicator)
indicator.style.display = "none";
if (draggedEl)
draggedEl.style.opacity = "";
const paths = getPaths().slice();
const fromIdx = paths.indexOf(draggedPath);
if (fromIdx !== -1 && dropIndex !== -1) {
paths.splice(fromIdx, 1);
const insertAt = fromIdx < dropIndex ? dropIndex : dropIndex;
paths.splice(insertAt, 0, draggedPath);
void onReorder(paths);
}
draggedEl = null;
draggedPath = "";
dropIndex = -1;
};
container.addEventListener("mousedown", onMouseDown);
return () => {
container.removeEventListener("mousedown", onMouseDown);
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
ghost == null ? void 0 : ghost.remove();
indicator == null ? void 0 : indicator.remove();
if (draggedEl)
draggedEl.style.opacity = "";
ghost = null;
draggedEl = null;
active = false;
indicator = null;
};
}
// src/views/CardSidebarView.ts
var CardSidebarView = class extends import_obsidian4.ItemView {
@ -1921,8 +2195,7 @@ var CardSidebarView = class extends import_obsidian4.ItemView {
"`": ["`", "`"],
"%": ["%", "%"],
"=": ["=", "="],
'"': ['"', '"'],
"'": ["'", "'"]
'"': ['"', '"']
};
const pair = wrapMap[event.key];
if (!pair)
@ -2438,6 +2711,38 @@ var CardSidebarView = class extends import_obsidian4.ItemView {
}, {}, this.store.settings);
})();
});
const moreBtn = buttonContainer.createEl("button");
moreBtn.addClass("sc-icon-btn");
try {
(0, import_obsidian4.setIcon)(moreBtn, "more-vertical");
} catch (e) {
moreBtn.textContent = "\u2026";
}
moreBtn.title = "More";
moreBtn.addEventListener("click", (e) => {
const menu = new import_obsidian4.Menu();
menu.addItem((item) => {
var _a;
item.setTitle("Show tags").setChecked((_a = this.plugin.settings.sidebarShowTags) != null ? _a : true).onClick(async () => {
var _a2;
const current = (_a2 = this.plugin.settings.sidebarShowTags) != null ? _a2 : true;
this.plugin.settings.sidebarShowTags = !current;
await this.plugin.saveSettings();
await this.renderCards();
});
});
menu.addItem((item) => {
var _a;
item.setTitle("Show timestamps").setChecked((_a = this.plugin.settings.sidebarShowTimestamps) != null ? _a : this.plugin.settings.showTimestamps).onClick(async () => {
var _a2;
const current = (_a2 = this.plugin.settings.sidebarShowTimestamps) != null ? _a2 : this.plugin.settings.showTimestamps;
this.plugin.settings.sidebarShowTimestamps = !current;
await this.plugin.saveSettings();
await this.renderCards();
});
});
menu.showAtMouseEvent(e);
});
const addButton = buttonContainer.createEl("button");
addButton.addClass("sc-add-btn");
addButton.textContent = "Add card";
@ -2516,6 +2821,7 @@ var CardSidebarView = class extends import_obsidian4.ItemView {
const settings = { ...this.store.settings };
const scrollTop = ((_a = this.cardsContainer) == null ? void 0 : _a.scrollTop) || 0;
await flipAnimateAsync(this.cardsContainer, async () => {
var _a2, _b2, _c;
this.cardComponents.forEach((comp) => comp.destroy());
this.cardComponents.clear();
this.cardsContainer.empty();
@ -2523,7 +2829,11 @@ var CardSidebarView = class extends import_obsidian4.ItemView {
cards = this.filterService.filter(cards, this.activeFilters);
cards = this.sortService.sort(cards, this.currentSortMode, this.sortAscending, this.app);
for (const card of cards) {
const comp = new CardComponent(this.cardsContainer, card, this.store, this.app, this.plugin);
const comp = new CardComponent(this.cardsContainer, card, this.store, this.app, this.plugin, {
groupTags: (_a2 = this.plugin.settings.sidebarGroupTags) != null ? _a2 : this.plugin.settings.groupTags,
showTimestamps: (_b2 = this.plugin.settings.sidebarShowTimestamps) != null ? _b2 : this.plugin.settings.showTimestamps,
showTags: (_c = this.plugin.settings.sidebarShowTags) != null ? _c : true
});
this.cardComponents.set(card.id, comp);
}
this.applyLayoutMode();
@ -2746,6 +3056,7 @@ var CardStore = class {
this.loadFromSettings();
this.setupExpiryTimer();
this.handleDateRollover();
void this.runStartupDateCleanup();
}
loadFromSettings() {
this.cards.clear();
@ -3124,6 +3435,39 @@ var CardStore = class {
})();
}, 1e3);
}
async runStartupDateCleanup() {
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const todayStartMs = todayStart.getTime();
const tomorrowCards = this.getAll().filter((c) => {
var _a, _b;
if (String(c.category || "").toLowerCase() !== "tomorrow")
return false;
const lastActivity = Math.max((_a = c.modified) != null ? _a : 0, (_b = c.created) != null ? _b : 0);
return lastActivity < todayStartMs;
});
for (const card of tomorrowCards) {
await this.setCategory(card.id, "today");
}
await this.cleanupStaleTodayCards(todayStartMs);
}
async cleanupStaleTodayCards(todayStartMs) {
if (todayStartMs === void 0) {
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
todayStartMs = todayStart.getTime();
}
const stale = this.getAll().filter((c) => {
var _a, _b;
if (String(c.category || "").toLowerCase() !== "today")
return false;
const lastActivity = Math.max((_a = c.modified) != null ? _a : 0, (_b = c.created) != null ? _b : 0);
return lastActivity < todayStartMs;
});
for (const card of stale) {
await this.setCategory(card.id, null);
}
}
handleDateRollover() {
if (this.dateRolloverTimeout) {
window.clearTimeout(this.dateRolloverTimeout);
@ -3134,10 +3478,7 @@ var CardStore = class {
const delay = Math.max(1e3, next.getTime() - now.getTime());
this.dateRolloverTimeout = window.setTimeout(() => {
void (async () => {
const cards = this.getAll().filter((c) => String(c.category || "").toLowerCase() === "tomorrow");
for (const card of cards) {
await this.setCategory(card.id, "today");
}
await this.runStartupDateCleanup();
this.handleDateRollover();
})();
}, delay);
@ -4099,8 +4440,7 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
"`": ["`", "`"],
"%": ["%", "%"],
"=": ["=", "="],
'"': ['"', '"'],
"'": ["'", "'"]
'"': ['"', '"']
};
const pair = wrapMap[key];
if (!pair)
@ -4407,6 +4747,8 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
}
for (const file of files) {
const item = container.createDiv({ cls: "sc-home-file-item" });
if (type === "pinned")
item.dataset.path = file.path;
const iconSpan = item.createSpan({ cls: "sc-home-file-icon" });
await this.renderCustomFileIcon(iconSpan, file);
item.createSpan({ text: file.basename, cls: "sc-home-file-name" });
@ -4426,6 +4768,20 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
});
}
}
if (type === "pinned") {
attachPinnedListDragToReorder(
container,
() => {
var _a;
return (_a = this.plugin.settings.pinnedNotes) != null ? _a : [];
},
async (newPaths) => {
this.plugin.settings.pinnedNotes = newPaths;
await this.plugin.saveSettings();
await this.renderFileList(container, "pinned");
}
);
}
}
async renderCustomFileIcon(iconEl, file) {
var _a;
@ -4682,7 +5038,7 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
});
const searchIcon = searchWrap.createSpan({ cls: "sc-home-search-icon" });
(0, import_obsidian9.setIcon)(searchIcon, "search");
const searchInput = searchWrap.createEl("input", { cls: "sc-home-search-input", placeholder: "Search card..." });
const searchInput = searchWrap.createEl("input", { cls: "sc-home-search-input", placeholder: "Search cards..." });
searchInput.addEventListener("input", () => {
this.currentSearchQuery = searchInput.value;
void this.renderCards(cardList);
@ -4701,15 +5057,21 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
moreBtn.addEventListener("click", (e) => {
const menu = new import_obsidian9.Menu();
menu.addItem((item) => {
item.setTitle("Show tags").setChecked(!this.plugin.settings.groupTags).onClick(async () => {
this.plugin.settings.groupTags = !this.plugin.settings.groupTags;
var _a;
item.setTitle("Show tags").setChecked((_a = this.plugin.settings.homeShowTags) != null ? _a : true).onClick(async () => {
var _a2;
const current = (_a2 = this.plugin.settings.homeShowTags) != null ? _a2 : true;
this.plugin.settings.homeShowTags = !current;
await this.plugin.saveSettings();
void this.renderCards(cardList);
});
});
menu.addItem((item) => {
item.setTitle("Show timestamps").setChecked(this.plugin.settings.showTimestamps).onClick(async () => {
this.plugin.settings.showTimestamps = !this.plugin.settings.showTimestamps;
var _a;
item.setTitle("Show timestamps").setChecked((_a = this.plugin.settings.homeShowTimestamps) != null ? _a : this.plugin.settings.showTimestamps).onClick(async () => {
var _a2;
const current = (_a2 = this.plugin.settings.homeShowTimestamps) != null ? _a2 : this.plugin.settings.showTimestamps;
this.plugin.settings.homeShowTimestamps = !current;
await this.plugin.saveSettings();
void this.renderCards(cardList);
});
@ -4805,7 +5167,12 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
if (chunk.length === 0)
return;
chunk.forEach((card) => {
const comp = new CardComponent(container, card, this.store, this.app, this.plugin);
var _a2, _b, _c;
const comp = new CardComponent(container, card, this.store, this.app, this.plugin, {
groupTags: (_a2 = this.plugin.settings.homeGroupTags) != null ? _a2 : this.plugin.settings.groupTags,
showTimestamps: (_b = this.plugin.settings.homeShowTimestamps) != null ? _b : this.plugin.settings.showTimestamps,
showTags: (_c = this.plugin.settings.homeShowTags) != null ? _c : true
});
this.cardComponents.set(card.id, comp);
});
if (startIdx + CHUNK_SIZE < cards.length) {
@ -4825,22 +5192,42 @@ var SideCardsHomeView = class extends import_obsidian9.ItemView {
);
}
getAvailableFilters() {
const filters = [{ type: "all", label: "All", value: "all" }];
if (!this.plugin.settings.hideTodayFilter) {
filters.push({ type: "category", label: "Today", value: "today" });
}
if (!this.plugin.settings.hideTomorrowFilter) {
filters.push({ type: "category", label: "Tomorrow", value: "tomorrow" });
}
if (this.plugin.settings.enableCustomCategories) {
const cats = Array.isArray(this.plugin.settings.customCategories) ? this.plugin.settings.customCategories : [];
cats.forEach((cat) => {
if (cat)
const settings = this.plugin.settings;
const cats = Array.isArray(settings.customCategories) ? settings.customCategories : [];
const defaultOrder = ["filter-all"].concat(!settings.hideTodayFilter ? ["filter-today"] : []).concat(!settings.hideTomorrowFilter ? ["filter-tomorrow"] : []).concat(!settings.hideArchivedFilterButton ? ["filter-archived"] : []).concat(cats.map((c) => String(c.id || "")));
const combinedOrder = Array.isArray(settings.allItemsOrder) && settings.allItemsOrder.length > 0 ? settings.allItemsOrder : defaultOrder;
const filters = [];
combinedOrder.forEach((itemId) => {
if (!itemId)
return;
if (itemId === "filter-all") {
filters.push({ type: "all", label: "All", value: "all" });
return;
}
if (itemId === "filter-today") {
if (!settings.hideTodayFilter)
filters.push({ type: "category", label: "Today", value: "today" });
return;
}
if (itemId === "filter-tomorrow") {
if (!settings.hideTomorrowFilter)
filters.push({ type: "category", label: "Tomorrow", value: "tomorrow" });
return;
}
if (itemId === "filter-archived") {
if (!settings.hideArchivedFilterButton)
filters.push({ type: "archived", label: "Archived", value: "archived" });
return;
}
if (settings.enableCustomCategories) {
const cat = cats.find((c) => String(c.id) === String(itemId));
if (cat && cat.showInMenu !== false) {
filters.push({ type: "category", label: cat.label || "", value: cat.id || cat.label || "" });
});
}
if (!this.plugin.settings.hideArchivedFilterButton) {
filters.push({ type: "archived", label: "Archived", value: "archived" });
}
}
});
if (!filters.find((f) => f.value === "all")) {
filters.unshift({ type: "all", label: "All", value: "all" });
}
return filters;
}
@ -5047,6 +5434,17 @@ var SideCardsPlugin = class extends import_obsidian10.Plugin {
this.addRibbonIcon("rows-3", "Open sidecards sidebar", () => void this.activateView());
this.addRibbonIcon("rectangle-horizontal", "Add card to sidecards", () => new QuickCardWithFilterModal(this.app, this, this.store).open());
this.addSettingTab(new SideCardsSettingTab(this.app, this));
this.registerEvent(
this.app.vault.on("rename", (file, oldPath) => {
if (!(file instanceof import_obsidian10.TFile))
return;
const card = this.store.getAll().find((c) => c.notePath === oldPath);
if (!card)
return;
card.notePath = file.path;
void this.store.saveCards();
})
);
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof import_obsidian10.TFile) {
@ -5094,6 +5492,76 @@ var SideCardsPlugin = class extends import_obsidian10.Plugin {
);
this.injectStatusColors();
this.applyButtonPadding();
this.registerDomEvent(document, "dragover", (ev) => {
if (!ev.dataTransfer)
return;
const types = Array.from(ev.dataTransfer.types || []);
if (!types.includes("text/x-card-sidebar"))
return;
ev.preventDefault();
ev.dataTransfer.dropEffect = "copy";
});
let lastDropTime = 0;
const cardDropHandler = (ev) => {
var _a2;
if (!ev.dataTransfer)
return;
const types = Array.from(ev.dataTransfer.types || []);
if (!types.includes("text/x-card-sidebar"))
return;
const now = Date.now();
if (now - lastDropTime < 200) {
ev.preventDefault();
ev.stopImmediatePropagation();
return;
}
lastDropTime = now;
ev.preventDefault();
ev.stopImmediatePropagation();
let content = null;
try {
const json = ev.dataTransfer.getData("text/x-card-sidebar");
const payload = JSON.parse(json);
content = (_a2 = payload.content) != null ? _a2 : null;
} catch (e) {
}
if (!content)
return;
let mdView = null;
const target = ev.target;
if (target) {
this.app.workspace.iterateAllLeaves((leaf) => {
var _a3, _b;
if (mdView)
return;
const view = leaf.view;
if (!(view instanceof import_obsidian10.MarkdownView))
return;
const editorEl = (_b = (_a3 = view.editor) == null ? void 0 : _a3.containerEl) != null ? _b : view.contentEl;
if (editorEl && editorEl.contains(target))
mdView = view;
});
}
if (!mdView)
mdView = this.app.workspace.getActiveViewOfType(import_obsidian10.MarkdownView);
if (!(mdView == null ? void 0 : mdView.editor))
return;
const cm = mdView.editor.cm;
if (cm == null ? void 0 : cm.posAtCoords) {
const pos = cm.posAtCoords({ x: ev.clientX, y: ev.clientY }, false);
if (pos != null)
cm.dispatch({ selection: { anchor: pos } });
}
mdView.editor.replaceSelection(content);
mdView.editor.focus();
};
document.addEventListener(
"drop",
cardDropHandler,
true
/* capture */
);
this._cardDropCleanup = () => document.removeEventListener("drop", cardDropHandler, true);
this.app.workspace.onLayoutReady(() => {
if (!this.settings.storageFolder) {
this.showStorageFolderSetupModal();
@ -5109,6 +5577,8 @@ var SideCardsPlugin = class extends import_obsidian10.Plugin {
const el = document.getElementById("sc-status-colors");
if (el)
el.remove();
if (this._cardDropCleanup)
this._cardDropCleanup();
}
applyButtonPadding() {
var _a;
@ -5474,7 +5944,7 @@ var SideCardsSettingTab = class extends import_obsidian10.PluginSettingTab {
this.plugin.settings.nextLineKey = value;
await this.plugin.saveSettings();
}));
new import_obsidian10.Setting(containerEl).setName("Auto-pair brackets, quotes and code").setDesc("Automatically wrap selected text or place cursor between pairs when typing (, [, {, `, =, %, \", or '").addToggle((toggle) => toggle.setValue(this.plugin.settings.autoPairBrackets !== false).onChange(async (value) => {
new import_obsidian10.Setting(containerEl).setName("Auto-pair brackets, quotes and code").setDesc('Automatically wrap selected text or place cursor between pairs when typing (, [, {, `, =, %, or "').addToggle((toggle) => toggle.setValue(this.plugin.settings.autoPairBrackets !== false).onChange(async (value) => {
this.plugin.settings.autoPairBrackets = value;
await this.plugin.saveSettings();
}));

View file

@ -1,5 +1,5 @@
import { Plugin, PluginSettingTab, App, Setting, WorkspaceLeaf, setIcon, Notice, TFile, Platform, requestUrl, Component, MarkdownRenderer, Modal, getIconIds } from "obsidian";
import { Plugin, PluginSettingTab, App, Setting, WorkspaceLeaf, setIcon, Notice, TFile, Platform, requestUrl, Component, MarkdownRenderer, Modal, getIconIds, MarkdownView } from "obsidian";
import { CardSidebarView } from "../views/CardSidebarView";
import { CardStore } from "../services/CardStore";
import { FilterService } from "../services/FilterService";
@ -179,6 +179,18 @@ export default class SideCardsPlugin extends Plugin {
this.addSettingTab(new SideCardsSettingTab(this.app, this));
// Keep card.notePath in sync when a note is renamed/moved
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
if (!(file instanceof TFile)) return;
const card = this.store.getAll().find(c => c.notePath === oldPath);
if (!card) return;
// Update in-memory path and persist
card.notePath = file.path;
void this.store.saveCards();
})
);
this.registerEvent(
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof TFile) {
@ -229,6 +241,69 @@ export default class SideCardsPlugin extends Plugin {
this.injectStatusColors();
this.applyButtonPadding();
// Allow drop on any element — needed so the browser accepts the drop
this.registerDomEvent(document, 'dragover', (ev: DragEvent) => {
if (!ev.dataTransfer) return;
const types = Array.from(ev.dataTransfer.types || []);
if (!types.includes('text/x-card-sidebar')) return;
ev.preventDefault();
ev.dataTransfer.dropEffect = 'copy';
});
// Drop card content into a markdown editor.
// Must use capture phase so our handler runs before CodeMirror's own drop handler,
// which would otherwise also insert the dragged text and cause duplicates.
let lastDropTime = 0;
const cardDropHandler = (ev: DragEvent) => {
if (!ev.dataTransfer) return;
const types = Array.from(ev.dataTransfer.types || []);
if (!types.includes('text/x-card-sidebar')) return;
// Deduplicate: ignore if we already handled a drop within 200ms
const now = Date.now();
if (now - lastDropTime < 200) { ev.preventDefault(); ev.stopImmediatePropagation(); return; }
lastDropTime = now;
ev.preventDefault();
ev.stopImmediatePropagation();
let content: string | null = null;
try {
const json = ev.dataTransfer.getData('text/x-card-sidebar');
const payload = JSON.parse(json);
content = payload.content ?? null;
} catch { /* malformed payload */ }
if (!content) return;
// Find the MarkdownView whose editor DOM contains the drop target,
// falling back to whichever MarkdownView is currently active.
let mdView: MarkdownView | null = null;
const target = ev.target as HTMLElement | null;
if (target) {
this.app.workspace.iterateAllLeaves((leaf) => {
if (mdView) return;
const view = leaf.view;
if (!(view instanceof MarkdownView)) return;
const editorEl = (view as any).editor?.containerEl ?? (view as any).contentEl;
if (editorEl && editorEl.contains(target)) mdView = view;
});
}
if (!mdView) mdView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (!mdView?.editor) return;
// Place cursor at the drop position before inserting
const cm = (mdView.editor as any).cm;
if (cm?.posAtCoords) {
const pos = cm.posAtCoords({ x: ev.clientX, y: ev.clientY }, false);
if (pos != null) cm.dispatch({ selection: { anchor: pos } });
}
mdView.editor.replaceSelection(content);
mdView.editor.focus();
};
document.addEventListener('drop', cardDropHandler, true /* capture */);
(this as any)._cardDropCleanup = () => document.removeEventListener('drop', cardDropHandler, true);
// Auto-open sidebar on startup; show setup modal if no storage folder set
this.app.workspace.onLayoutReady(() => {
if (!this.settings.storageFolder) {
@ -246,6 +321,8 @@ export default class SideCardsPlugin extends Plugin {
onunload() {
const el = document.getElementById('sc-status-colors');
if (el) el.remove();
// Remove the capture-phase drop handler registered in onload
if ((this as any)._cardDropCleanup) (this as any)._cardDropCleanup();
}
applyButtonPadding(): void {
@ -661,7 +738,7 @@ class SideCardsSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
}));
new Setting(containerEl).setName('Auto-pair brackets, quotes and code').setDesc('Automatically wrap selected text or place cursor between pairs when typing (, [, {, `, =, %, ", or \'').addToggle(toggle => toggle.setValue(this.plugin.settings.autoPairBrackets !== false).onChange(async (value) => {
new Setting(containerEl).setName('Auto-pair brackets, quotes and code').setDesc('Automatically wrap selected text or place cursor between pairs when typing (, [, {, `, =, %, or "').addToggle(toggle => toggle.setValue(this.plugin.settings.autoPairBrackets !== false).onChange(async (value) => {
this.plugin.settings.autoPairBrackets = value;
await this.plugin.saveSettings();
}));

View file

@ -31,6 +31,12 @@ export interface SideCardsSettings {
showTimestamps: boolean;
omitTagHash: boolean;
groupTags: boolean;
sidebarShowTimestamps?: boolean;
sidebarGroupTags?: boolean;
homeShowTimestamps?: boolean;
homeGroupTags?: boolean;
sidebarShowTags?: boolean;
homeShowTags?: boolean;
disableCardRendering: boolean;
minCardWidth: number;
colorNames: string[];

View file

@ -25,6 +25,7 @@ export class CardStore {
this.loadFromSettings();
this.setupExpiryTimer();
this.handleDateRollover();
void this.runStartupDateCleanup();
}
private loadFromSettings() {
@ -428,6 +429,44 @@ export class CardStore {
}, 1000);
}
private async runStartupDateCleanup(): Promise<void> {
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
const todayStartMs = todayStart.getTime();
// Promote tomorrow → today only for cards assigned on a previous day
// (i.e. the day has rolled over since they were put in "tomorrow")
const tomorrowCards = this.getAll().filter(c => {
if (String(c.category || '').toLowerCase() !== 'tomorrow') return false;
const lastActivity = Math.max(c.modified ?? 0, c.created ?? 0);
return lastActivity < todayStartMs;
});
for (const card of tomorrowCards) {
await this.setCategory(card.id, 'today');
}
// Clear stale today cards (assigned on a previous day, never modified since)
await this.cleanupStaleTodayCards(todayStartMs);
}
private async cleanupStaleTodayCards(todayStartMs?: number): Promise<void> {
if (todayStartMs === undefined) {
const todayStart = new Date();
todayStart.setHours(0, 0, 0, 0);
todayStartMs = todayStart.getTime();
}
const stale = this.getAll().filter(c => {
if (String(c.category || '').toLowerCase() !== 'today') return false;
const lastActivity = Math.max(c.modified ?? 0, c.created ?? 0);
return lastActivity < todayStartMs!;
});
for (const card of stale) {
await this.setCategory(card.id, null);
}
}
handleDateRollover(): void {
if (this.dateRolloverTimeout) {
window.clearTimeout(this.dateRolloverTimeout);
@ -438,10 +477,7 @@ export class CardStore {
const delay = Math.max(1000, next.getTime() - now.getTime());
this.dateRolloverTimeout = window.setTimeout(() => {
void (async () => {
const cards = this.getAll().filter(c => String(c.category || '').toLowerCase() === 'tomorrow');
for (const card of cards) {
await this.setCategory(card.id, 'today');
}
await this.runStartupDateCleanup();
this.handleDateRollover();
})();
}, delay);

View file

@ -142,6 +142,30 @@ export function attachDragToReorder(
});
}
let dragStartX = 0;
let dragStartY = 0;
let dragPending = false; // mousedown happened but threshold not yet crossed
const DRAG_THRESHOLD = 5; // px
// If a native HTML5 drag starts (e.g. drag-to-editor), cancel the custom reorder
// immediately so the ghost never appears and mouseup cleanup is skipped.
const onNativeDragStart = () => {
if (!dragPending && !active) return;
// Cancel pending/active custom drag without committing
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
ghost?.remove();
ghost = null;
if (draggedEl) {
draggedEl.style.opacity = '';
draggedEl.style.pointerEvents = '';
}
clearTransforms();
draggedEl = null; active = false; dragPending = false;
snapRects = new Map(); lastFlipRects = new Map();
originalOrder = []; currentTargetIndex = -1; draggedIndex = -1;
};
const onMouseDown = (e: MouseEvent) => {
if (getSortMode() !== 'manual') return;
if (e.button !== 0) return;
@ -152,8 +176,11 @@ export function attachDragToReorder(
originalOrder = getCards();
draggedIndex = originalOrder.indexOf(card);
currentTargetIndex = draggedIndex;
dragStartX = e.clientX;
dragStartY = e.clientY;
dragPending = true;
// Snapshot home rects before any changes
// Snapshot rects eagerly so they're ready when drag activates
snapRects = new Map();
originalOrder.forEach(c => snapRects.set(c, c.getBoundingClientRect()));
@ -161,14 +188,26 @@ export function attachDragToReorder(
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
// Don't preventDefault here — let click events through until threshold crossed
};
function activateDrag(e: MouseEvent) {
if (!draggedEl) return;
dragPending = false;
active = true;
const rect = snapRects.get(draggedEl)!;
// Hide source card in-place (keeps its grid slot)
card.style.opacity = '0';
card.style.pointerEvents = 'none';
draggedEl.style.opacity = '0';
draggedEl.style.pointerEvents = 'none';
// Ghost: clone with resolved computed styles so CSS vars work on document.body
ghost = card.cloneNode(true) as HTMLElement;
ghost = draggedEl.cloneNode(true) as HTMLElement;
ghost.classList.add('sc-drag-ghost');
const cs = getComputedStyle(card);
const cs = getComputedStyle(draggedEl);
ghost.style.background = cs.background;
ghost.style.backgroundColor = cs.backgroundColor;
ghost.style.borderColor = cs.borderColor;
@ -184,8 +223,8 @@ export function attachDragToReorder(
ghost.style.pointerEvents = 'none';
ghost.style.width = rect.width + 'px';
ghost.style.height = rect.height + 'px';
ghost.style.left = rect.left + 'px';
ghost.style.top = rect.top + 'px';
ghost.style.left = (e.clientX - offsetX) + 'px';
ghost.style.top = (e.clientY - offsetY) + 'px';
ghost.style.margin = '0';
ghost.style.boxShadow = '0 8px 24px rgba(0,0,0,0.25)';
ghost.style.opacity = '1';
@ -193,14 +232,18 @@ export function attachDragToReorder(
ghost.style.transform = '';
ghost.style.transition = '';
document.body.appendChild(ghost);
active = true;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
}
const onMouseMove = (e: MouseEvent) => {
if (dragPending) {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
if (Math.hypot(dx, dy) >= DRAG_THRESHOLD) {
activateDrag(e);
} else {
return;
}
}
if (!active || !ghost || !draggedEl) return;
ghost.style.left = (e.clientX - offsetX) + 'px';
@ -209,18 +252,27 @@ export function attachDragToReorder(
const newTarget = computeTargetIndex(e.clientX, e.clientY);
if (newTarget !== currentTargetIndex) {
currentTargetIndex = newTarget;
// FLIP: read real layout for this target, then animate
lastFlipRects = readFlipRects(currentTargetIndex);
applyFlip(lastFlipRects);
}
};
const onMouseUp = () => {
if (!active || !draggedEl) return;
active = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// Threshold never crossed — just a click, cancel silently
if (dragPending) {
dragPending = false;
draggedEl = null;
snapRects = new Map();
originalOrder = [];
return;
}
if (!active || !draggedEl) return;
active = false;
ghost?.remove();
ghost = null;
@ -249,9 +301,11 @@ export function attachDragToReorder(
};
container.addEventListener('mousedown', onMouseDown);
container.addEventListener('dragstart', onNativeDragStart);
return () => {
container.removeEventListener('mousedown', onMouseDown);
container.removeEventListener('dragstart', onNativeDragStart);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
ghost?.remove();
@ -260,8 +314,163 @@ export function attachDragToReorder(
draggedEl.style.pointerEvents = '';
}
clearTransforms();
ghost = null; draggedEl = null; active = false;
ghost = null; draggedEl = null; active = false; dragPending = false;
snapRects = new Map(); lastFlipRects = new Map();
originalOrder = []; currentTargetIndex = -1; draggedIndex = -1;
};
}
/**
* Drag-to-reorder for the pinned notes list.
* Items are vertical list rows (.sc-home-file-item).
* Persists new order to plugin.settings.pinnedNotes.
*/
export function attachPinnedListDragToReorder(
container: HTMLElement,
getPaths: () => string[],
onReorder: (newPaths: string[]) => void | Promise<void>
): () => void {
let ghost: HTMLElement | null = null;
let draggedEl: HTMLElement | null = null;
let draggedPath = '';
let offsetY = 0;
let active = false;
let indicator: HTMLElement | null = null;
let dropIndex = -1;
function getItems(): HTMLElement[] {
return Array.from(container.querySelectorAll<HTMLElement>(':scope > .sc-home-file-item'));
}
function getItemEl(el: EventTarget | null): HTMLElement | null {
if (!(el instanceof HTMLElement)) return null;
return el.closest<HTMLElement>('.sc-home-file-item');
}
function getOrCreateIndicator(): HTMLElement {
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'sc-pinned-drop-indicator';
indicator.style.cssText = 'position:absolute;left:0;right:0;height:2px;background:var(--interactive-accent);border-radius:2px;pointer-events:none;z-index:100;display:none;';
}
return indicator;
}
function computeDropIndex(clientY: number): number {
const items = getItems().filter(i => i !== draggedEl);
if (items.length === 0) return 0;
for (let i = 0; i < items.length; i++) {
const r = items[i].getBoundingClientRect();
if (clientY < r.top + r.height / 2) return i;
}
return items.length;
}
function positionIndicator(idx: number) {
const ind = getOrCreateIndicator();
const items = getItems().filter(i => i !== draggedEl);
const containerRect = container.getBoundingClientRect();
let top: number;
if (items.length === 0) {
top = 0;
} else if (idx >= items.length) {
const last = items[items.length - 1].getBoundingClientRect();
top = last.bottom - containerRect.top;
} else {
const target = items[idx].getBoundingClientRect();
top = target.top - containerRect.top;
}
ind.style.display = 'block';
ind.style.top = (top - 1) + 'px';
if (!ind.parentElement) {
container.style.position = 'relative';
container.appendChild(ind);
}
}
const onMouseDown = (e: MouseEvent) => {
if (e.button !== 0) return;
const item = getItemEl(e.target);
if (!item) return;
draggedEl = item;
draggedPath = item.dataset.path ?? '';
if (!draggedPath) return;
const rect = item.getBoundingClientRect();
offsetY = e.clientY - rect.top;
// Ghost
ghost = item.cloneNode(true) as HTMLElement;
ghost.classList.add('sc-drag-ghost');
const cs = getComputedStyle(item);
ghost.style.cssText = `
position:fixed;z-index:9999;pointer-events:none;
width:${rect.width}px;height:${rect.height}px;
left:${rect.left}px;top:${rect.top}px;
background:${cs.background};background-color:${cs.backgroundColor};
border-radius:${cs.borderRadius};padding:${cs.padding};
font-size:${cs.fontSize};color:${cs.color};
box-shadow:0 4px 16px rgba(0,0,0,0.2);opacity:0.9;cursor:grabbing;
`;
document.body.appendChild(ghost);
item.style.opacity = '0.3';
active = true;
dropIndex = computeDropIndex(e.clientY);
positionIndicator(dropIndex);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
e.preventDefault();
};
const onMouseMove = (e: MouseEvent) => {
if (!active || !ghost) return;
ghost.style.top = (e.clientY - offsetY) + 'px';
dropIndex = computeDropIndex(e.clientY);
positionIndicator(dropIndex);
};
const onMouseUp = () => {
if (!active) return;
active = false;
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
ghost?.remove();
ghost = null;
if (indicator) indicator.style.display = 'none';
if (draggedEl) draggedEl.style.opacity = '';
// Commit new order
const paths = getPaths().slice();
const fromIdx = paths.indexOf(draggedPath);
if (fromIdx !== -1 && dropIndex !== -1) {
paths.splice(fromIdx, 1);
// dropIndex is relative to items excluding dragged — adjust for removal
const insertAt = fromIdx < dropIndex ? dropIndex : dropIndex;
paths.splice(insertAt, 0, draggedPath);
void onReorder(paths);
}
draggedEl = null;
draggedPath = '';
dropIndex = -1;
};
container.addEventListener('mousedown', onMouseDown);
return () => {
container.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
ghost?.remove();
indicator?.remove();
if (draggedEl) draggedEl.style.opacity = '';
ghost = null; draggedEl = null; active = false; indicator = null;
};
}

View file

@ -13,7 +13,6 @@ export function handleKeyWrap(event: KeyboardEvent, editorEl: HTMLElement, edito
"%": ["%", "%"],
"=": ["=", "="],
'"': ['"', '"'],
"'": ["'", "'"],
};
const pair = wrapMap[key];

View file

@ -141,7 +141,6 @@ export class CardSidebarView extends ItemView {
"%": ["%", "%"],
"=": ["=", "="],
'"': ['"', '"'],
"'": ["'", "'"],
};
const pair = wrapMap[event.key];
if (!pair) return false;
@ -691,6 +690,35 @@ export class CardSidebarView extends ItemView {
})();
});
const moreBtn = buttonContainer.createEl('button');
moreBtn.addClass('sc-icon-btn');
try { setIcon(moreBtn, 'more-vertical'); } catch { moreBtn.textContent = '…'; }
moreBtn.title = 'More';
moreBtn.addEventListener('click', (e) => {
const menu = new Menu();
menu.addItem(item => {
item.setTitle('Show tags')
.setChecked(this.plugin.settings.sidebarShowTags ?? true)
.onClick(async () => {
const current = this.plugin.settings.sidebarShowTags ?? true;
this.plugin.settings.sidebarShowTags = !current;
await this.plugin.saveSettings();
await this.renderCards();
});
});
menu.addItem(item => {
item.setTitle('Show timestamps')
.setChecked(this.plugin.settings.sidebarShowTimestamps ?? this.plugin.settings.showTimestamps)
.onClick(async () => {
const current = this.plugin.settings.sidebarShowTimestamps ?? this.plugin.settings.showTimestamps;
this.plugin.settings.sidebarShowTimestamps = !current;
await this.plugin.saveSettings();
await this.renderCards();
});
});
menu.showAtMouseEvent(e);
});
const addButton = buttonContainer.createEl('button');
addButton.addClass('sc-add-btn');
addButton.textContent = 'Add card';
@ -783,7 +811,11 @@ export class CardSidebarView extends ItemView {
// Render each card
for (const card of cards) {
const comp = new CardComponent(this.cardsContainer, card, this.store, this.app, this.plugin);
const comp = new CardComponent(this.cardsContainer, card, this.store, this.app, this.plugin, {
groupTags: this.plugin.settings.sidebarGroupTags ?? this.plugin.settings.groupTags,
showTimestamps: this.plugin.settings.sidebarShowTimestamps ?? this.plugin.settings.showTimestamps,
showTags: this.plugin.settings.sidebarShowTags ?? true,
});
this.cardComponents.set(card.id, comp);
}

View file

@ -7,7 +7,7 @@ import { SortService, SortMode } from "../services/SortService";
import { resolveAutoColor } from "../utils/dom";
import { animateCardsEntrance } from "../utils/animations";
import { InlineAutocomplete } from "./components/InlineAutocomplete";
import { attachDragToReorder } from "../utils/drag-drop";
import { attachDragToReorder, attachPinnedListDragToReorder } from "../utils/drag-drop";
export class SideCardsHomeView extends ItemView {
private selectedColor = 'var(--card-color-1)';
@ -146,7 +146,6 @@ export class SideCardsHomeView extends ItemView {
"%": ["%", "%"],
"=": ["=", "="],
'"': ['"', '"'],
"'": ["'", "'"],
};
const pair = wrapMap[key];
@ -492,6 +491,7 @@ export class SideCardsHomeView extends ItemView {
for (const file of files) {
const item = container.createDiv({ cls: 'sc-home-file-item' });
if (type === 'pinned') item.dataset.path = file.path;
const iconSpan = item.createSpan({ cls: 'sc-home-file-icon' });
await this.renderCustomFileIcon(iconSpan, file);
item.createSpan({ text: file.basename, cls: 'sc-home-file-name' });
@ -513,6 +513,18 @@ export class SideCardsHomeView extends ItemView {
});
}
}
if (type === 'pinned') {
attachPinnedListDragToReorder(
container,
() => this.plugin.settings.pinnedNotes ?? [],
async (newPaths) => {
this.plugin.settings.pinnedNotes = newPaths;
await this.plugin.saveSettings();
await this.renderFileList(container, 'pinned');
}
);
}
}
private async renderCustomFileIcon(iconEl: HTMLElement, file: TFile): Promise<void> {
@ -791,7 +803,7 @@ export class SideCardsHomeView extends ItemView {
// Search Bar
const searchIcon = searchWrap.createSpan({ cls: 'sc-home-search-icon' });
setIcon(searchIcon, 'search');
const searchInput = searchWrap.createEl('input', { cls: 'sc-home-search-input', placeholder: 'Search card...' });
const searchInput = searchWrap.createEl('input', { cls: 'sc-home-search-input', placeholder: 'Search cards...' });
searchInput.addEventListener('input', () => {
this.currentSearchQuery = searchInput.value;
void this.renderCards(cardList);
@ -814,18 +826,20 @@ export class SideCardsHomeView extends ItemView {
const menu = new Menu();
menu.addItem(item => {
item.setTitle('Show tags')
.setChecked(!this.plugin.settings.groupTags)
.setChecked(this.plugin.settings.homeShowTags ?? true)
.onClick(async () => {
this.plugin.settings.groupTags = !this.plugin.settings.groupTags;
const current = this.plugin.settings.homeShowTags ?? true;
this.plugin.settings.homeShowTags = !current;
await this.plugin.saveSettings();
void this.renderCards(cardList);
});
});
menu.addItem(item => {
item.setTitle('Show timestamps')
.setChecked(this.plugin.settings.showTimestamps)
.setChecked(this.plugin.settings.homeShowTimestamps ?? this.plugin.settings.showTimestamps)
.onClick(async () => {
this.plugin.settings.showTimestamps = !this.plugin.settings.showTimestamps;
const current = this.plugin.settings.homeShowTimestamps ?? this.plugin.settings.showTimestamps;
this.plugin.settings.homeShowTimestamps = !current;
await this.plugin.saveSettings();
void this.renderCards(cardList);
});
@ -934,7 +948,11 @@ export class SideCardsHomeView extends ItemView {
const chunk = cards.slice(startIdx, startIdx + CHUNK_SIZE);
if (chunk.length === 0) return;
chunk.forEach(card => {
const comp = new CardComponent(container, card, this.store, this.app, this.plugin);
const comp = new CardComponent(container, card, this.store, this.app, this.plugin, {
groupTags: this.plugin.settings.homeGroupTags ?? this.plugin.settings.groupTags,
showTimestamps: this.plugin.settings.homeShowTimestamps ?? this.plugin.settings.showTimestamps,
showTags: this.plugin.settings.homeShowTags ?? true,
});
this.cardComponents.set(card.id, comp);
});
if (startIdx + CHUNK_SIZE < cards.length) {
@ -959,22 +977,53 @@ export class SideCardsHomeView extends ItemView {
}
private getAvailableFilters(): Array<{ type: string; label: string; value: string }> {
const filters = [{ type: 'all', label: 'All', value: 'all' }];
if (!this.plugin.settings.hideTodayFilter) {
filters.push({ type: 'category', label: 'Today', value: 'today' });
}
if (!this.plugin.settings.hideTomorrowFilter) {
filters.push({ type: 'category', label: 'Tomorrow', value: 'tomorrow' });
}
if (this.plugin.settings.enableCustomCategories) {
const cats = Array.isArray(this.plugin.settings.customCategories) ? this.plugin.settings.customCategories : [];
cats.forEach(cat => {
if (cat) filters.push({ type: 'category', label: cat.label || '', value: cat.id || cat.label || '' });
});
}
if (!this.plugin.settings.hideArchivedFilterButton) {
filters.push({ type: 'archived', label: 'Archived', value: 'archived' });
const settings = this.plugin.settings;
const cats = Array.isArray(settings.customCategories) ? settings.customCategories : [];
// Build default order matching the sidebar
const defaultOrder = ['filter-all']
.concat(!settings.hideTodayFilter ? ['filter-today'] : [])
.concat(!settings.hideTomorrowFilter ? ['filter-tomorrow'] : [])
.concat(!settings.hideArchivedFilterButton ? ['filter-archived'] : [])
.concat(cats.map(c => String(c.id || '')));
const combinedOrder = Array.isArray(settings.allItemsOrder) && settings.allItemsOrder.length > 0
? settings.allItemsOrder
: defaultOrder;
const filters: Array<{ type: string; label: string; value: string }> = [];
combinedOrder.forEach(itemId => {
if (!itemId) return;
if (itemId === 'filter-all') {
filters.push({ type: 'all', label: 'All', value: 'all' });
return;
}
if (itemId === 'filter-today') {
if (!settings.hideTodayFilter) filters.push({ type: 'category', label: 'Today', value: 'today' });
return;
}
if (itemId === 'filter-tomorrow') {
if (!settings.hideTomorrowFilter) filters.push({ type: 'category', label: 'Tomorrow', value: 'tomorrow' });
return;
}
if (itemId === 'filter-archived') {
if (!settings.hideArchivedFilterButton) filters.push({ type: 'archived', label: 'Archived', value: 'archived' });
return;
}
if (settings.enableCustomCategories) {
const cat = cats.find(c => String(c.id) === String(itemId));
if (cat && cat.showInMenu !== false) {
filters.push({ type: 'category', label: cat.label || '', value: cat.id || cat.label || '' });
}
}
});
// Ensure 'All' is always present
if (!filters.find(f => f.value === 'all')) {
filters.unshift({ type: 'all', label: 'All', value: 'all' });
}
return filters;
}

View file

@ -35,7 +35,8 @@ export class CardComponent {
card: Card,
private store: CardStore,
private app: App,
private plugin: Plugin
private plugin: Plugin,
private settingsOverride?: { groupTags?: boolean; showTimestamps?: boolean; showTags?: boolean }
) {
CardComponent.instanceCount += 1;
this.card = card;
@ -215,9 +216,75 @@ export class CardComponent {
private async render(): Promise<void> {
const currentRender = ++this.renderCount;
this.stopExpiryTick();
// When entering edit mode, swap only the content div in-place
// to avoid a full re-render race that removes the contenteditable element
if (this.isEditing) {
const existingContent = this.el.querySelector('.sc-content') as HTMLElement | null;
if (existingContent) {
existingContent.setAttribute('contenteditable', 'true');
existingContent.textContent = this.card.content;
existingContent.addClass('is-editing');
setTimeout(() => {
existingContent.focus();
const range = document.createRange();
const sel = window.getSelection();
range.selectNodeContents(existingContent);
range.collapse(false);
sel?.removeAllRanges();
sel?.addRange(range);
}, 0);
existingContent.addEventListener('focusin', () => {
// @ts-ignore
this.app.keymap.pushScope(this.scope);
// @ts-ignore
this.app.workspace.activeEditor = this.owner;
});
existingContent.addEventListener('blur', () => {
// @ts-ignore
this.app.keymap.popScope(this.scope);
// @ts-ignore
if (this.app.workspace.activeEditor === this.owner) {
// @ts-ignore
this.app.workspace.activeEditor = null;
}
if (this.isEditing) {
void (async () => {
const newContent = existingContent.textContent || '';
if (newContent !== this.card.content) {
await this.store.update(this.card.id, { content: newContent });
}
this.isEditing = false;
if (CardComponent.activeEditor === this) {
CardComponent.activeEditor = null;
}
void this.render();
})();
}
});
existingContent.addEventListener('keydown', (e) => {
if (handleKeyWrap(e, existingContent, this.editor, (this.plugin as any).settings?.autoPairBrackets !== false)) {
e.preventDefault(); e.stopPropagation(); return;
}
if (this.applyFormattingHotkey(e, existingContent)) return;
const settings = this.store.settings;
const normalizeKey = (v: string) => String(v || '').toLowerCase().replace(/[\s+_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
const saveKey = normalizeKey(settings.saveKey || 'enter');
let pressed = '';
if (e.ctrlKey) pressed += 'ctrl-';
if (e.shiftKey) pressed += 'shift-';
if (e.altKey) pressed += 'alt-';
if (e.key && e.key.toLowerCase() === 'enter') pressed += 'enter';
if (pressed === saveKey) { e.preventDefault(); existingContent.blur(); }
});
return;
}
// No existing content div — fall through to full render
}
this.el.empty();
this.el.dataset.id = this.card.id;
this.el.draggable = false;
this.el.draggable = true;
// Apply styling
const statusDef = this.card.status
@ -304,6 +371,7 @@ export class CardComponent {
if (this.card.expiresAt && this.store.settings.showExpiryTimeLeft) {
const pill = container.createDiv('sc-expiry-pill');
pill.textContent = this.formatExpiryTimeLeft(this.card.expiresAt);
if (this.card.status) pill.style.marginBottom = '4px';
}
if (this.card.status) {
const pill = container.createDiv('sc-status-pill');
@ -523,10 +591,14 @@ export class CardComponent {
private renderFooter(container: HTMLElement): void {
const settings = this.store.settings;
const hasTags = this.card.tags && this.card.tags.length > 0;
const groupTags = this.settingsOverride?.groupTags ?? settings.groupTags;
const showTimestamps = this.settingsOverride?.showTimestamps ?? settings.showTimestamps;
// showTags override: if explicitly set use it, otherwise tags are always shown
const showTags = this.settingsOverride?.showTags ?? true;
const hasTags = showTags && this.card.tags && this.card.tags.length > 0;
if (settings.groupTags) {
if (settings.showTimestamps && settings.timestampBelowTags) {
if (groupTags) {
if (showTimestamps && settings.timestampBelowTags) {
// "above tags" mode — render timestamp first
const ts = container.createDiv('sc-timestamp sc-timestamp--block');
ts.textContent = this.formatTimestamp(this.card.created);
@ -546,20 +618,29 @@ export class CardComponent {
});
}
if (settings.showTimestamps && !settings.timestampBelowTags) {
if (showTimestamps && !settings.timestampBelowTags) {
// default: inline after tags
const ts = container.createDiv(`sc-timestamp ${hasTags ? 'sc-timestamp--inline-spaced' : 'sc-timestamp--inline'}`);
ts.textContent = this.formatTimestamp(this.card.created);
}
} else {
// Tags already rendered above footer in render()
if (settings.showTimestamps) {
if (showTimestamps) {
container.createDiv('sc-timestamp').textContent = this.formatTimestamp(this.card.created);
}
}
}
private setupListeners(): void {
// Drag card content into an editor
this.el.addEventListener('dragstart', (e: DragEvent) => {
if (!e.dataTransfer) return;
e.dataTransfer.effectAllowed = 'copyMove';
const payload = JSON.stringify({ id: this.card.id, content: this.card.content });
e.dataTransfer.setData('text/x-card-sidebar', payload);
try { e.dataTransfer.setData('text/plain', this.card.content); } catch { /* some browsers restrict */ }
});
// Enter edit mode on click
this.el.addEventListener('click', (e) => {
if (this.ignoreNextClick) {
@ -758,7 +839,11 @@ export class CardComponent {
const unbind = this.store.eventBus.on('card:updated', (updated: Card) => {
if (updated.id === this.card.id) {
this.card = updated;
void this.render();
// Don't re-render while the user is actively editing — it would
// race with the in-flight render and remove the contenteditable div
if (!this.isEditing) {
void this.render();
}
}
});
this.unsubscribe.push(unbind);

View file

@ -120,7 +120,7 @@
}
.sc-search-input::placeholder {
color: var(--text-muted) !important;
color: var(--text-muted);
}
.sc-search-input:focus {
@ -130,11 +130,6 @@
box-shadow: 0 0 0 2px rgba(var(--interactive-accent), 0.1);
}
.sc-search-input::placeholder {
color: var(--text-muted);
opacity: 0.5;
}
.sc-sidebar-input {
width: 100%;
min-height: 36px;
@ -273,7 +268,7 @@
padding: 8px 12px;
background-color: var(--background-modifier-form-field);
border-radius: 6px;
cursor: pointer;
cursor: grab;
transition: background-color 0.1s ease;
}
@ -281,6 +276,10 @@
background-color: var(--background-modifier-hover);
}
.sc-home-file-list:not(.pinned) .sc-home-file-item {
cursor: pointer;
}
.sc-home-file-icon {
color: var(--text-muted);
display: flex;
@ -326,7 +325,7 @@
align-items: center;
background-color: var(--background-modifier-form-field);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
border-radius: var(--input-radius);
padding: 0 8px;
}
@ -564,7 +563,9 @@
.sc-expiry-pill {
font-size: small;
color: var(--text-muted);
color: var(--text-normal);
opacity: 0.7;
margin-bottom: -2px;
}
@ -902,7 +903,7 @@ de
/* Timestamp display variants */
.sc-timestamp--block {
display: block;
margin-bottom: 4px;
margin-bottom: 3px;
}
.sc-timestamp--inline {
@ -1120,9 +1121,11 @@ de
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--background-modifier-border);
border: none;
border-radius: 4px;
cursor: pointer;
background: none;
box-shadow: none;
}
/* Icon btn plus state */