Updated Readme + bug fixes
171
README.md
|
|
@ -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
|
||||
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>
|
||||

|
||||
|
||||
## Features
|
||||
### Drag to Editor
|
||||
Drag any card into your markdown editor to insert its content.
|
||||
|
||||
### **Core Card System:**
|
||||

|
||||
|
||||
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" />
|
||||

|
||||
|
||||
|
||||
### Auto-color Rules
|
||||
Automatically assign colors based on text content or tag matches.
|
||||

|
||||
|
||||
- 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!
|
||||

|
||||
|
||||
# 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 they’ll 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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
### 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.
|
||||
|
||||

|
||||
|
||||
## Homepage
|
||||
|
||||
### Dedicated Homepage View
|
||||
A full-page view with a card editor, pinned notes, and recent notes.
|
||||
|
||||

|
||||
|
||||
You can replace your default new tab with this homepage.
|
||||
|
||||

|
||||
|
||||
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 vault’s `.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
|
After Width: | Height: | Size: 434 KiB |
BIN
assets/card-expiry.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
assets/drag-into-editor.gif
Normal file
|
After Width: | Height: | Size: 595 KiB |
BIN
assets/inline-editing.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
assets/pinned-recent-notes.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
assets/settings-autocolor.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
BIN
assets/settings-card-styling.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
BIN
assets/settings-categories.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
assets/status-menu.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/two-row-color-dots.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
578
main.js
|
|
@ -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();
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ export function handleKeyWrap(event: KeyboardEvent, editorEl: HTMLElement, edito
|
|||
"%": ["%", "%"],
|
||||
"=": ["=", "="],
|
||||
'"': ['"', '"'],
|
||||
"'": ["'", "'"],
|
||||
};
|
||||
|
||||
const pair = wrapMap[key];
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
25
styles.css
|
|
@ -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 */
|
||||
|
|
|
|||