mirror of
https://github.com/kazi-aidah/sidecards.git
synced 2026-07-22 06:42:13 +00:00
New Homepage
This commit is contained in:
parent
e6b59afe1d
commit
6e37a33808
18 changed files with 4383 additions and 5729 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
|||
.adstar
|
||||
data.json
|
||||
compare/
|
||||
compare/
|
||||
node_modules/
|
||||
12
s-styles.css
12
s-styles.css
|
|
@ -1,3 +1,5 @@
|
|||
|
||||
|
||||
.card-sidebar-container {
|
||||
padding: 0;
|
||||
background: var(--background-secondary);
|
||||
|
|
@ -561,7 +563,6 @@
|
|||
color: var(--text-muted) !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
/*
|
||||
.card-sidebar-cards-container {
|
||||
gap: 8px;
|
||||
}
|
||||
|
|
@ -588,7 +589,7 @@
|
|||
.vertical-card-mode .card-sidebar-card.has-pills, .card-sidebar-card:has(.card-tags) {
|
||||
padding-bottom: 12px !important;
|
||||
padding-top: 12px !important;
|
||||
} */
|
||||
}
|
||||
.card-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
|
@ -733,13 +734,14 @@ padding-top: 12px !important;
|
|||
}
|
||||
|
||||
.sidecards-home-main {
|
||||
margin: 90px 40px;
|
||||
margin: 40px 40px;
|
||||
}
|
||||
|
||||
.sidecards-home-color-dot {
|
||||
border-radius: 50% !important;
|
||||
border-radius: 10px !important;
|
||||
}
|
||||
|
||||
.sidecards-home-toolbar {
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -45,8 +45,7 @@ export interface SideCardsSettings {
|
|||
filterColors?: Record<string, { bgColor?: string; textColor?: string }>;
|
||||
saveKey?: string;
|
||||
nextLineKey?: string;
|
||||
hideClearButton?: boolean;
|
||||
sortMode?: 'manual' | 'created' | 'alpha';
|
||||
sortMode?: 'manual' | 'created' | 'modified' | 'alpha' | 'status';
|
||||
sortAscending?: boolean;
|
||||
showPinnedOnly?: boolean;
|
||||
enableCardStatus?: boolean;
|
||||
|
|
@ -67,11 +66,8 @@ export interface SideCardsSettings {
|
|||
inheritStatusColor?: boolean;
|
||||
statusPillOpacity?: number;
|
||||
replaceHomepageWithSidecards?: boolean;
|
||||
|
||||
// From act.js or similar
|
||||
enableRegexSupport: boolean;
|
||||
caseSensitive: boolean;
|
||||
partialMatch: boolean;
|
||||
searchBarVisible?: boolean;
|
||||
pinnedNotes?: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: SideCardsSettings = {
|
||||
|
|
@ -110,7 +106,6 @@ export const DEFAULT_SETTINGS: SideCardsSettings = {
|
|||
filterColors: {},
|
||||
saveKey: 'enter',
|
||||
nextLineKey: 'shift-enter',
|
||||
hideClearButton: false,
|
||||
sortMode: 'manual',
|
||||
sortAscending: true,
|
||||
showPinnedOnly: false,
|
||||
|
|
@ -132,7 +127,6 @@ export const DEFAULT_SETTINGS: SideCardsSettings = {
|
|||
inheritStatusColor: false,
|
||||
statusPillOpacity: 1,
|
||||
replaceHomepageWithSidecards: false,
|
||||
enableRegexSupport: false,
|
||||
caseSensitive: false,
|
||||
partialMatch: true
|
||||
searchBarVisible: false,
|
||||
pinnedNotes: [],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export class Card {
|
|||
tags: this.tags,
|
||||
category: this.category,
|
||||
created: this.created,
|
||||
modified: this.modified,
|
||||
archived: this.archived,
|
||||
pinned: this.pinned,
|
||||
notePath: this.notePath,
|
||||
|
|
|
|||
|
|
@ -34,6 +34,42 @@ export class CardStore {
|
|||
});
|
||||
}
|
||||
|
||||
private parseColorIndex(value: string | number | null | undefined): number | null {
|
||||
if (typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 10) {
|
||||
return value;
|
||||
}
|
||||
const raw = String(value ?? '').trim();
|
||||
if (!raw) return null;
|
||||
if (/^\d+$/.test(raw)) {
|
||||
const parsed = Number(raw);
|
||||
if (parsed >= 1 && parsed <= 10) return parsed;
|
||||
}
|
||||
const varMatch = raw.match(/var\(\s*--card-color-?(\d+)\s*\)/i);
|
||||
if (varMatch) {
|
||||
const parsed = Number(varMatch[1]);
|
||||
if (parsed >= 1 && parsed <= 10) return parsed;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private normalizeCardColorValue(value: string | number | null | undefined, fallback: string): { color: string; frontmatterColor: number | null } {
|
||||
const idx = this.parseColorIndex(value);
|
||||
if (idx !== null) {
|
||||
return { color: `var(--card-color-${idx})`, frontmatterColor: idx };
|
||||
}
|
||||
const raw = String(value ?? '').trim();
|
||||
if (raw) {
|
||||
return { color: raw, frontmatterColor: null };
|
||||
}
|
||||
return { color: fallback, frontmatterColor: this.parseColorIndex(fallback) };
|
||||
}
|
||||
|
||||
private getFrontmatterColorValueForCard(color: string): string | number {
|
||||
const idx = this.parseColorIndex(color);
|
||||
if (idx !== null) return idx;
|
||||
return color;
|
||||
}
|
||||
|
||||
getAll(): Card[] {
|
||||
return Array.from(this.cards.values());
|
||||
}
|
||||
|
|
@ -106,7 +142,7 @@ export class CardStore {
|
|||
const frontmatter = [
|
||||
'---',
|
||||
tagsYaml,
|
||||
`card-color: ${card.color}`,
|
||||
`card-color: ${this.getFrontmatterColorValueForCard(card.color)}`,
|
||||
`Created-Date: ${new Date(card.created).toISOString()}`,
|
||||
card.archived ? 'Archived: true' : '',
|
||||
card.pinned ? 'Pinned: true' : '',
|
||||
|
|
@ -217,6 +253,8 @@ export class CardStore {
|
|||
const expiresMatch = fm.match(/^\s*expires-at\s*:\s*(.+)$/im);
|
||||
const statusMatch = fm.match(/^\s*status\s*:\s*(.+)$/im);
|
||||
const colorMatch = fm.match(/^\s*card-color\s*:\s*(.+)$/im);
|
||||
const colorRaw = colorMatch ? colorMatch[1].trim() : null;
|
||||
const normalizedColor = this.normalizeCardColorValue(colorRaw, card.color);
|
||||
|
||||
await this.update(card.id, {
|
||||
tags,
|
||||
|
|
@ -225,9 +263,15 @@ export class CardStore {
|
|||
category: categoryMatch ? categoryMatch[1].trim() : null,
|
||||
expiresAt: expiresMatch ? new Date(expiresMatch[1].trim()).getTime() : null,
|
||||
status: statusMatch ? { name: statusMatch[1].trim(), color: card.status?.color || '', textColor: card.status?.textColor || '#000' } : null,
|
||||
color: colorMatch ? colorMatch[1].trim() : card.color,
|
||||
color: normalizedColor.color,
|
||||
content: content.replace(fmMatch[0], '').trim()
|
||||
});
|
||||
if (colorRaw && normalizedColor.frontmatterColor !== null && colorRaw !== String(normalizedColor.frontmatterColor)) {
|
||||
const normalizedText = updateFrontmatter(content, 'card-color', normalizedColor.frontmatterColor);
|
||||
if (normalizedText !== content) {
|
||||
await this.app.vault.modify(file, normalizedText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await this.update(card.id, { content: content.trim() });
|
||||
}
|
||||
|
|
@ -337,11 +381,40 @@ export class CardStore {
|
|||
text = updateFrontmatter(text, 'Expires-At', updates.expiresAt ? new Date(updates.expiresAt).toISOString() : null);
|
||||
}
|
||||
if (typeof updates.color !== 'undefined') {
|
||||
text = updateFrontmatter(text, 'card-color', updates.color || null);
|
||||
const normalizedColor = this.normalizeCardColorValue(updates.color as any, card.color);
|
||||
text = updateFrontmatter(text, 'card-color', normalizedColor.frontmatterColor ?? normalizedColor.color);
|
||||
}
|
||||
if (typeof updates.status !== 'undefined') {
|
||||
text = updateFrontmatter(text, 'Status', updates.status?.name || null);
|
||||
}
|
||||
await this.app.vault.modify(file, text);
|
||||
}
|
||||
|
||||
async migrateCardColorFrontmatterFormat(): Promise<void> {
|
||||
const seen = new Set<string>();
|
||||
for (const card of this.getAll()) {
|
||||
if (!card.notePath || seen.has(card.notePath)) continue;
|
||||
seen.add(card.notePath);
|
||||
const file = this.app.vault.getAbstractFileByPath(card.notePath);
|
||||
if (!(file instanceof TFile)) continue;
|
||||
let text = '';
|
||||
try {
|
||||
text = await this.app.vault.read(file);
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
const fmMatch = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
||||
if (!fmMatch) continue;
|
||||
const colorMatch = fmMatch[1].match(/^\s*card-color\s*:\s*(.+)$/im);
|
||||
if (!colorMatch) continue;
|
||||
const raw = colorMatch[1].trim();
|
||||
const idx = this.parseColorIndex(raw);
|
||||
if (idx === null) continue;
|
||||
if (raw === String(idx)) continue;
|
||||
const updated = updateFrontmatter(text, 'card-color', idx);
|
||||
if (updated !== text) {
|
||||
await this.app.vault.modify(file, updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,17 +14,25 @@ export class SortService {
|
|||
case 'manual':
|
||||
return this.sortManual(sorted, ascending);
|
||||
case 'created':
|
||||
return sorted.sort((a, b) =>
|
||||
ascending
|
||||
? a.created - b.created
|
||||
: b.created - a.created
|
||||
);
|
||||
return sorted.sort((a, b) => {
|
||||
const primary = ascending ? a.created - b.created : b.created - a.created;
|
||||
if (primary !== 0) return primary;
|
||||
const am = typeof a.modified === 'number' ? a.modified : a.created;
|
||||
const bm = typeof b.modified === 'number' ? b.modified : b.created;
|
||||
const secondary = ascending ? am - bm : bm - am;
|
||||
if (secondary !== 0) return secondary;
|
||||
return ascending ? a.content.localeCompare(b.content) : b.content.localeCompare(a.content);
|
||||
});
|
||||
case 'modified':
|
||||
return sorted.sort((a, b) =>
|
||||
ascending
|
||||
? (a as any).modified - (b as any).modified
|
||||
: (b as any).modified - (a as any).modified
|
||||
);
|
||||
return sorted.sort((a, b) => {
|
||||
const am = typeof a.modified === 'number' ? a.modified : a.created;
|
||||
const bm = typeof b.modified === 'number' ? b.modified : b.created;
|
||||
const primary = ascending ? am - bm : bm - am;
|
||||
if (primary !== 0) return primary;
|
||||
const secondary = ascending ? a.created - b.created : b.created - a.created;
|
||||
if (secondary !== 0) return secondary;
|
||||
return ascending ? a.content.localeCompare(b.content) : b.content.localeCompare(a.content);
|
||||
});
|
||||
case 'alpha':
|
||||
return sorted.sort((a, b) =>
|
||||
ascending
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export async function flipAnimateAsync(
|
|||
const stagger = opts.stagger ?? 20;
|
||||
const entranceOffset = opts.offset ?? 28;
|
||||
|
||||
const oldEls = Array.from(container.querySelectorAll('.card-sidebar-card')) as HTMLElement[];
|
||||
const oldEls = Array.from(container.querySelectorAll('.sc-card')) as HTMLElement[];
|
||||
const oldMap = new Map<string, DOMRect>();
|
||||
oldEls.forEach(el => {
|
||||
const id = el.dataset.id;
|
||||
|
|
@ -35,7 +35,7 @@ export async function flipAnimateAsync(
|
|||
|
||||
await asyncDomChange();
|
||||
|
||||
const newEls = Array.from(container.querySelectorAll('.card-sidebar-card')) as HTMLElement[];
|
||||
const newEls = Array.from(container.querySelectorAll('.sc-card')) as HTMLElement[];
|
||||
const newMap = new Map<string, DOMRect>();
|
||||
const elById = new Map<string, HTMLElement>();
|
||||
newEls.forEach(el => {
|
||||
|
|
@ -63,7 +63,9 @@ export async function flipAnimateAsync(
|
|||
el.style.transition = 'none';
|
||||
el.style.transform = `translateY(${entranceOffset}px)`;
|
||||
el.style.willChange = 'transform';
|
||||
el.style.opacity = '0';
|
||||
if (!settings.disableCardFadeIn) {
|
||||
el.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -77,7 +79,9 @@ export async function flipAnimateAsync(
|
|||
setTimeout(() => {
|
||||
el.style.transition = `transform ${duration}ms ease-out, opacity ${duration}ms ease-out`;
|
||||
el.style.transform = '';
|
||||
el.style.opacity = '1';
|
||||
if (!settings.disableCardFadeIn) {
|
||||
el.style.opacity = '1';
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
|
||||
|
|
@ -86,8 +90,6 @@ export async function flipAnimateAsync(
|
|||
const el = elById.get(id);
|
||||
if (el) {
|
||||
el.style.transition = '';
|
||||
el.style.willChange = '';
|
||||
el.style.transform = '';
|
||||
}
|
||||
});
|
||||
}, duration + (ids.length * stagger) + 50);
|
||||
|
|
@ -101,7 +103,7 @@ export function animateCardsEntrance(
|
|||
if (!settings.animatedCards) return;
|
||||
if (window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
|
||||
const els = Array.from(container.querySelectorAll('.card-sidebar-card'))
|
||||
const els = Array.from(container.querySelectorAll('.sc-card'))
|
||||
.filter(el => (el as HTMLElement).style.display !== 'none') as HTMLElement[];
|
||||
if (els.length === 0) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -42,26 +42,35 @@ export function resolveColorVarToHex(colorVar: string, settings: any): string |
|
|||
}
|
||||
|
||||
export function applyCardColorToElement(cardEl: HTMLElement, colorVar: string, settings: any): void {
|
||||
const style = settings.cardStyle ?? 2;
|
||||
const opacity = settings.cardBgOpacity ?? 0.08;
|
||||
const style = Number(settings.cardStyle ?? 2);
|
||||
const opacity = Number(settings.cardBgOpacity ?? 0.08);
|
||||
const borderThickness = Number(settings.borderThickness ?? 2);
|
||||
|
||||
cardEl.style.borderLeft = '';
|
||||
cardEl.style.border = '';
|
||||
cardEl.style.backgroundColor = '';
|
||||
cardEl.style.boxShadow = '';
|
||||
// Reset styles using setProperty to ensure consistency
|
||||
cardEl.style.removeProperty('border-left');
|
||||
cardEl.style.removeProperty('border');
|
||||
cardEl.style.removeProperty('background-color');
|
||||
cardEl.style.removeProperty('box-shadow');
|
||||
|
||||
const hex = resolveColorVarToHex(colorVar, settings) || colorVar;
|
||||
const rgba = hexToRgba(hex, opacity);
|
||||
const borderRadius = settings.borderRadius ?? 6;
|
||||
|
||||
if (style === 1) {
|
||||
cardEl.style.border = `${borderThickness}px solid ${colorVar}`;
|
||||
cardEl.style.backgroundColor = hexToRgba(hex, opacity);
|
||||
cardEl.style.setProperty('border', `${borderThickness}px solid ${colorVar}`, 'important');
|
||||
cardEl.style.setProperty('background-color', rgba, 'important');
|
||||
} else if (style === 3) {
|
||||
cardEl.style.borderLeft = `4px solid ${colorVar}`;
|
||||
cardEl.style.backgroundColor = hexToRgba(hex, opacity);
|
||||
cardEl.style.setProperty('border-left', `${borderThickness}px solid ${colorVar}`, 'important');
|
||||
cardEl.style.setProperty('background-color', rgba, 'important');
|
||||
cardEl.style.setProperty('border-top', `1px solid var(--background-modifier-border)`, 'important');
|
||||
cardEl.style.setProperty('border-right', `1px solid var(--background-modifier-border)`, 'important');
|
||||
cardEl.style.setProperty('border-bottom', `1px solid var(--background-modifier-border)`, 'important');
|
||||
} else {
|
||||
cardEl.style.border = `${borderThickness}px solid ${colorVar}`;
|
||||
cardEl.style.backgroundColor = hexToRgba(hex, opacity);
|
||||
cardEl.style.boxShadow = `2px 2px 0 0 ${colorVar}`;
|
||||
// Style 2 (Default)
|
||||
cardEl.style.setProperty('border', `${borderThickness}px solid ${colorVar}`, 'important');
|
||||
cardEl.style.setProperty('background-color', rgba, 'important');
|
||||
cardEl.style.setProperty('box-shadow', `2px 2px 0 0 ${colorVar}`, 'important');
|
||||
}
|
||||
|
||||
cardEl.style.setProperty('border-radius', `${borderRadius}px`, 'important');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,15 +42,38 @@ export function updateFrontmatter(content: string, key: string, value: any): str
|
|||
}
|
||||
|
||||
export function parseTagsFromFrontmatter(fm: string): string[] {
|
||||
const match = fm.match(/tags:\s*\[(.*?)\]/i) || fm.match(/tags:\s*(.*)/i);
|
||||
if (!match) return [];
|
||||
const raw = match[1];
|
||||
if (raw.startsWith('[')) {
|
||||
const lines = fm.split(/\r?\n/);
|
||||
const tagsLine = lines.find(l => /^\s*tags\s*:/i.test(l));
|
||||
if (!tagsLine) return [];
|
||||
|
||||
const content = tagsLine.replace(/^\s*tags\s*:/i, '').trim();
|
||||
const sanitize = (t: string) => t.trim().replace(/^[-#\s]+/, '').replace(/^- /g, '').trim();
|
||||
|
||||
if (!content) {
|
||||
// Check if it's a list format below
|
||||
const tagsIdx = lines.findIndex(l => /^\s*tags\s*:/i.test(l));
|
||||
const listTags: string[] = [];
|
||||
for (let i = tagsIdx + 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
const match = line.match(/^\s*-\s+(.*)$/);
|
||||
if (match) {
|
||||
listTags.push(sanitize(match[1]));
|
||||
} else if (line.trim() && !line.startsWith(' ')) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return listTags;
|
||||
}
|
||||
|
||||
if (content.startsWith('[')) {
|
||||
try {
|
||||
return JSON.parse(raw.replace(/'/g, '"'));
|
||||
const parsed = JSON.parse(content.replace(/'/g, '"'));
|
||||
return Array.isArray(parsed) ? parsed.map(sanitize) : [sanitize(String(parsed))];
|
||||
} catch (e) {
|
||||
return raw.replace(/[\[\]"']/g, '').split(',').map(t => t.trim()).filter(Boolean);
|
||||
return content.replace(/[\[\]"']/g, '').split(',').map(sanitize).filter(Boolean);
|
||||
}
|
||||
}
|
||||
return raw.split(',').map(t => t.trim()).filter(Boolean);
|
||||
|
||||
// Single tag or comma-separated
|
||||
return content.split(',').map(sanitize).filter(Boolean);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { ItemView, WorkspaceLeaf, App, Notice, Menu, Modal, MarkdownView, TFile, setIcon } from "obsidian";
|
||||
import { ItemView, WorkspaceLeaf, App, Notice, Menu, Modal, MarkdownView, TFile, setIcon, Scope, Editor } from "obsidian";
|
||||
import { CardStore } from "../services/CardStore";
|
||||
import { FilterService, FilterOptions } from "../services/FilterService";
|
||||
import { SortService, SortMode } from "../services/SortService";
|
||||
|
|
@ -19,6 +19,9 @@ export class CardSidebarView extends ItemView {
|
|||
private activeFilters: FilterOptions = { query: '', tags: [] };
|
||||
private currentSortMode: SortMode = 'manual';
|
||||
private sortAscending: boolean = true;
|
||||
private editorScope: Scope;
|
||||
private editor!: Editor;
|
||||
private owner!: any;
|
||||
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
|
|
@ -29,6 +32,66 @@ export class CardSidebarView extends ItemView {
|
|||
private sortService: SortService
|
||||
) {
|
||||
super(leaf);
|
||||
this.editorScope = new Scope(this.app.scope);
|
||||
// Block native formatting keys so Obsidian's global commands take over
|
||||
this.editorScope.register(["Mod"], "b", () => true);
|
||||
this.editorScope.register(["Mod"], "i", () => true);
|
||||
this.editorScope.register(["Mod"], "u", () => true);
|
||||
this.setupMockEditor();
|
||||
}
|
||||
|
||||
private setupMockEditor() {
|
||||
this.editor = {
|
||||
getSelection: () => {
|
||||
const sel = window.getSelection();
|
||||
return sel ? sel.toString() : "";
|
||||
},
|
||||
replaceSelection: (text: string) => {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
},
|
||||
toggleBold: () => this.toggleMarkdownWrapper("**"),
|
||||
toggleItalic: () => this.toggleMarkdownWrapper("*"),
|
||||
} as any;
|
||||
|
||||
this.owner = {
|
||||
editor: this.editor,
|
||||
editMode: true,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleMarkdownWrapper(wrapper: "**" | "*" | "~~" | "==") {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
const selectedText = sel.toString();
|
||||
|
||||
if (selectedText.length === 0) {
|
||||
// Empty selection: insert **** and place cursor in middle
|
||||
const text = wrapper + wrapper;
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStart(node, wrapper.length);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
} else {
|
||||
const alreadyWrapped = selectedText.startsWith(wrapper) && selectedText.endsWith(wrapper);
|
||||
const newText = alreadyWrapped
|
||||
? selectedText.slice(wrapper.length, -wrapper.length)
|
||||
: wrapper + selectedText + wrapper;
|
||||
|
||||
this.editor.replaceSelection(newText);
|
||||
}
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
|
|
@ -53,9 +116,13 @@ export class CardSidebarView extends ItemView {
|
|||
async onOpen(): Promise<void> {
|
||||
const container = this.containerEl;
|
||||
container.empty();
|
||||
container.addClass('card-sidebar-container');
|
||||
container.addClass('sc-sidebar-container');
|
||||
this.currentSortMode = ((this.plugin as any).settings.sortMode || 'manual') as SortMode;
|
||||
this.sortAscending = typeof (this.plugin as any).settings.sortAscending === 'boolean'
|
||||
? !!(this.plugin as any).settings.sortAscending
|
||||
: true;
|
||||
|
||||
const mainContainer = container.createDiv('card-sidebar-main');
|
||||
const mainContainer = container.createDiv('sc-sidebar-main');
|
||||
mainContainer.style.display = 'flex';
|
||||
mainContainer.style.flexDirection = 'column';
|
||||
mainContainer.style.height = '100%';
|
||||
|
|
@ -63,7 +130,7 @@ export class CardSidebarView extends ItemView {
|
|||
this.createHeader(mainContainer);
|
||||
this.createSearchBar(mainContainer);
|
||||
|
||||
this.cardsContainer = mainContainer.createDiv('card-sidebar-cards-container');
|
||||
this.cardsContainer = mainContainer.createDiv('sc-sidebar-cards-container');
|
||||
this.cardsContainer.style.flex = '1';
|
||||
this.cardsContainer.style.overflow = 'auto';
|
||||
this.cardsContainer.style.position = 'relative';
|
||||
|
|
@ -79,21 +146,24 @@ export class CardSidebarView extends ItemView {
|
|||
this.registerVaultEvents();
|
||||
|
||||
this.showLoadingOverlay();
|
||||
await this.renderCards();
|
||||
this.hideLoadingOverlay(300);
|
||||
try {
|
||||
await this.renderCards();
|
||||
} finally {
|
||||
this.hideLoadingOverlay(300);
|
||||
}
|
||||
}
|
||||
|
||||
private showLoadingOverlay(maxMs = 2000) {
|
||||
const parent = this.cardsContainer || this.containerEl;
|
||||
if (!parent || this._loadingEl) return;
|
||||
|
||||
this._loadingEl = parent.createDiv('card-sidebar-loading');
|
||||
this._loadingEl = parent.createDiv('sc-sidebar-loading');
|
||||
this._loadingEl.style.position = 'absolute';
|
||||
this._loadingEl.style.inset = '0';
|
||||
this._loadingEl.style.display = 'flex';
|
||||
this._loadingEl.style.alignItems = 'center';
|
||||
this._loadingEl.style.justifyContent = 'center';
|
||||
this._loadingEl.style.background = 'var(--background-modifier-card, rgba(0,0,0,0.02))';
|
||||
this._loadingEl.style.background = 'var(--background-primary)';
|
||||
this._loadingEl.style.zIndex = '9999';
|
||||
|
||||
const box = this._loadingEl.createDiv();
|
||||
|
|
@ -102,7 +172,7 @@ export class CardSidebarView extends ItemView {
|
|||
box.style.alignItems = 'center';
|
||||
box.style.gap = '8px';
|
||||
|
||||
const spinner = box.createDiv('card-sidebar-spinner');
|
||||
const spinner = box.createDiv('sc-sidebar-spinner');
|
||||
spinner.style.width = '36px';
|
||||
spinner.style.height = '36px';
|
||||
spinner.style.border = '4px solid var(--background-modifier-border)';
|
||||
|
|
@ -111,7 +181,7 @@ export class CardSidebarView extends ItemView {
|
|||
// Inline animation if not in CSS
|
||||
spinner.animate([{ transform: 'rotate(0deg)' }, { transform: 'rotate(360deg)' }], { duration: 800, iterations: Infinity });
|
||||
|
||||
box.createDiv({ text: 'Loading cards...', cls: 'card-sidebar-loading-text' });
|
||||
box.createDiv({ text: 'Loading cards...', cls: 'sc-sidebar-loading-text' });
|
||||
|
||||
this._loadingTimeout = setTimeout(() => this.hideLoadingOverlay(), maxMs);
|
||||
}
|
||||
|
|
@ -178,42 +248,88 @@ export class CardSidebarView extends ItemView {
|
|||
}
|
||||
|
||||
private createHeader(container: HTMLElement): void {
|
||||
const header = container.createDiv('card-sidebar-header');
|
||||
const filterGroup = header.createDiv('category-group');
|
||||
filterGroup.addClass('card-sidebar-category-group');
|
||||
if (this.plugin.settings.disableFilterButtons) return;
|
||||
|
||||
const header = container.createDiv('sc-sidebar-header');
|
||||
const filterGroup = header.createDiv('sc-category-group');
|
||||
filterGroup.style.display = 'flex';
|
||||
filterGroup.style.gap = '8px';
|
||||
filterGroup.style.overflowX = 'auto';
|
||||
filterGroup.style.flexWrap = 'nowrap';
|
||||
filterGroup.style.whiteSpace = 'nowrap';
|
||||
|
||||
const showTimeBasedChips = !this.plugin.settings.disableTimeBasedFiltering;
|
||||
const settings = this.plugin.settings;
|
||||
const cats: Array<{ id: string; label: string; showInMenu?: boolean }> =
|
||||
Array.isArray(this.plugin.settings?.customCategories) ? this.plugin.settings.customCategories : [];
|
||||
Array.isArray(settings.customCategories) ? settings.customCategories : [];
|
||||
|
||||
const chips: Array<{ type: string, label: string, value: string }> = [];
|
||||
chips.push({ type: 'all', label: 'All', value: 'all' });
|
||||
if (showTimeBasedChips) {
|
||||
chips.push({ type: 'category', label: 'Today', value: 'today' });
|
||||
chips.push({ type: 'category', label: 'Tomorrow', value: 'tomorrow' });
|
||||
}
|
||||
if (this.plugin.settings?.enableCustomCategories) {
|
||||
cats.forEach((c) => {
|
||||
if (!c || c.showInMenu === false) return;
|
||||
chips.push({ type: 'category', label: c.label || '', value: c.id || c.label || '' });
|
||||
});
|
||||
}
|
||||
if (!(this.plugin as any).settings?.hideArchivedFilterButton) {
|
||||
chips.push({ type: 'archived', label: 'Archived', value: 'archived' });
|
||||
// Build ordered chip list using allItemsOrder if available
|
||||
const defaultOrder = ['filter-all']
|
||||
.concat(!settings.disableTimeBasedFiltering ? ['filter-today', '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 chips: Array<{ type: string; label: string; value: string }> = [];
|
||||
|
||||
combinedOrder.forEach(itemId => {
|
||||
if (!itemId) return;
|
||||
if (itemId === 'filter-all') {
|
||||
chips.push({ type: 'all', label: 'All', value: 'all' });
|
||||
return;
|
||||
}
|
||||
if (itemId === 'filter-today') {
|
||||
if (!settings.disableTimeBasedFiltering) chips.push({ type: 'category', label: 'Today', value: 'today' });
|
||||
return;
|
||||
}
|
||||
if (itemId === 'filter-tomorrow') {
|
||||
if (!settings.disableTimeBasedFiltering) chips.push({ type: 'category', label: 'Tomorrow', value: 'tomorrow' });
|
||||
return;
|
||||
}
|
||||
if (itemId === 'filter-archived') {
|
||||
if (!settings.hideArchivedFilterButton) chips.push({ type: 'archived', label: 'Archived', value: 'archived' });
|
||||
return;
|
||||
}
|
||||
const cat = cats.find(c => String(c.id) === String(itemId));
|
||||
if (cat && cat.showInMenu !== false) {
|
||||
chips.push({ type: 'category', label: cat.label || '', value: cat.id || cat.label || '' });
|
||||
}
|
||||
});
|
||||
|
||||
// Ensure 'All' is always present
|
||||
if (!chips.find(c => c.value === 'all')) {
|
||||
chips.unshift({ type: 'all', label: 'All', value: 'all' });
|
||||
}
|
||||
|
||||
chips.forEach(chip => {
|
||||
const btn = filterGroup.createEl('button', { text: chip.label });
|
||||
btn.addClass('card-category-btn');
|
||||
btn.addClass('sc-category-btn');
|
||||
btn.dataset.filterType = chip.type;
|
||||
btn.dataset.filterValue = chip.value;
|
||||
|
||||
// Apply custom colors from filterColors settings
|
||||
const colorKey = chip.value === 'all' ? 'all'
|
||||
: chip.value === 'today' ? 'today'
|
||||
: chip.value === 'tomorrow' ? 'tomorrow'
|
||||
: chip.value === 'archived' ? 'archived'
|
||||
: chip.value;
|
||||
|
||||
const customColors = settings.filterColors?.[colorKey];
|
||||
if (customColors?.bgColor) btn.style.setProperty('background-color', customColors.bgColor, 'important');
|
||||
if (customColors?.textColor) btn.style.setProperty('color', customColors.textColor, 'important');
|
||||
|
||||
btn.addEventListener('click', async () => {
|
||||
filterGroup.querySelectorAll('.card-category-btn').forEach(b => b.removeClass('active'));
|
||||
filterGroup.querySelectorAll('.sc-category-btn').forEach(b => {
|
||||
(b as HTMLElement).removeClass('active');
|
||||
const bVal = (b as HTMLElement).dataset.filterValue || '';
|
||||
const bColors = settings.filterColors?.[bVal];
|
||||
if (bColors?.bgColor) (b as HTMLElement).style.setProperty('background-color', bColors.bgColor, 'important');
|
||||
else (b as HTMLElement).style.removeProperty('background-color');
|
||||
if (bColors?.textColor) (b as HTMLElement).style.setProperty('color', bColors.textColor, 'important');
|
||||
else (b as HTMLElement).style.removeProperty('color');
|
||||
});
|
||||
btn.addClass('active');
|
||||
if (chip.type === 'archived') {
|
||||
this.activeFilters.archivedOnly = true;
|
||||
|
|
@ -230,15 +346,32 @@ export class CardSidebarView extends ItemView {
|
|||
});
|
||||
}
|
||||
|
||||
private createSearchBar(container: HTMLElement): void {
|
||||
const wrapper = container.createDiv('card-search-wrap');
|
||||
const row = wrapper.createDiv('card-search-row');
|
||||
private applyScrollbarSetting(): void {
|
||||
if (!this.cardsContainer) return;
|
||||
if (this.plugin.settings.hideScrollbar) {
|
||||
this.cardsContainer.addClass('hide-scrollbar');
|
||||
// Also apply to parent if needed
|
||||
this.containerEl.querySelector('.view-content')?.addClass('hide-scrollbar');
|
||||
} else {
|
||||
this.cardsContainer.removeClass('hide-scrollbar');
|
||||
this.containerEl.querySelector('.view-content')?.removeClass('hide-scrollbar');
|
||||
}
|
||||
}
|
||||
|
||||
const iconSpan = row.createSpan({ cls: 'card-search-input-icon' });
|
||||
private createSearchBar(container: HTMLElement): void {
|
||||
const wrapper = container.createDiv('sc-search-wrap');
|
||||
if (this.plugin.settings.searchBarVisible) {
|
||||
wrapper.style.display = '';
|
||||
} else {
|
||||
wrapper.style.display = 'none';
|
||||
}
|
||||
const row = wrapper.createDiv('sc-search-row');
|
||||
|
||||
const iconSpan = row.createSpan({ cls: 'sc-search-input-icon' });
|
||||
try { setIcon(iconSpan as any, 'search'); } catch {}
|
||||
|
||||
const input = row.createEl('input', { type: 'search', placeholder: 'Search cards…', cls: 'card-search-input' });
|
||||
const clearBtn = row.createEl('button', { text: '✕', cls: 'card-search-clear-btn' });
|
||||
const input = row.createEl('input', { type: 'search', placeholder: 'Search cards…', cls: 'sc-search-input' });
|
||||
const clearBtn = row.createEl('button', { text: '✕', cls: 'sc-search-clear-btn' });
|
||||
clearBtn.style.display = 'none';
|
||||
clearBtn.title = 'Clear search';
|
||||
clearBtn.addEventListener('click', () => {
|
||||
|
|
@ -246,6 +379,7 @@ export class CardSidebarView extends ItemView {
|
|||
this.activeFilters.query = '';
|
||||
this.renderCardsDebounced();
|
||||
clearBtn.style.display = 'none';
|
||||
input.focus();
|
||||
});
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
|
|
@ -256,58 +390,95 @@ export class CardSidebarView extends ItemView {
|
|||
}
|
||||
|
||||
private createInputBox(container: HTMLElement): void {
|
||||
const inputContainer = container.createDiv('card-sidebar-input-container');
|
||||
const inputContainer = container.createDiv('sc-sidebar-input-container');
|
||||
inputContainer.style.padding = '8px';
|
||||
inputContainer.style.borderTop = '1px solid var(--background-modifier-border)';
|
||||
inputContainer.style.background = 'var(--background-primary)';
|
||||
inputContainer.style.position = 'sticky';
|
||||
inputContainer.style.bottom = '0';
|
||||
|
||||
const textarea = inputContainer.createEl('textarea');
|
||||
textarea.addClass('card-sidebar-input');
|
||||
textarea.placeholder = 'Type your idea here... (Use @category and #tag)';
|
||||
textarea.rows = 1;
|
||||
textarea.style.width = '100%';
|
||||
textarea.style.minHeight = '36px';
|
||||
textarea.style.maxHeight = '200px';
|
||||
textarea.style.padding = '8px';
|
||||
textarea.style.border = '1px solid var(--background-modifier-border)';
|
||||
textarea.style.borderRadius = '4px';
|
||||
textarea.style.resize = 'vertical';
|
||||
textarea.style.overflowY = 'hidden';
|
||||
const editorEl = inputContainer.createDiv({
|
||||
cls: 'sc-sidebar-input',
|
||||
});
|
||||
editorEl.setAttribute('contenteditable', 'true');
|
||||
editorEl.dataset.placeholder = 'Type here... (@category, #tag)';
|
||||
editorEl.style.width = '100%';
|
||||
editorEl.style.minHeight = '36px';
|
||||
editorEl.style.maxHeight = '200px';
|
||||
editorEl.style.padding = '8px';
|
||||
editorEl.style.border = '1px solid var(--background-modifier-border)';
|
||||
editorEl.style.overflowY = 'auto';
|
||||
editorEl.style.whiteSpace = 'pre-wrap';
|
||||
editorEl.style.position = 'relative';
|
||||
|
||||
// Simple placeholder logic for contenteditable
|
||||
const updatePlaceholder = () => {
|
||||
if (!editorEl.textContent?.trim()) {
|
||||
editorEl.addClass('is-empty');
|
||||
} else {
|
||||
editorEl.removeClass('is-empty');
|
||||
}
|
||||
};
|
||||
updatePlaceholder();
|
||||
editorEl.addEventListener('input', updatePlaceholder);
|
||||
|
||||
const autoResize = () => {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
// For contenteditable, it auto-resizes by default, but we might want to enforce max-height
|
||||
};
|
||||
textarea.addEventListener('input', autoResize);
|
||||
setTimeout(autoResize, 0);
|
||||
const tagAutocomplete = new TagAutocomplete(textarea, this.store);
|
||||
tagAutocomplete.attach();
|
||||
|
||||
editorEl.addEventListener('focusin', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.pushScope(this.editorScope);
|
||||
// @ts-ignore
|
||||
this.app.workspace.activeEditor = this.owner;
|
||||
});
|
||||
|
||||
const buttonContainer = inputContainer.createDiv('card-sidebar-button-container');
|
||||
editorEl.addEventListener('blur', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.popScope(this.editorScope);
|
||||
// @ts-ignore
|
||||
if (this.app.workspace.activeEditor === this.owner) {
|
||||
// @ts-ignore
|
||||
this.app.workspace.activeEditor = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Tag autocomplete logic for contenteditable (simplified version for now)
|
||||
editorEl.addEventListener('input', () => {
|
||||
// Simplified autocomplete trigger or use the TagAutocomplete class if compatible
|
||||
});
|
||||
|
||||
const buttonContainer = inputContainer.createDiv('sc-sidebar-button-container');
|
||||
buttonContainer.style.display = 'flex';
|
||||
buttonContainer.style.gap = '8px';
|
||||
buttonContainer.style.justifyContent = 'flex-end';
|
||||
buttonContainer.style.marginTop = '8px';
|
||||
|
||||
const searchBtn = buttonContainer.createEl('button');
|
||||
searchBtn.addClass('sc-icon-btn');
|
||||
try { setIcon(searchBtn, 'search'); } catch { searchBtn.textContent = 'Search'; }
|
||||
searchBtn.title = 'Toggle search';
|
||||
searchBtn.addEventListener('click', () => {
|
||||
const wrap = this.containerEl.querySelector('.card-search-wrap') as HTMLElement;
|
||||
if (wrap) wrap.style.display = wrap.style.display === 'none' ? '' : 'none';
|
||||
const wrap = this.containerEl.querySelector('.sc-search-wrap') as HTMLElement;
|
||||
if (wrap) {
|
||||
const isVisible = wrap.style.display !== 'none';
|
||||
wrap.style.display = isVisible ? 'none' : '';
|
||||
this.plugin.settings.searchBarVisible = !isVisible;
|
||||
this.plugin.saveSettings();
|
||||
}
|
||||
});
|
||||
|
||||
const reloadBtn = buttonContainer.createEl('button');
|
||||
reloadBtn.addClass('sc-icon-btn');
|
||||
reloadBtn.title = 'Reload cards';
|
||||
try { setIcon(reloadBtn, 'refresh-cw'); } catch { reloadBtn.textContent = 'Reload'; }
|
||||
reloadBtn.addEventListener('click', async () => {
|
||||
await this.renderCards();
|
||||
await this.renderCards(true);
|
||||
new Notice('Cards reloaded');
|
||||
});
|
||||
|
||||
const sortBtn = buttonContainer.createEl('button');
|
||||
sortBtn.addClass('sc-icon-btn');
|
||||
sortBtn.title = 'Sort';
|
||||
try { setIcon(sortBtn, 'sort-desc'); } catch { sortBtn.textContent = 'Sort'; }
|
||||
sortBtn.addEventListener('click', async (e) => {
|
||||
|
|
@ -345,6 +516,7 @@ export class CardSidebarView extends ItemView {
|
|||
});
|
||||
|
||||
const pinToggleBtn = buttonContainer.createEl('button');
|
||||
pinToggleBtn.addClass('sc-icon-btn');
|
||||
try { setIcon(pinToggleBtn, 'pin'); } catch { pinToggleBtn.textContent = 'Pin'; }
|
||||
pinToggleBtn.title = 'Show pinned only';
|
||||
pinToggleBtn.addEventListener('click', async () => {
|
||||
|
|
@ -353,6 +525,7 @@ export class CardSidebarView extends ItemView {
|
|||
});
|
||||
|
||||
const gridToggleBtn = buttonContainer.createEl('button');
|
||||
gridToggleBtn.addClass('sc-icon-btn');
|
||||
try { setIcon(gridToggleBtn, 'layout-grid'); } catch { gridToggleBtn.textContent = 'Grid'; }
|
||||
gridToggleBtn.title = 'Toggle grid layout';
|
||||
gridToggleBtn.addEventListener('click', async () => {
|
||||
|
|
@ -365,25 +538,36 @@ export class CardSidebarView extends ItemView {
|
|||
});
|
||||
|
||||
const addButton = buttonContainer.createEl('button');
|
||||
addButton.addClass('card-add-btn');
|
||||
addButton.addClass('sc-add-btn');
|
||||
addButton.textContent = 'Add Card';
|
||||
addButton.addEventListener('click', async () => {
|
||||
const content = textarea.value.trim();
|
||||
const content = editorEl.textContent?.trim();
|
||||
if (!content) return;
|
||||
const card = new Card({ content, category: this.activeFilters.category || undefined });
|
||||
await this.store.add(card);
|
||||
textarea.value = '';
|
||||
await this.renderCards();
|
||||
});
|
||||
|
||||
const clearButton = buttonContainer.createEl('button');
|
||||
clearButton.addClass('card-clear-btn');
|
||||
clearButton.textContent = 'Clear';
|
||||
clearButton.style.display = ((this.plugin as any).settings?.hideClearButton ? 'none' : '');
|
||||
clearButton.addEventListener('click', () => {
|
||||
textarea.value = '';
|
||||
textarea.focus();
|
||||
autoResize();
|
||||
// Extract tags (#tag) and categories (@category)
|
||||
const tags: string[] = [];
|
||||
const tagRegex = /#([^\s#@,.]+)/g;
|
||||
let match;
|
||||
while ((match = tagRegex.exec(content)) !== null) {
|
||||
tags.push(match[1]);
|
||||
}
|
||||
|
||||
const catRegex = /@([^\s#@,.]+)/g;
|
||||
const catMatch = catRegex.exec(content);
|
||||
const category = catMatch ? catMatch[1] : (this.activeFilters.category || undefined);
|
||||
|
||||
// Clean content (optional: user might want to keep them)
|
||||
// For now, keep them in content but also add to metadata
|
||||
|
||||
const card = new Card({
|
||||
content,
|
||||
tags,
|
||||
category: category === 'all' ? undefined : category
|
||||
});
|
||||
await this.store.add(card);
|
||||
editorEl.textContent = '';
|
||||
updatePlaceholder();
|
||||
await this.renderCards();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +606,18 @@ export class CardSidebarView extends ItemView {
|
|||
this.renderTimeout = setTimeout(() => this.renderCards(), 300);
|
||||
}
|
||||
|
||||
private async renderCards(): Promise<void> {
|
||||
private async renderCards(isManualReload = false): Promise<void> {
|
||||
const settings = { ...this.store.settings };
|
||||
if (isManualReload) {
|
||||
// If manually reloading, we might want to skip animation or use a faster one
|
||||
// But user said "reload cards makes the cards come in with the fade in" as if it's an issue
|
||||
// Let's make reload NOT fade in if they prefer instant
|
||||
settings.animatedCards = false;
|
||||
settings.disableCardFadeIn = true;
|
||||
}
|
||||
|
||||
const scrollTop = this.cardsContainer?.scrollTop || 0;
|
||||
|
||||
await flipAnimateAsync(this.cardsContainer, async () => {
|
||||
// Clear existing components
|
||||
this.cardComponents.forEach(comp => comp.destroy());
|
||||
|
|
@ -439,9 +634,15 @@ export class CardSidebarView extends ItemView {
|
|||
const comp = new CardComponent(this.cardsContainer, card, this.store, this.app, this.plugin);
|
||||
this.cardComponents.set(card.id, comp);
|
||||
}
|
||||
}, {}, this.store.settings);
|
||||
|
||||
this.applyLayoutMode();
|
||||
// Apply layout mode (Masonry) BEFORE measurements are taken by flipAnimateAsync
|
||||
this.applyLayoutMode();
|
||||
|
||||
// Restore scroll position BEFORE measurements are taken
|
||||
if (this.cardsContainer) {
|
||||
this.cardsContainer.scrollTop = scrollTop;
|
||||
}
|
||||
}, {}, settings);
|
||||
}
|
||||
|
||||
async onClose(): Promise<void> {
|
||||
|
|
@ -462,7 +663,8 @@ export class CardSidebarView extends ItemView {
|
|||
}
|
||||
|
||||
private applyLayoutMode(): void {
|
||||
const grid = !!(this.plugin as any).settings?.verticalCardMode;
|
||||
if (!this.cardsContainer) return;
|
||||
const grid = !!this.plugin.settings.verticalCardMode;
|
||||
if (grid) {
|
||||
this.cardsContainer.addClass('grid-mode');
|
||||
this.cardsContainer.addClass('vertical-card-mode');
|
||||
|
|
@ -479,7 +681,7 @@ export class CardSidebarView extends ItemView {
|
|||
this._masonryMutationObserver.disconnect();
|
||||
this._masonryMutationObserver = null;
|
||||
}
|
||||
this.cardsContainer.querySelectorAll('.card-sidebar-card').forEach((el) => {
|
||||
this.cardsContainer.querySelectorAll('.sc-card').forEach((el) => {
|
||||
(el as HTMLElement).style.gridRowEnd = '';
|
||||
});
|
||||
}
|
||||
|
|
@ -497,15 +699,15 @@ export class CardSidebarView extends ItemView {
|
|||
this._masonryTimeout = setTimeout(() => this.refreshMasonrySpans(), 50);
|
||||
});
|
||||
this._masonryObserver.observe(this.cardsContainer);
|
||||
this.cardsContainer.querySelectorAll('.card-sidebar-card').forEach(el => {
|
||||
this.cardsContainer.querySelectorAll('.sc-card').forEach(el => {
|
||||
this._masonryObserver?.observe(el);
|
||||
});
|
||||
this.setupMasonryMutationObserver();
|
||||
}
|
||||
|
||||
private refreshMasonrySpans(): void {
|
||||
if (!(this.plugin as any).settings?.verticalCardMode || !this.cardsContainer) return;
|
||||
const cards = this.cardsContainer.querySelectorAll('.card-sidebar-card:not(.drag-spacer)');
|
||||
if (!this.plugin.settings.verticalCardMode || !this.cardsContainer) return;
|
||||
const cards = this.cardsContainer.querySelectorAll('.sc-card:not(.drag-spacer)');
|
||||
cards.forEach((el) => {
|
||||
const card = el as HTMLElement;
|
||||
card.style.gridRowEnd = 'auto';
|
||||
|
|
@ -541,4 +743,8 @@ export class CardSidebarView extends ItemView {
|
|||
childList: true,
|
||||
subtree: true,
|
||||
characterData: true,
|
||||
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,94 @@
|
|||
import { ItemView, WorkspaceLeaf, Menu, Notice, setIcon } from "obsidian";
|
||||
import { ItemView, WorkspaceLeaf, Menu, Notice, setIcon, Scope, Editor, TFile } from "obsidian";
|
||||
import type SideCardsPlugin from "../core/Plugin";
|
||||
import { CardStore } from "../services/CardStore";
|
||||
import { Card } from "../models/Card";
|
||||
import { CardComponent } from "./components/Card";
|
||||
import { SortService, SortMode } from "../services/SortService";
|
||||
|
||||
export class SideCardsHomeView extends ItemView {
|
||||
private selectedColor = 'var(--card-color-1)';
|
||||
private selectedTags: string[] = [];
|
||||
private filterType = '';
|
||||
private filterValue = '';
|
||||
private editorScope: Scope;
|
||||
private editor!: Editor;
|
||||
private owner!: any;
|
||||
private recentFiles: TFile[] = [];
|
||||
private pinnedFiles: TFile[] = [];
|
||||
private cardComponents: Map<string, CardComponent> = new Map();
|
||||
private currentSortMode: SortMode = 'created';
|
||||
private sortAscending: boolean = false;
|
||||
private pinnedOnly: boolean = false;
|
||||
private currentSearchQuery: string = '';
|
||||
private iconicFileIconsCache: Record<string, any> | null = null;
|
||||
|
||||
constructor(
|
||||
leaf: WorkspaceLeaf,
|
||||
private plugin: SideCardsPlugin,
|
||||
private store: CardStore
|
||||
private store: CardStore,
|
||||
private sortService: SortService
|
||||
) {
|
||||
super(leaf);
|
||||
this.editorScope = new Scope(this.app.scope);
|
||||
// Block native formatting keys so Obsidian's global commands take over
|
||||
this.editorScope.register(["Mod"], "b", () => true);
|
||||
this.editorScope.register(["Mod"], "i", () => true);
|
||||
this.editorScope.register(["Mod"], "u", () => true);
|
||||
this.setupMockEditor();
|
||||
}
|
||||
|
||||
private setupMockEditor() {
|
||||
this.editor = {
|
||||
getSelection: () => {
|
||||
const sel = window.getSelection();
|
||||
return sel ? sel.toString() : "";
|
||||
},
|
||||
replaceSelection: (text: string) => {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
},
|
||||
toggleBold: () => this.toggleMarkdownWrapper("**"),
|
||||
toggleItalic: () => this.toggleMarkdownWrapper("*"),
|
||||
} as any;
|
||||
|
||||
this.owner = {
|
||||
editor: this.editor,
|
||||
editMode: true,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleMarkdownWrapper(wrapper: "**" | "*" | "~~" | "==") {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
const selectedText = sel.toString();
|
||||
|
||||
if (selectedText.length === 0) {
|
||||
// Empty selection: insert **** and place cursor in middle
|
||||
const text = wrapper + wrapper;
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStart(node, wrapper.length);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
} else {
|
||||
const alreadyWrapped = selectedText.startsWith(wrapper) && selectedText.endsWith(wrapper);
|
||||
const newText = alreadyWrapped
|
||||
? selectedText.slice(wrapper.length, -wrapper.length)
|
||||
: wrapper + selectedText + wrapper;
|
||||
|
||||
this.editor.replaceSelection(newText);
|
||||
}
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
|
|
@ -32,37 +106,19 @@ export class SideCardsHomeView extends ItemView {
|
|||
async onOpen(): Promise<void> {
|
||||
const container = this.containerEl;
|
||||
container.empty();
|
||||
container.addClass('sidecards-home-container');
|
||||
container.addClass('sc-home-container');
|
||||
this.currentSortMode = (this.plugin.settings.sortMode as SortMode) || 'created';
|
||||
this.sortAscending = typeof this.plugin.settings.sortAscending === 'boolean' ? this.plugin.settings.sortAscending : false;
|
||||
|
||||
const main = container.createDiv({ cls: 'sidecards-home-main' });
|
||||
main.style.padding = '32px';
|
||||
const title = main.createEl('h2', { text: 'SideCards' });
|
||||
title.style.margin = '0 0 12px 0';
|
||||
const main = container.createDiv({ cls: 'sc-home-main' });
|
||||
|
||||
// Top Section
|
||||
const topSection = main.createDiv({ cls: 'sc-home-top' });
|
||||
topSection.createEl('h2', { text: 'SideCards', cls: 'sc-home-title' });
|
||||
|
||||
const inputBox = main.createDiv({ cls: 'sidecards-home-input' });
|
||||
inputBox.style.margin = '12px 0';
|
||||
const input = inputBox.createEl('textarea');
|
||||
input.placeholder = 'Type card content…';
|
||||
input.rows = 4;
|
||||
input.style.width = '100%';
|
||||
input.style.minHeight = '100px';
|
||||
input.style.padding = '12px';
|
||||
input.style.border = '1px solid var(--background-modifier-border)';
|
||||
input.style.borderRadius = '6px';
|
||||
input.style.resize = 'vertical';
|
||||
|
||||
const paletteRow = main.createDiv({ cls: 'sidecards-home-palette-row' });
|
||||
paletteRow.style.display = 'flex';
|
||||
paletteRow.style.gap = '6px';
|
||||
paletteRow.style.alignItems = 'center';
|
||||
paletteRow.style.marginTop = '8px';
|
||||
paletteRow.style.marginBottom = '20px';
|
||||
|
||||
const categoryBtn = paletteRow.createEl('button', { text: 'category', cls: 'sidecards-home-category-btn' });
|
||||
categoryBtn.style.padding = '6px 10px';
|
||||
categoryBtn.style.border = '1px solid var(--background-modifier-border)';
|
||||
categoryBtn.style.borderRadius = '6px';
|
||||
categoryBtn.addEventListener('click', () => {
|
||||
const paletteRow = topSection.createDiv({ cls: 'sc-home-palette-row' });
|
||||
const categoryBtn = paletteRow.createEl('button', { text: 'category', cls: 'sc-home-category-btn' });
|
||||
categoryBtn.addEventListener('click', (e) => {
|
||||
const menu = new Menu();
|
||||
this.getAvailableFilters().forEach((f) => {
|
||||
menu.addItem(item => item.setTitle(f.label).onClick(() => {
|
||||
|
|
@ -71,16 +127,11 @@ export class SideCardsHomeView extends ItemView {
|
|||
categoryBtn.textContent = f.label;
|
||||
}));
|
||||
});
|
||||
const r = categoryBtn.getBoundingClientRect();
|
||||
menu.showAtPosition({ x: r.left, y: r.bottom });
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
const separator = paletteRow.createDiv({ cls: 'sidecards-home-separator' });
|
||||
const separator = paletteRow.createDiv({ cls: 'sc-home-separator' });
|
||||
separator.textContent = '|';
|
||||
separator.style.color = 'var(--background-modifier-border)';
|
||||
separator.style.margin = '0 8px';
|
||||
separator.style.fontSize = '18px';
|
||||
separator.style.opacity = '0.6';
|
||||
|
||||
const colors = [
|
||||
{ name: 'gray', var: 'var(--card-color-1)' },
|
||||
|
|
@ -96,177 +147,74 @@ export class SideCardsHomeView extends ItemView {
|
|||
];
|
||||
const swatches: HTMLElement[] = [];
|
||||
colors.forEach((color) => {
|
||||
const swatch = paletteRow.createDiv({ cls: 'sidecards-home-color-dot' });
|
||||
swatch.style.width = '28px';
|
||||
swatch.style.height = '28px';
|
||||
swatch.style.borderRadius = '4px';
|
||||
swatch.style.border = this.selectedColor === color.var ? '2px solid var(--text-accent)' : '2px solid var(--background-modifier-border)';
|
||||
const swatch = paletteRow.createDiv({ cls: 'sc-home-color-dot' });
|
||||
const root = document.documentElement;
|
||||
const computedColor = getComputedStyle(root).getPropertyValue(color.var.replace('var(', '').replace(')', ''));
|
||||
swatch.style.backgroundColor = computedColor.trim() || color.var;
|
||||
swatch.style.cursor = 'pointer';
|
||||
swatch.style.transition = 'transform 0.15s ease';
|
||||
swatch.addEventListener('mouseenter', () => { swatch.style.transform = 'scale(1.1)'; });
|
||||
swatch.addEventListener('mouseleave', () => { swatch.style.transform = 'scale(1)'; });
|
||||
if (this.selectedColor === color.var) swatch.addClass('is-selected');
|
||||
|
||||
swatch.addEventListener('click', () => {
|
||||
swatches.forEach((s) => s.style.border = '2px solid var(--background-modifier-border)');
|
||||
swatch.style.border = '2px solid var(--text-accent)';
|
||||
swatches.forEach((s) => s.removeClass('is-selected'));
|
||||
swatch.addClass('is-selected');
|
||||
this.selectedColor = color.var;
|
||||
});
|
||||
swatches.push(swatch);
|
||||
});
|
||||
|
||||
const autocompleteWrap = main.createDiv({ cls: 'sidecards-home-autocomplete-wrap' });
|
||||
autocompleteWrap.style.position = 'relative';
|
||||
autocompleteWrap.appendChild(inputBox);
|
||||
const editorEl = topSection.createDiv({ cls: 'sc-home-editor' });
|
||||
editorEl.setAttribute('contenteditable', 'true');
|
||||
editorEl.dataset.placeholder = 'Type here... (@category, #tag)';
|
||||
|
||||
const homeTagAutocompleteContainer = autocompleteWrap.createDiv({ cls: 'card-tag-autocomplete' });
|
||||
homeTagAutocompleteContainer.style.display = 'none';
|
||||
homeTagAutocompleteContainer.style.position = 'absolute';
|
||||
homeTagAutocompleteContainer.style.bottom = 'calc(100% + 4px)';
|
||||
homeTagAutocompleteContainer.style.left = '0';
|
||||
homeTagAutocompleteContainer.style.right = '0';
|
||||
|
||||
const homeGroupAutocompleteContainer = autocompleteWrap.createDiv({ cls: 'card-group-autocomplete' });
|
||||
homeGroupAutocompleteContainer.style.display = 'none';
|
||||
homeGroupAutocompleteContainer.style.position = 'absolute';
|
||||
homeGroupAutocompleteContainer.style.bottom = 'calc(100% + 4px)';
|
||||
homeGroupAutocompleteContainer.style.left = '0';
|
||||
homeGroupAutocompleteContainer.style.right = '0';
|
||||
|
||||
let homeTagSelectedIndex = -1;
|
||||
let homeGroupSelectedIndex = -1;
|
||||
|
||||
const updateHomeTagAutocomplete = () => {
|
||||
try {
|
||||
const cursorPos = input.selectionStart || 0;
|
||||
const textBeforeCursor = input.value.substring(0, cursorPos);
|
||||
const lastHashIdx = textBeforeCursor.lastIndexOf('#');
|
||||
if (lastHashIdx === -1 || lastHashIdx < textBeforeCursor.length - 1) {
|
||||
homeTagAutocompleteContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
const currentWord = textBeforeCursor.substring(lastHashIdx + 1).toLowerCase();
|
||||
const allTags = this.getAllUsedTags();
|
||||
const suggestions = allTags.filter(t => t.startsWith(currentWord)).slice(0, 8);
|
||||
if (suggestions.length === 0 && currentWord.length > 0) {
|
||||
homeTagAutocompleteContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
homeTagAutocompleteContainer.empty();
|
||||
homeTagSelectedIndex = -1;
|
||||
const displayTags = currentWord.length === 0 ? allTags.slice(0, 8) : suggestions;
|
||||
if (displayTags.length === 0) {
|
||||
homeTagAutocompleteContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
displayTags.forEach((tag, idx) => {
|
||||
const item = homeTagAutocompleteContainer.createDiv();
|
||||
item.style.padding = '4px 8px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||
item.textContent = '#' + tag;
|
||||
item.addEventListener('mouseenter', () => { item.style.background = 'var(--background-modifier-hover)'; homeTagSelectedIndex = idx; });
|
||||
item.addEventListener('mouseleave', () => { item.style.background = ''; });
|
||||
item.addEventListener('click', () => {
|
||||
const before = input.value.substring(0, lastHashIdx);
|
||||
const after = input.value.substring(cursorPos);
|
||||
input.value = before + '#' + tag + ' ' + after;
|
||||
input.selectionStart = input.selectionEnd = before.length + tag.length + 2;
|
||||
input.focus();
|
||||
updateHomeTagAutocomplete();
|
||||
});
|
||||
});
|
||||
homeTagAutocompleteContainer.style.display = '';
|
||||
} catch {}
|
||||
const updatePlaceholder = () => {
|
||||
if (!editorEl.textContent?.trim()) editorEl.addClass('is-empty');
|
||||
else editorEl.removeClass('is-empty');
|
||||
};
|
||||
|
||||
const updateHomeGroupAutocomplete = () => {
|
||||
try {
|
||||
const cursorPos = input.selectionStart || 0;
|
||||
const textBeforeCursor = input.value.substring(0, cursorPos);
|
||||
const lines = textBeforeCursor.split('\n');
|
||||
const currentLine = lines[lines.length - 1];
|
||||
const atIdx = currentLine.lastIndexOf('@');
|
||||
if (atIdx === -1) {
|
||||
homeGroupAutocompleteContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
const currentWord = currentLine.substring(atIdx + 1).toLowerCase();
|
||||
const groups = ['all', 'today', 'tomorrow'];
|
||||
const customCats = Array.isArray(this.plugin.settings.customCategories) ? this.plugin.settings.customCategories : [];
|
||||
const allSuggestions = [
|
||||
...groups.map(g => ({ text: '@' + g, label: g })),
|
||||
...customCats.map(c => ({ text: '@' + (c.id || c.label), label: c.label || c.id }))
|
||||
];
|
||||
const suggestions = currentWord.length === 0 ? allSuggestions : allSuggestions.filter(s => s.text.substring(1).startsWith(currentWord)).slice(0, 8);
|
||||
if (suggestions.length === 0) {
|
||||
homeGroupAutocompleteContainer.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
homeGroupAutocompleteContainer.empty();
|
||||
homeGroupSelectedIndex = -1;
|
||||
suggestions.forEach(({ text, label }, idx) => {
|
||||
const item = homeGroupAutocompleteContainer.createDiv();
|
||||
item.style.padding = '4px 8px';
|
||||
item.style.cursor = 'pointer';
|
||||
item.style.borderBottom = '1px solid var(--background-modifier-border)';
|
||||
item.style.fontSize = '12px';
|
||||
item.textContent = String(label || '');
|
||||
item.addEventListener('mouseenter', () => { item.style.background = 'var(--background-modifier-hover)'; homeGroupSelectedIndex = idx; });
|
||||
item.addEventListener('mouseleave', () => { item.style.background = ''; });
|
||||
item.addEventListener('click', () => {
|
||||
const lineStart = textBeforeCursor.lastIndexOf('\n') + 1;
|
||||
const atAbs = lineStart + atIdx;
|
||||
const before = input.value.substring(0, atAbs);
|
||||
const after = input.value.substring(cursorPos);
|
||||
input.value = before + text + ' ' + after;
|
||||
input.selectionStart = input.selectionEnd = before.length + text.length + 1;
|
||||
input.focus();
|
||||
updateHomeGroupAutocomplete();
|
||||
});
|
||||
});
|
||||
homeGroupAutocompleteContainer.style.display = '';
|
||||
} catch {}
|
||||
};
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
updateHomeTagAutocomplete();
|
||||
updateHomeGroupAutocomplete();
|
||||
updatePlaceholder();
|
||||
editorEl.addEventListener('input', updatePlaceholder);
|
||||
editorEl.addEventListener('focusin', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.pushScope(this.editorScope);
|
||||
// @ts-ignore
|
||||
this.app.workspace.activeEditor = this.owner;
|
||||
});
|
||||
editorEl.addEventListener('blur', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.popScope(this.editorScope);
|
||||
// @ts-ignore
|
||||
if (this.app.workspace.activeEditor === this.owner) this.app.workspace.activeEditor = null;
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', async (e) => {
|
||||
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && homeGroupAutocompleteContainer.style.display !== 'none') {
|
||||
e.preventDefault();
|
||||
const items = homeGroupAutocompleteContainer.querySelectorAll('div');
|
||||
if (items.length === 0) return;
|
||||
if (e.key === 'ArrowDown') homeGroupSelectedIndex = (homeGroupSelectedIndex + 1) % items.length;
|
||||
else homeGroupSelectedIndex = (homeGroupSelectedIndex - 1 + items.length) % items.length;
|
||||
items.forEach((item, idx) => (item as HTMLElement).style.background = idx === homeGroupSelectedIndex ? 'var(--background-modifier-hover)' : '');
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter' && homeGroupAutocompleteContainer.style.display !== 'none' && homeGroupSelectedIndex >= 0) {
|
||||
e.preventDefault();
|
||||
const items = homeGroupAutocompleteContainer.querySelectorAll('div');
|
||||
(items[homeGroupSelectedIndex] as HTMLElement)?.click();
|
||||
return;
|
||||
}
|
||||
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && homeTagAutocompleteContainer.style.display !== 'none') {
|
||||
e.preventDefault();
|
||||
const items = homeTagAutocompleteContainer.querySelectorAll('div');
|
||||
if (items.length === 0) return;
|
||||
if (e.key === 'ArrowDown') homeTagSelectedIndex = (homeTagSelectedIndex + 1) % items.length;
|
||||
else homeTagSelectedIndex = (homeTagSelectedIndex - 1 + items.length) % items.length;
|
||||
items.forEach((item, idx) => (item as HTMLElement).style.background = idx === homeTagSelectedIndex ? 'var(--background-modifier-hover)' : '');
|
||||
return;
|
||||
}
|
||||
if (e.key === 'Enter' && homeTagAutocompleteContainer.style.display !== 'none' && homeTagSelectedIndex >= 0) {
|
||||
e.preventDefault();
|
||||
const items = homeTagAutocompleteContainer.querySelectorAll('div');
|
||||
(items[homeTagSelectedIndex] as HTMLElement)?.click();
|
||||
return;
|
||||
}
|
||||
// Content Section (Two Columns)
|
||||
const contentGrid = main.createDiv({ cls: 'sc-home-grid' });
|
||||
|
||||
// Left Column: Notes
|
||||
const leftCol = contentGrid.createDiv({ cls: 'sc-home-left' });
|
||||
|
||||
leftCol.createEl('h3', { text: 'Pinned Notes', cls: 'sc-home-section-title' });
|
||||
const pinnedList = leftCol.createDiv({ cls: 'sc-home-file-list pinned' });
|
||||
await this.renderFileList(pinnedList, 'pinned');
|
||||
|
||||
leftCol.createEl('h3', { text: 'Recent Notes', cls: 'sc-home-section-title' });
|
||||
const recentList = leftCol.createDiv({ cls: 'sc-home-file-list' });
|
||||
await this.renderFileList(recentList, 'recent');
|
||||
|
||||
// Right Column: Cards
|
||||
const rightCol = contentGrid.createDiv({ cls: 'sc-home-right' });
|
||||
|
||||
const toolbar = rightCol.createDiv({ cls: 'sc-home-toolbar' });
|
||||
const cardList = rightCol.createDiv({ cls: 'sc-home-card-list' });
|
||||
|
||||
// Category Buttons Bar (Create before card list)
|
||||
const categoryBar = rightCol.createDiv({ cls: 'sc-home-category-bar' });
|
||||
rightCol.insertBefore(categoryBar, cardList);
|
||||
|
||||
this.renderToolbar(toolbar, editorEl, cardList, pinnedList, recentList);
|
||||
this.renderCategoryBar(categoryBar, cardList);
|
||||
|
||||
await this.renderCards(cardList);
|
||||
|
||||
// Keydown for input
|
||||
editorEl.addEventListener('keydown', async (e) => {
|
||||
let pressed = '';
|
||||
if (e.ctrlKey) pressed += 'ctrl-';
|
||||
if (e.shiftKey) pressed += 'shift-';
|
||||
|
|
@ -276,35 +224,376 @@ export class SideCardsHomeView extends ItemView {
|
|||
const saveKey = normalizeKey(this.plugin.settings.saveKey || 'enter');
|
||||
if (pressed === saveKey || (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.altKey)) {
|
||||
e.preventDefault();
|
||||
await this.createCardFromHomeInput(input);
|
||||
await this.createCardFromHomeInput(editorEl, cardList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const toolbarRow = main.createDiv({ cls: 'sidecards-home-toolbar' });
|
||||
toolbarRow.style.display = 'flex';
|
||||
toolbarRow.style.gap = '6px';
|
||||
toolbarRow.style.alignItems = 'center';
|
||||
private async renderFileList(container: HTMLElement, type: 'recent' | 'pinned') {
|
||||
container.empty();
|
||||
const files = type === 'recent' ? await this.getRecentFiles() : await this.getPinnedFiles();
|
||||
|
||||
if (files.length === 0) {
|
||||
container.createDiv({ text: `No ${type} notes found`, cls: 'sc-home-empty-msg' });
|
||||
return;
|
||||
}
|
||||
|
||||
const openSidebarBtn = toolbarRow.createEl('button', { cls: 'sidecards-home-reload-btn' });
|
||||
openSidebarBtn.title = 'Open sidebar';
|
||||
setIcon(openSidebarBtn, 'panel-right');
|
||||
openSidebarBtn.addEventListener('click', async () => this.plugin.activateView());
|
||||
for (const file of files) {
|
||||
const item = container.createDiv({ cls: 'sc-home-file-item' });
|
||||
const iconSpan = item.createSpan({ cls: 'sc-home-file-icon' });
|
||||
await this.renderCustomFileIcon(iconSpan, file);
|
||||
item.createSpan({ text: file.basename, cls: 'sc-home-file-name' });
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
this.app.workspace.getLeaf(false).openFile(file);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const searchBtn = toolbarRow.createEl('button', { cls: 'sidecards-home-sort-btn' });
|
||||
searchBtn.title = 'Search cards';
|
||||
setIcon(searchBtn, 'search');
|
||||
searchBtn.addEventListener('click', async () => {
|
||||
await this.plugin.activateView();
|
||||
const view: any = this.app.workspace.getLeavesOfType('card-sidebar')?.[0]?.view;
|
||||
if (view?._searchWrap) view._searchWrap.style.display = '';
|
||||
if (view?._searchInput) view._searchInput.focus();
|
||||
private async renderCustomFileIcon(iconEl: HTMLElement, file: TFile): Promise<void> {
|
||||
const iconInfo = await this.getIconicFileIcon(file);
|
||||
if (!iconInfo) {
|
||||
setIcon(iconEl, 'file-text');
|
||||
return;
|
||||
}
|
||||
|
||||
const iconValue = String(iconInfo.icon || '').trim();
|
||||
if (!iconValue) {
|
||||
setIcon(iconEl, 'file-text');
|
||||
return;
|
||||
}
|
||||
|
||||
let rendered = false;
|
||||
const normalizedLucide = this.normalizeLucideIconName(iconValue);
|
||||
if (normalizedLucide) {
|
||||
try {
|
||||
setIcon(iconEl, normalizedLucide);
|
||||
rendered = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
if (!rendered && iconValue.includes('<svg')) {
|
||||
iconEl.innerHTML = iconValue;
|
||||
rendered = true;
|
||||
}
|
||||
|
||||
if (!rendered && /^:.*:$/.test(iconValue)) {
|
||||
iconEl.setText(iconValue);
|
||||
rendered = true;
|
||||
}
|
||||
|
||||
if (!rendered) {
|
||||
iconEl.setText(iconValue);
|
||||
rendered = true;
|
||||
}
|
||||
|
||||
if (iconInfo.color) {
|
||||
iconEl.style.color = iconInfo.color;
|
||||
}
|
||||
}
|
||||
|
||||
private normalizeLucideIconName(rawIcon: string): string | null {
|
||||
const cleaned = rawIcon.trim();
|
||||
if (!cleaned) return null;
|
||||
if (cleaned.includes('<svg')) return null;
|
||||
if (/^:.*:$/.test(cleaned)) return null;
|
||||
if (/[\u{1F300}-\u{1FAFF}]/u.test(cleaned)) return null;
|
||||
|
||||
let iconName = cleaned;
|
||||
const prefixes = ['lucide:', 'lucide/', 'lucide-'];
|
||||
for (const prefix of prefixes) {
|
||||
if (iconName.startsWith(prefix)) {
|
||||
iconName = iconName.slice(prefix.length);
|
||||
}
|
||||
}
|
||||
if (iconName.includes(':')) {
|
||||
const parts = iconName.split(':');
|
||||
iconName = parts[parts.length - 1];
|
||||
}
|
||||
if (iconName.includes('/')) {
|
||||
const parts = iconName.split('/');
|
||||
iconName = parts[parts.length - 1];
|
||||
}
|
||||
iconName = iconName.trim();
|
||||
|
||||
if (!/^[a-z0-9\-]+$/i.test(iconName)) return null;
|
||||
return iconName.toLowerCase();
|
||||
}
|
||||
|
||||
private resolveIconicIconEntry(entry: any): { icon: string; color?: string } | null {
|
||||
if (!entry) return null;
|
||||
if (typeof entry === 'string') {
|
||||
return { icon: entry };
|
||||
}
|
||||
if (typeof entry === 'object') {
|
||||
const iconValue = entry.icon ?? entry.name ?? entry.value;
|
||||
if (!iconValue) return null;
|
||||
return {
|
||||
icon: String(iconValue),
|
||||
color: typeof entry.color === 'string' ? entry.color : undefined,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getIconicFileIcon(file: TFile): Promise<{ icon: string; color?: string } | null> {
|
||||
const iconicPlugin = (this.app as any).plugins?.getPlugin?.('iconic');
|
||||
if (!iconicPlugin) return null;
|
||||
|
||||
const path = file.path;
|
||||
|
||||
const immediateEntry = this.resolveIconicIconEntry(
|
||||
iconicPlugin.settings?.fileIcons?.[path]
|
||||
?? iconicPlugin.data?.fileIcons?.[path]
|
||||
?? iconicPlugin.fileIcons?.[path]
|
||||
);
|
||||
if (immediateEntry) return immediateEntry;
|
||||
|
||||
if (this.iconicFileIconsCache === null) {
|
||||
try {
|
||||
const loaded = await iconicPlugin.loadData?.();
|
||||
this.iconicFileIconsCache = loaded?.fileIcons && typeof loaded.fileIcons === 'object'
|
||||
? loaded.fileIcons
|
||||
: {};
|
||||
} catch (e) {
|
||||
this.iconicFileIconsCache = {};
|
||||
}
|
||||
}
|
||||
|
||||
const cache = this.iconicFileIconsCache || {};
|
||||
return this.resolveIconicIconEntry(cache[path]);
|
||||
}
|
||||
|
||||
private async getRecentFiles(): Promise<TFile[]> {
|
||||
return this.app.vault.getMarkdownFiles()
|
||||
.sort((a, b) => b.stat.mtime - a.stat.mtime)
|
||||
.slice(0, 5);
|
||||
}
|
||||
|
||||
private async getPinnedFiles(): Promise<TFile[]> {
|
||||
const pinned: TFile[] = [];
|
||||
|
||||
// 1. Get from settings
|
||||
if (this.plugin.settings.pinnedNotes) {
|
||||
this.plugin.settings.pinnedNotes.forEach(path => {
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) pinned.push(file);
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Get from bookmarks plugin if available
|
||||
try {
|
||||
const bookmarks = (this.app as any).internalPlugins?.getPluginById("bookmarks")?.instance;
|
||||
if (bookmarks && bookmarks.items) {
|
||||
bookmarks.items.forEach((item: any) => {
|
||||
if (item.type === 'file') {
|
||||
const file = this.app.vault.getAbstractFileByPath(item.path);
|
||||
if (file instanceof TFile && !pinned.includes(file)) pinned.push(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
return pinned.slice(0, 10); // Limit to 10 pinned notes
|
||||
}
|
||||
|
||||
public async refreshPinnedNotes() {
|
||||
const pinnedList = this.containerEl.querySelector('.sc-home-file-list.pinned');
|
||||
if (pinnedList) {
|
||||
await this.renderFileList(pinnedList as HTMLElement, 'pinned');
|
||||
}
|
||||
}
|
||||
|
||||
private async refreshHomeContent(cardList: HTMLElement, pinnedList: HTMLElement, recentList: HTMLElement): Promise<void> {
|
||||
this.iconicFileIconsCache = null;
|
||||
await this.renderFileList(pinnedList, 'pinned');
|
||||
await this.renderFileList(recentList, 'recent');
|
||||
await this.renderCards(cardList);
|
||||
}
|
||||
|
||||
private renderToolbar(container: HTMLElement, editorEl: HTMLElement, cardList: HTMLElement, pinnedList: HTMLElement, recentList: HTMLElement) {
|
||||
container.empty();
|
||||
|
||||
const leftActions = container.createDiv({ cls: 'sc-home-toolbar-left' });
|
||||
const searchWrap = container.createDiv({ cls: 'sc-home-search-wrap' });
|
||||
const rightActions = container.createDiv({ cls: 'sc-home-toolbar-right' });
|
||||
|
||||
// Left Actions
|
||||
const sortBtn = leftActions.createEl('button', { cls: 'sc-icon-btn' });
|
||||
setIcon(sortBtn, 'sort-desc');
|
||||
sortBtn.title = 'Sort';
|
||||
sortBtn.addEventListener('click', (e) => {
|
||||
const menu = new Menu();
|
||||
|
||||
const modes: { label: string, mode: SortMode }[] = [
|
||||
{ label: 'Manual', mode: 'manual' },
|
||||
{ label: 'Created Time', mode: 'created' },
|
||||
{ label: 'Modified Time', mode: 'modified' },
|
||||
{ label: 'A → Z', mode: 'alpha' },
|
||||
{ label: 'Status', mode: 'status' }
|
||||
];
|
||||
|
||||
modes.forEach(({ label, mode }) => {
|
||||
menu.addItem(item => {
|
||||
item.setTitle(label)
|
||||
.setChecked(this.currentSortMode === mode)
|
||||
.onClick(async () => {
|
||||
this.currentSortMode = mode;
|
||||
this.plugin.settings.sortMode = mode;
|
||||
await this.plugin.saveSettings();
|
||||
await this.renderCards(cardList);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
menu.addItem(item => {
|
||||
item.setTitle('Direction: ' + (this.sortAscending ? 'Ascending' : 'Descending'))
|
||||
.onClick(async () => {
|
||||
this.sortAscending = !this.sortAscending;
|
||||
this.plugin.settings.sortAscending = this.sortAscending;
|
||||
await this.plugin.saveSettings();
|
||||
await this.renderCards(cardList);
|
||||
});
|
||||
});
|
||||
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
const addBtn = toolbarRow.createEl('button', { cls: 'sidecards-home-pinned-btn' });
|
||||
addBtn.title = 'Add card';
|
||||
setIcon(addBtn, 'plus');
|
||||
addBtn.addEventListener('click', async () => {
|
||||
await this.createCardFromHomeInput(input);
|
||||
const refreshBtn = leftActions.createEl('button', { cls: 'sc-icon-btn' });
|
||||
setIcon(refreshBtn, 'refresh-cw');
|
||||
refreshBtn.title = 'Refresh';
|
||||
refreshBtn.addEventListener('click', async () => {
|
||||
await this.refreshHomeContent(cardList, pinnedList, recentList);
|
||||
});
|
||||
|
||||
// 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...' });
|
||||
searchInput.addEventListener('input', () => {
|
||||
this.currentSearchQuery = searchInput.value;
|
||||
this.renderCards(cardList);
|
||||
});
|
||||
|
||||
// Right Actions
|
||||
const pinBtn = rightActions.createEl('button', { cls: 'sc-icon-btn' });
|
||||
setIcon(pinBtn, 'pin');
|
||||
pinBtn.title = 'Pinned';
|
||||
pinBtn.addEventListener('click', () => {
|
||||
pinBtn.toggleClass('active', !pinBtn.hasClass('active'));
|
||||
this.pinnedOnly = pinBtn.hasClass('active');
|
||||
this.renderCards(cardList);
|
||||
});
|
||||
|
||||
const moreBtn = rightActions.createEl('button', { cls: 'sc-icon-btn' });
|
||||
setIcon(moreBtn, 'more-vertical');
|
||||
moreBtn.title = 'More';
|
||||
moreBtn.addEventListener('click', (e) => {
|
||||
const menu = new Menu();
|
||||
menu.addItem(item => {
|
||||
item.setTitle('Show tags')
|
||||
.setChecked(!this.plugin.settings.groupTags)
|
||||
.onClick(async () => {
|
||||
this.plugin.settings.groupTags = !this.plugin.settings.groupTags;
|
||||
await this.plugin.saveSettings();
|
||||
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;
|
||||
await this.plugin.saveSettings();
|
||||
this.renderCards(cardList);
|
||||
});
|
||||
});
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
const sidebarBtn = rightActions.createEl('button', { cls: 'sc-icon-btn' });
|
||||
setIcon(sidebarBtn, 'panel-right');
|
||||
sidebarBtn.title = 'Open Sidebar';
|
||||
sidebarBtn.addEventListener('click', async () => {
|
||||
this.plugin.activateView();
|
||||
});
|
||||
}
|
||||
|
||||
private renderCategoryBar(container: HTMLElement, cardList: HTMLElement) {
|
||||
container.empty();
|
||||
const filters = this.getAvailableFilters();
|
||||
|
||||
filters.forEach(f => {
|
||||
const btn = container.createEl('button', { text: f.label, cls: 'sc-category-btn' });
|
||||
if (this.filterValue === f.value) btn.addClass('active');
|
||||
|
||||
const colorKey = f.value === 'all' ? 'all'
|
||||
: f.value === 'today' ? 'today'
|
||||
: f.value === 'tomorrow' ? 'tomorrow'
|
||||
: f.value === 'archived' ? 'archived'
|
||||
: f.value;
|
||||
btn.dataset.filterValue = f.value;
|
||||
btn.dataset.filterColorKey = colorKey;
|
||||
|
||||
const customColors = this.plugin.settings.filterColors?.[colorKey];
|
||||
if (customColors?.bgColor) btn.style.setProperty('background-color', customColors.bgColor, 'important');
|
||||
if (customColors?.textColor) btn.style.setProperty('color', customColors.textColor, 'important');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
container.querySelectorAll('.sc-category-btn').forEach(b => {
|
||||
(b as HTMLElement).removeClass('active');
|
||||
const bColorKey = (b as HTMLElement).dataset.filterColorKey || '';
|
||||
const bColors = this.plugin.settings.filterColors?.[bColorKey];
|
||||
if (bColors?.bgColor) (b as HTMLElement).style.setProperty('background-color', bColors.bgColor, 'important');
|
||||
else (b as HTMLElement).style.removeProperty('background-color');
|
||||
if (bColors?.textColor) (b as HTMLElement).style.setProperty('color', bColors.textColor, 'important');
|
||||
else (b as HTMLElement).style.removeProperty('color');
|
||||
});
|
||||
btn.addClass('active');
|
||||
this.filterType = f.type;
|
||||
this.filterValue = f.value;
|
||||
this.renderCards(cardList);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async renderCards(container: HTMLElement) {
|
||||
container.empty();
|
||||
this.cardComponents.forEach(c => c.destroy());
|
||||
this.cardComponents.clear();
|
||||
|
||||
let cards = this.store.getAll();
|
||||
|
||||
if (this.pinnedOnly) {
|
||||
cards = cards.filter(c => c.pinned);
|
||||
}
|
||||
|
||||
if (this.currentSearchQuery) {
|
||||
cards = cards.filter(c => c.content.toLowerCase().includes(this.currentSearchQuery.toLowerCase()));
|
||||
}
|
||||
|
||||
const category = this.filterValue;
|
||||
if (category && category !== 'all') {
|
||||
if (category === 'today') {
|
||||
const today = new Date().toDateString();
|
||||
cards = cards.filter(c => new Date(c.created).toDateString() === today);
|
||||
} else if (category === 'tomorrow') {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
const tomorrowStr = tomorrow.toDateString();
|
||||
cards = cards.filter(c => new Date(c.created).toDateString() === tomorrowStr);
|
||||
} else {
|
||||
cards = cards.filter(c => c.category === category);
|
||||
}
|
||||
}
|
||||
|
||||
cards = this.sortService.sort(cards, this.currentSortMode, this.sortAscending);
|
||||
|
||||
cards.slice(0, 10).forEach(card => {
|
||||
const comp = new CardComponent(container, card, this.store, this.app, this.plugin);
|
||||
this.cardComponents.set(card.id, comp);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -338,23 +627,37 @@ export class SideCardsHomeView extends ItemView {
|
|||
}
|
||||
}
|
||||
|
||||
private async createCardFromHomeInput(input: HTMLTextAreaElement): Promise<void> {
|
||||
let content = input.value.trim();
|
||||
private async createCardFromHomeInput(editorEl: HTMLElement, cardList: HTMLElement) {
|
||||
const content = editorEl.textContent?.trim();
|
||||
if (!content) return;
|
||||
const tagMatches = content.match(/#[a-zA-Z0-9_-]+/g) || [];
|
||||
const tags = Array.from(new Set([...this.selectedTags, ...tagMatches.map(t => t.substring(1))]));
|
||||
content = content.replace(/#[a-zA-Z0-9_-]+/g, '').trim();
|
||||
|
||||
const card = new Card({
|
||||
content,
|
||||
color: this.selectedColor,
|
||||
tags,
|
||||
category: this.filterType === 'category' ? this.filterValue : null,
|
||||
archived: this.filterType === 'archived'
|
||||
// Extract tags (#tag) and categories (@category)
|
||||
const tags: string[] = [];
|
||||
const tagRegex = /#([^\s#@,.]+)/g;
|
||||
let match;
|
||||
while ((match = tagRegex.exec(content)) !== null) {
|
||||
tags.push(match[1]);
|
||||
}
|
||||
|
||||
const catRegex = /@([^\s#@,.]+)/g;
|
||||
const catMatch = catRegex.exec(content);
|
||||
const category = catMatch ? catMatch[1] : (this.filterValue === 'all' ? undefined : this.filterValue);
|
||||
|
||||
const card = new Card({
|
||||
content,
|
||||
color: this.selectedColor,
|
||||
tags: [...new Set([...tags, ...this.selectedTags])],
|
||||
category
|
||||
});
|
||||
await this.store.add(card);
|
||||
input.value = '';
|
||||
editorEl.textContent = '';
|
||||
// @ts-ignore
|
||||
editorEl.dispatchEvent(new Event('input')); // trigger placeholder
|
||||
|
||||
// Clear selection state
|
||||
this.selectedTags = [];
|
||||
new Notice('Card added');
|
||||
|
||||
new Notice('Card created');
|
||||
this.renderCards(cardList);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,28 @@
|
|||
import { Card } from "../../models/Card";
|
||||
import { CardStore } from "../../services/CardStore";
|
||||
import { applyCardColorToElement } from "../../utils/dom";
|
||||
import { App, MarkdownRenderer, Plugin, Menu, Notice, TFile, setIcon } from "obsidian";
|
||||
import { App, MarkdownRenderer, Plugin, Menu, Notice, TFile, setIcon, Scope, Editor } from "obsidian";
|
||||
import { DateTimeModal } from "../modals/DateTimeModal";
|
||||
|
||||
export class CardComponent {
|
||||
public el: HTMLElement;
|
||||
static activeEditor: CardComponent | null = null;
|
||||
private static instanceCount = 0;
|
||||
private static globalMouseDownBound = false;
|
||||
private static readonly handleGlobalMouseDown = (event: MouseEvent) => {
|
||||
const active = CardComponent.activeEditor;
|
||||
if (!active || !active.isEditing) return;
|
||||
const target = event.target as Node | null;
|
||||
if (target && active.el.contains(target)) return;
|
||||
active.blurAndSave();
|
||||
};
|
||||
private card: Card;
|
||||
private unsubscribe: (() => void)[] = [];
|
||||
private isEditing: boolean = false;
|
||||
private renderCount: number = 0;
|
||||
private scope: Scope;
|
||||
private editor!: Editor;
|
||||
private owner!: any;
|
||||
|
||||
constructor(
|
||||
private container: HTMLElement,
|
||||
|
|
@ -17,15 +32,85 @@ export class CardComponent {
|
|||
private app: App,
|
||||
private plugin: Plugin
|
||||
) {
|
||||
CardComponent.instanceCount += 1;
|
||||
this.card = card;
|
||||
this.el = container.createDiv('card-sidebar-card');
|
||||
this.el = container.createDiv('sc-card');
|
||||
this.scope = new Scope(this.app.scope);
|
||||
// Block native formatting keys so Obsidian's global commands take over
|
||||
this.scope.register(["Mod"], "b", () => true);
|
||||
this.scope.register(["Mod"], "i", () => true);
|
||||
this.scope.register(["Mod"], "u", () => true);
|
||||
this.setupMockEditor();
|
||||
this.ensureGlobalMouseDownHandler();
|
||||
this.render();
|
||||
this.setupListeners();
|
||||
}
|
||||
|
||||
private ensureGlobalMouseDownHandler() {
|
||||
if (CardComponent.globalMouseDownBound) return;
|
||||
document.addEventListener('mousedown', CardComponent.handleGlobalMouseDown, true);
|
||||
CardComponent.globalMouseDownBound = true;
|
||||
}
|
||||
|
||||
private setupMockEditor() {
|
||||
this.editor = {
|
||||
getSelection: () => {
|
||||
const sel = window.getSelection();
|
||||
return sel ? sel.toString() : "";
|
||||
},
|
||||
replaceSelection: (text: string) => {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
},
|
||||
toggleBold: () => this.toggleMarkdownWrapper("**"),
|
||||
toggleItalic: () => this.toggleMarkdownWrapper("*"),
|
||||
} as any;
|
||||
|
||||
this.owner = {
|
||||
editor: this.editor,
|
||||
editMode: true,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleMarkdownWrapper(wrapper: "**" | "*" | "~~" | "==") {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
const selectedText = sel.toString();
|
||||
|
||||
if (selectedText.length === 0) {
|
||||
// Empty selection: insert **** and place cursor in middle
|
||||
const text = wrapper + wrapper;
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStart(node, wrapper.length);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
} else {
|
||||
const alreadyWrapped = selectedText.startsWith(wrapper) && selectedText.endsWith(wrapper);
|
||||
const newText = alreadyWrapped
|
||||
? selectedText.slice(wrapper.length, -wrapper.length)
|
||||
: wrapper + selectedText + wrapper;
|
||||
|
||||
this.editor.replaceSelection(newText);
|
||||
}
|
||||
}
|
||||
|
||||
private async render(): Promise<void> {
|
||||
const currentRender = ++this.renderCount;
|
||||
this.el.empty();
|
||||
this.el.dataset.id = this.card.id;
|
||||
this.el.draggable = !this.isEditing; // Disable dragging while editing
|
||||
|
||||
// Apply styling
|
||||
applyCardColorToElement(this.el, this.card.color, this.store.settings);
|
||||
|
|
@ -33,18 +118,23 @@ export class CardComponent {
|
|||
// Apply max card height
|
||||
const maxH = this.store.settings.maxCardHeight;
|
||||
if (maxH && maxH > 0) {
|
||||
this.el.style.maxHeight = `${maxH}px`;
|
||||
this.el.style.overflow = 'hidden';
|
||||
this.el.style.setProperty('max-height', `${maxH}px`, 'important');
|
||||
this.el.style.setProperty('overflow', 'hidden', 'important');
|
||||
} else {
|
||||
this.el.style.maxHeight = '';
|
||||
this.el.style.overflow = '';
|
||||
this.el.style.removeProperty('max-height');
|
||||
this.el.style.removeProperty('overflow');
|
||||
}
|
||||
|
||||
// detect layout mode and apply masonry if needed
|
||||
if (this.store.settings.cardStyle === 2) {
|
||||
this.el.style.breakInside = 'avoid';
|
||||
}
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Copy button (if enabled)
|
||||
if (this.store.settings.enableCopyCardContent) {
|
||||
const copyBtn = fragment.createDiv('card-copy-btn');
|
||||
const copyBtn = fragment.createDiv('sc-copy-btn');
|
||||
try {
|
||||
setIcon(copyBtn, 'copy');
|
||||
} catch {
|
||||
|
|
@ -58,38 +148,36 @@ export class CardComponent {
|
|||
}
|
||||
|
||||
// Pill bar
|
||||
const pillBar = fragment.createDiv('card-pill-bar');
|
||||
const pillBar = fragment.createDiv('sc-pill-bar');
|
||||
this.renderPills(pillBar);
|
||||
|
||||
// Content
|
||||
const content = fragment.createDiv('card-content');
|
||||
const content = fragment.createDiv('sc-content');
|
||||
await this.renderContent(content);
|
||||
|
||||
// If a new render has started, discard this one
|
||||
if (currentRender !== this.renderCount) return;
|
||||
|
||||
// Tags inline (when groupTags is off, show tags right after content)
|
||||
if (!this.store.settings.groupTags && this.card.tags && this.card.tags.length > 0) {
|
||||
const tagsEl = fragment.createDiv('card-tags');
|
||||
const tagsEl = fragment.createDiv('sc-tags');
|
||||
this.card.tags.forEach(tag => {
|
||||
const tagEl = tagsEl.createSpan('card-tag');
|
||||
tagEl.textContent = this.store.settings.omitTagHash ? tag : `#${tag}`;
|
||||
const tagEl = tagsEl.createSpan('sc-tag');
|
||||
const cleanTag = tag.trim().replace(/^[-#\s]+/, '').trim();
|
||||
tagEl.textContent = this.store.settings.omitTagHash ? cleanTag : `#${cleanTag}`;
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// @ts-ignore
|
||||
this.store.eventBus.emit('filter:tag', tag);
|
||||
this.store.eventBus.emit('filter:tag', cleanTag);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Footer (grouped tags + timestamp)
|
||||
const footer = fragment.createDiv('card-footer');
|
||||
const footer = fragment.createDiv('sc-footer');
|
||||
this.renderFooter(footer);
|
||||
|
||||
this.el.appendChild(fragment);
|
||||
|
||||
// detect layout mode and apply masonry if needed
|
||||
if (this.store.settings.cardStyle === 2) {
|
||||
this.el.style.breakInside = 'avoid';
|
||||
this.el.style.marginBottom = '12px';
|
||||
}
|
||||
}
|
||||
|
||||
private copyCardContent(): void {
|
||||
|
|
@ -97,7 +185,7 @@ export class CardComponent {
|
|||
let contentToCopy = this.card.content;
|
||||
|
||||
// Try to get text from card content element
|
||||
const contentEl = this.el.querySelector('.card-content');
|
||||
const contentEl = this.el.querySelector('.sc-content');
|
||||
if (contentEl && !this.store.settings.disableCardRendering) {
|
||||
contentToCopy = contentEl.textContent || this.card.content;
|
||||
}
|
||||
|
|
@ -112,11 +200,11 @@ export class CardComponent {
|
|||
|
||||
private renderPills(container: HTMLElement): void {
|
||||
if (this.card.expiresAt) {
|
||||
const pill = container.createDiv('card-expiry-pill');
|
||||
const pill = container.createDiv('sc-expiry-pill');
|
||||
pill.textContent = this.formatExpiry(this.card.expiresAt);
|
||||
}
|
||||
if (this.card.status) {
|
||||
const pill = container.createDiv('card-status-pill');
|
||||
const pill = container.createDiv('sc-status-pill');
|
||||
pill.textContent = this.card.status.name;
|
||||
pill.style.backgroundColor = this.card.status.color;
|
||||
pill.style.color = this.card.status.textColor || '#000';
|
||||
|
|
@ -124,16 +212,75 @@ export class CardComponent {
|
|||
}
|
||||
|
||||
private async renderContent(container: HTMLElement): Promise<void> {
|
||||
if (this.store.settings.disableCardRendering) {
|
||||
if (this.isEditing || this.store.settings.disableCardRendering) {
|
||||
container.setAttribute('contenteditable', 'true');
|
||||
container.textContent = this.card.content;
|
||||
container.addClass('is-editing');
|
||||
|
||||
// Auto-focus and place cursor at the end
|
||||
setTimeout(() => {
|
||||
container.focus();
|
||||
const range = document.createRange();
|
||||
const sel = window.getSelection();
|
||||
range.selectNodeContents(container);
|
||||
range.collapse(false);
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
}, 0);
|
||||
|
||||
container.addEventListener('focusin', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.pushScope(this.scope);
|
||||
// @ts-ignore
|
||||
this.app.workspace.activeEditor = this.owner;
|
||||
});
|
||||
|
||||
container.addEventListener('blur', async () => {
|
||||
if (container.textContent !== this.card.content) {
|
||||
await this.store.update(this.card.id, { content: container.textContent || '' });
|
||||
// @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) {
|
||||
const newContent = container.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;
|
||||
}
|
||||
// Removed redundant this.render() - CardSidebarView will re-render everything on card:updated
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Enter and Shift+Enter according to settings
|
||||
container.addEventListener('keydown', async (e) => {
|
||||
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');
|
||||
const nextLineKey = normalizeKey(settings.nextLineKey || 'shift-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();
|
||||
container.blur();
|
||||
} else if (pressed === nextLineKey) {
|
||||
// Default behavior for contenteditable usually handles Enter/Shift+Enter
|
||||
// but if we need manual control, we can insert a newline here.
|
||||
}
|
||||
});
|
||||
} else {
|
||||
container.setAttribute('contenteditable', 'false');
|
||||
container.removeClass('is-editing');
|
||||
const temp = document.createElement('div');
|
||||
await MarkdownRenderer.render(
|
||||
this.app,
|
||||
|
|
@ -150,49 +297,68 @@ export class CardComponent {
|
|||
const settings = this.store.settings;
|
||||
const hasTags = this.card.tags && this.card.tags.length > 0;
|
||||
|
||||
if (settings.groupTags && !settings.timestampBelowTags) {
|
||||
// Tags then timestamp
|
||||
if (settings.groupTags) {
|
||||
if (hasTags) {
|
||||
const tagsEl = container.createDiv('card-tags');
|
||||
const tagsEl = container.createDiv('sc-tags');
|
||||
this.card.tags.forEach(tag => {
|
||||
const tagEl = tagsEl.createSpan('card-tag');
|
||||
tagEl.textContent = settings.omitTagHash ? tag : `#${tag}`;
|
||||
const tagEl = tagsEl.createSpan('sc-tag');
|
||||
const cleanTag = tag.trim().replace(/^[-#\s]+/, '').trim();
|
||||
tagEl.textContent = settings.omitTagHash ? cleanTag : `#${cleanTag}`;
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// @ts-ignore
|
||||
this.store.eventBus.emit('filter:tag', tag);
|
||||
this.store.eventBus.emit('filter:tag', cleanTag);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.showTimestamps) {
|
||||
container.createDiv('card-timestamp').textContent = this.formatTimestamp(this.card.created);
|
||||
}
|
||||
} else if (settings.groupTags && settings.timestampBelowTags) {
|
||||
// Tags then timestamp below
|
||||
if (hasTags) {
|
||||
const tagsEl = container.createDiv('card-tags');
|
||||
this.card.tags.forEach(tag => {
|
||||
const tagEl = tagsEl.createSpan('card-tag');
|
||||
tagEl.textContent = settings.omitTagHash ? tag : `#${tag}`;
|
||||
tagEl.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// @ts-ignore
|
||||
this.store.eventBus.emit('filter:tag', tag);
|
||||
});
|
||||
});
|
||||
}
|
||||
if (settings.showTimestamps) {
|
||||
container.createDiv('card-timestamp').textContent = this.formatTimestamp(this.card.created);
|
||||
const ts = container.createDiv('sc-timestamp');
|
||||
if (settings.timestampBelowTags) {
|
||||
ts.style.display = 'block';
|
||||
ts.style.marginTop = '4px';
|
||||
} else {
|
||||
ts.style.display = 'inline-block';
|
||||
ts.style.marginLeft = hasTags ? '8px' : '0';
|
||||
}
|
||||
ts.textContent = this.formatTimestamp(this.card.created);
|
||||
}
|
||||
} else {
|
||||
// Inline tags (rendered above footer in render()), just show timestamp here
|
||||
// Tags already rendered above footer in render()
|
||||
if (settings.showTimestamps) {
|
||||
container.createDiv('card-timestamp').textContent = this.formatTimestamp(this.card.created);
|
||||
container.createDiv('sc-timestamp').textContent = this.formatTimestamp(this.card.created);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupListeners(): void {
|
||||
// Enter edit mode on click
|
||||
this.el.addEventListener('click', (e) => {
|
||||
// If another card is already being edited, blur it first.
|
||||
if (CardComponent.activeEditor && CardComponent.activeEditor !== this) {
|
||||
CardComponent.activeEditor.blurAndSave();
|
||||
}
|
||||
|
||||
// Don't trigger if already editing or clicking a button/pill/tag
|
||||
if (this.isEditing) return;
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest('.sc-copy-btn') ||
|
||||
target.closest('.sc-expiry-pill') ||
|
||||
target.closest('.sc-status-pill') ||
|
||||
target.closest('.sc-tag') ||
|
||||
target.tagName === 'A' || // Don't trigger if clicking a link
|
||||
target.closest('button')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isEditing = true;
|
||||
CardComponent.activeEditor = this;
|
||||
this.render();
|
||||
});
|
||||
|
||||
// Context menu
|
||||
this.el.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -206,16 +372,25 @@ export class CardComponent {
|
|||
menu.addItem(item => {
|
||||
item.setTitle('Colors');
|
||||
const container = document.createElement('div');
|
||||
container.style.display = 'flex';
|
||||
container.style.gap = '6px';
|
||||
container.style.flexWrap = this.store.settings.twoRowSwatches ? 'wrap' : 'nowrap';
|
||||
if (this.store.settings.twoRowSwatches) container.style.maxWidth = '180px';
|
||||
container.className = 'sc-color-dots';
|
||||
if (this.store.settings.twoRowSwatches) {
|
||||
container.classList.add('two-row');
|
||||
container.style.display = 'grid';
|
||||
container.style.gridTemplateColumns = 'repeat(5, 18px)';
|
||||
container.style.gridAutoRows = '18px';
|
||||
container.style.columnGap = '8px';
|
||||
container.style.rowGap = '8px';
|
||||
container.style.width = 'fit-content';
|
||||
}
|
||||
|
||||
colors.forEach((color, idx) => {
|
||||
const swatch = document.createElement('div');
|
||||
swatch.className = 'sc-color-dot';
|
||||
swatch.style.width = '18px';
|
||||
swatch.style.height = '18px';
|
||||
swatch.style.borderRadius = '4px';
|
||||
swatch.style.cursor = 'pointer';
|
||||
swatch.style.flexShrink = '0';
|
||||
swatch.style.border = this.card.color === color ? '2px solid var(--text-accent)' : '1px solid var(--background-modifier-border)';
|
||||
swatch.title = this.store.settings.colorNames[idx] || `Color ${idx + 1}`;
|
||||
const root = document.documentElement;
|
||||
|
|
@ -316,4 +491,87 @@ export class CardComponent {
|
|||
if (!path) return;
|
||||
const file = this.app.vault.getAbstractFileByPath(path);
|
||||
if (file instanceof TFile) {
|
||||
await this
|
||||
await this.app.workspace.getLeaf(true).openFile(file);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
menu.addItem(item => {
|
||||
item.setTitle(this.card.archived ? 'Unarchive' : 'Archive')
|
||||
.setIcon('archive')
|
||||
.onClick(async () => {
|
||||
await this.store.toggleArchive(this.card.id, !this.card.archived);
|
||||
});
|
||||
});
|
||||
menu.addItem(item => {
|
||||
item.setTitle('Delete')
|
||||
.setIcon('trash')
|
||||
.onClick(async () => {
|
||||
await this.store.delete(this.card.id);
|
||||
});
|
||||
});
|
||||
menu.showAtMouseEvent(e);
|
||||
});
|
||||
|
||||
// Drag start
|
||||
this.el.addEventListener('dragstart', (e) => {
|
||||
if (this.isEditing) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
// @ts-ignore
|
||||
this.store.eventBus.emit('card:dragstart', { card: this.card, event: e });
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.setData('text/plain', this.card.content);
|
||||
e.dataTransfer.setData('application/json', JSON.stringify(this.card));
|
||||
}
|
||||
});
|
||||
|
||||
// Store updates
|
||||
// @ts-ignore
|
||||
const unbind = this.store.eventBus.on('card:updated', (updated: Card) => {
|
||||
if (updated.id === this.card.id) {
|
||||
this.card = updated;
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
this.unsubscribe.push(unbind);
|
||||
}
|
||||
|
||||
blurAndSave() {
|
||||
const contentEl = this.el.querySelector('.sc-content') as HTMLElement;
|
||||
if (contentEl) {
|
||||
contentEl.blur();
|
||||
}
|
||||
}
|
||||
|
||||
private formatTimestamp(ts: number): string {
|
||||
const fmt = this.store.settings.datetimeFormat;
|
||||
if (fmt && (window as any).moment) {
|
||||
return (window as any).moment(ts).format(fmt);
|
||||
}
|
||||
return new Date(ts).toLocaleDateString() + ' ' + new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
}
|
||||
|
||||
private formatExpiry(ts: number): string {
|
||||
const diff = ts - Date.now();
|
||||
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
|
||||
if (days < 0) return 'Expired';
|
||||
if (days === 0) return 'Expires today';
|
||||
if (days === 1) return 'Expires tomorrow';
|
||||
return `Expires in ${days} days`;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
if (CardComponent.activeEditor === this) {
|
||||
CardComponent.activeEditor = null;
|
||||
}
|
||||
CardComponent.instanceCount = Math.max(0, CardComponent.instanceCount - 1);
|
||||
if (CardComponent.instanceCount === 0 && CardComponent.globalMouseDownBound) {
|
||||
document.removeEventListener('mousedown', CardComponent.handleGlobalMouseDown, true);
|
||||
CardComponent.globalMouseDownBound = false;
|
||||
}
|
||||
this.unsubscribe.forEach(fn => fn());
|
||||
this.el.remove();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export class TagAutocomplete {
|
|||
private store: CardStore
|
||||
) {
|
||||
const parent = this.input.parentElement || document.body;
|
||||
this.container = parent.createDiv('card-tag-autocomplete');
|
||||
this.container = parent.createDiv('sc-tag-autocomplete');
|
||||
this.container.style.display = 'none';
|
||||
this.container.style.position = 'absolute';
|
||||
this.container.style.bottom = 'calc(100% + 4px)';
|
||||
|
|
@ -52,7 +52,7 @@ export class TagAutocomplete {
|
|||
this.container.empty();
|
||||
this.selectedIndex = -1;
|
||||
this.suggestions.forEach((tag, idx) => {
|
||||
const item = this.container.createDiv('card-autocomplete-item');
|
||||
const item = this.container.createDiv('sc-autocomplete-item');
|
||||
item.textContent = '#' + tag;
|
||||
item.addEventListener('mouseenter', () => {
|
||||
item.style.background = 'var(--background-modifier-hover)';
|
||||
|
|
@ -73,7 +73,7 @@ export class TagAutocomplete {
|
|||
|
||||
private onKeyDown(e: KeyboardEvent): void {
|
||||
if (this.container.style.display === 'none') return;
|
||||
const items = this.container.querySelectorAll('.card-autocomplete-item');
|
||||
const items = this.container.querySelectorAll('.sc-autocomplete-item');
|
||||
if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && items.length > 0) {
|
||||
e.preventDefault();
|
||||
if (e.key === 'ArrowDown') this.selectedIndex = (this.selectedIndex + 1) % items.length;
|
||||
|
|
|
|||
|
|
@ -1,75 +1,321 @@
|
|||
|
||||
import { Modal, App, Plugin, Notice, Setting } from "obsidian";
|
||||
import { Modal, App, Plugin, Notice, setIcon, Menu, Scope, Editor } from "obsidian";
|
||||
import { CardStore } from "../../services/CardStore";
|
||||
import { Card } from "../../models/Card";
|
||||
|
||||
export class QuickCardWithFilterModal extends Modal {
|
||||
private editorScope: Scope;
|
||||
private editor!: Editor;
|
||||
private owner!: any;
|
||||
|
||||
constructor(
|
||||
app: App,
|
||||
private plugin: Plugin,
|
||||
private store: CardStore
|
||||
) {
|
||||
super(app);
|
||||
this.editorScope = new Scope(this.app.scope);
|
||||
// Block native formatting keys so Obsidian's global commands take over
|
||||
this.editorScope.register(["Mod"], "b", () => true);
|
||||
this.editorScope.register(["Mod"], "i", () => true);
|
||||
this.editorScope.register(["Mod"], "u", () => true);
|
||||
this.setupMockEditor();
|
||||
}
|
||||
|
||||
private setupMockEditor() {
|
||||
this.editor = {
|
||||
getSelection: () => {
|
||||
const sel = window.getSelection();
|
||||
return sel ? sel.toString() : "";
|
||||
},
|
||||
replaceSelection: (text: string) => {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
},
|
||||
toggleBold: () => this.toggleMarkdownWrapper("**"),
|
||||
toggleItalic: () => this.toggleMarkdownWrapper("*"),
|
||||
} as any;
|
||||
|
||||
this.owner = {
|
||||
editor: this.editor,
|
||||
editMode: true,
|
||||
};
|
||||
}
|
||||
|
||||
private toggleMarkdownWrapper(wrapper: "**" | "*" | "~~" | "==") {
|
||||
const sel = window.getSelection();
|
||||
if (!sel || !sel.rangeCount) return;
|
||||
|
||||
const range = sel.getRangeAt(0);
|
||||
const selectedText = sel.toString();
|
||||
|
||||
if (selectedText.length === 0) {
|
||||
// Empty selection: insert **** and place cursor in middle
|
||||
const text = wrapper + wrapper;
|
||||
const node = document.createTextNode(text);
|
||||
range.insertNode(node);
|
||||
range.setStart(node, wrapper.length);
|
||||
range.collapse(true);
|
||||
sel.removeAllRanges();
|
||||
sel.addRange(range);
|
||||
} else {
|
||||
const alreadyWrapped = selectedText.startsWith(wrapper) && selectedText.endsWith(wrapper);
|
||||
const newText = alreadyWrapped
|
||||
? selectedText.slice(wrapper.length, -wrapper.length)
|
||||
: wrapper + selectedText + wrapper;
|
||||
|
||||
this.editor.replaceSelection(newText);
|
||||
}
|
||||
}
|
||||
|
||||
getAvailableFilters() {
|
||||
const filters = [
|
||||
{ type: 'all', label: 'All', value: 'all' }
|
||||
];
|
||||
|
||||
const showTimeBasedChips = !this.store.settings.disableTimeBasedFiltering;
|
||||
if (showTimeBasedChips) {
|
||||
filters.push(
|
||||
{ type: 'category', label: 'Today', value: 'today' },
|
||||
{ type: 'category', label: 'Tomorrow', value: 'tomorrow' }
|
||||
);
|
||||
}
|
||||
|
||||
if (this.store.settings.enableCustomCategories) {
|
||||
const cats = this.store.settings.customCategories || [];
|
||||
cats.forEach(cat => {
|
||||
filters.push({
|
||||
type: 'category',
|
||||
label: cat.label,
|
||||
value: cat.id || cat.label
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.store.settings.hideArchivedFilterButton) {
|
||||
filters.push({ type: 'archived', label: 'Archived', value: 'archived' });
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
contentEl.createEl('h2', { text: 'Quick Card Add' });
|
||||
contentEl.addClass('sc-quick-card-modal');
|
||||
|
||||
const textarea = contentEl.createEl('textarea', {
|
||||
placeholder: 'Card content...',
|
||||
cls: 'quick-card-textarea'
|
||||
contentEl.createEl('h2', { text: 'Quick Card Add', cls: 'sc-modal-title' });
|
||||
|
||||
// Content Section
|
||||
contentEl.createEl('h3', { text: 'Card Content', cls: 'sc-modal-section-title' });
|
||||
const editorEl = contentEl.createDiv({
|
||||
cls: 'sc-modal-textarea',
|
||||
});
|
||||
editorEl.setAttribute('contenteditable', 'true');
|
||||
editorEl.dataset.placeholder = 'Type here... (@category, #tag)';
|
||||
|
||||
// Simple placeholder logic for contenteditable
|
||||
if (!editorEl.textContent) {
|
||||
editorEl.addClass('is-empty');
|
||||
}
|
||||
editorEl.addEventListener('input', () => {
|
||||
editorEl.toggleClass('is-empty', !editorEl.textContent);
|
||||
});
|
||||
textarea.focus();
|
||||
|
||||
// Color swatches
|
||||
const colorContainer = contentEl.createDiv('color-container');
|
||||
editorEl.focus();
|
||||
|
||||
editorEl.addEventListener('focusin', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.pushScope(this.editorScope);
|
||||
// @ts-ignore
|
||||
this.app.workspace.activeEditor = this.owner;
|
||||
});
|
||||
|
||||
editorEl.addEventListener('blur', () => {
|
||||
// @ts-ignore
|
||||
this.app.keymap.popScope(this.editorScope);
|
||||
// @ts-ignore
|
||||
if (this.app.workspace.activeEditor === this.owner) {
|
||||
// @ts-ignore
|
||||
this.app.workspace.activeEditor = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Color Section
|
||||
contentEl.createEl('h3', { text: 'Color', cls: 'sc-modal-section-title' });
|
||||
const colorContainer = contentEl.createDiv('sc-modal-color-container');
|
||||
let selectedColor = 'var(--card-color-1)';
|
||||
const colors = [
|
||||
'var(--card-color-1)', 'var(--card-color-2)', 'var(--card-color-3)',
|
||||
'var(--card-color-4)', 'var(--card-color-5)', 'var(--card-color-6)',
|
||||
'var(--card-color-7)', 'var(--card-color-8)', 'var(--card-color-9)',
|
||||
'var(--card-color-10)'
|
||||
{ name: 'Gray', var: 'var(--card-color-1)' },
|
||||
{ name: 'Red', var: 'var(--card-color-2)' },
|
||||
{ name: 'Orange', var: 'var(--card-color-3)' },
|
||||
{ name: 'Yellow', var: 'var(--card-color-4)' },
|
||||
{ name: 'Green', var: 'var(--card-color-5)' },
|
||||
{ name: 'Blue', var: 'var(--card-color-6)' },
|
||||
{ name: 'Purple', var: 'var(--card-color-7)' },
|
||||
{ name: 'Magenta', var: 'var(--card-color-8)' },
|
||||
{ name: 'Pink', var: 'var(--card-color-9)' },
|
||||
{ name: 'Brown', var: 'var(--card-color-10)' }
|
||||
];
|
||||
|
||||
colors.forEach(color => {
|
||||
const swatch = colorContainer.createDiv('color-swatch');
|
||||
swatch.style.backgroundColor = color;
|
||||
colors.forEach((color, idx) => {
|
||||
const swatch = colorContainer.createDiv('sc-modal-color-swatch');
|
||||
const hex = this.resolveColor(color.var);
|
||||
swatch.style.backgroundColor = hex;
|
||||
swatch.title = this.store.settings.colorNames[idx] || color.name;
|
||||
|
||||
if (selectedColor === color.var) swatch.addClass('is-selected');
|
||||
|
||||
swatch.addEventListener('click', () => {
|
||||
colorContainer.querySelectorAll('.color-swatch').forEach(s => s.removeClass('selected'));
|
||||
swatch.addClass('selected');
|
||||
selectedColor = color;
|
||||
colorContainer.querySelectorAll('.sc-modal-color-swatch').forEach(s => s.removeClass('is-selected'));
|
||||
swatch.addClass('is-selected');
|
||||
selectedColor = color.var;
|
||||
});
|
||||
});
|
||||
|
||||
// Category select
|
||||
const select = contentEl.createEl('select', { cls: 'filter-select' });
|
||||
const categories = this.store.settings.customCategories || [];
|
||||
[ { id: 'all', label: 'All' }, ...categories ].forEach(cat => {
|
||||
const opt = select.createEl('option', { value: cat.id, text: cat.label });
|
||||
// Tags Section
|
||||
contentEl.createEl('h3', { text: 'Tags', cls: 'sc-modal-section-title' });
|
||||
const tagsWrapper = contentEl.createDiv('sc-modal-tags-wrapper');
|
||||
const tagsInput = tagsWrapper.createEl('input', {
|
||||
placeholder: 'Tags (comma separated)...',
|
||||
cls: 'sc-modal-tags-input'
|
||||
});
|
||||
const tagsAutocomplete = tagsWrapper.createDiv('sc-modal-tags-autocomplete');
|
||||
tagsAutocomplete.style.display = 'none';
|
||||
|
||||
// Create button
|
||||
const btnContainer = contentEl.createDiv('modal-button-container');
|
||||
const createBtn = btnContainer.createEl('button', { text: 'Create Card', cls: 'mod-cta' });
|
||||
createBtn.addEventListener('click', async () => {
|
||||
const content = textarea.value.trim();
|
||||
if (content) {
|
||||
const category = select.value === 'all' ? null : select.value;
|
||||
const card = new Card({ content, color: selectedColor, category });
|
||||
await this.store.add(card);
|
||||
this.close();
|
||||
} else {
|
||||
new Notice('Content cannot be empty');
|
||||
// Tag Autocomplete Logic
|
||||
let selectedTagIdx = -1;
|
||||
const updateAutocomplete = () => {
|
||||
const val = tagsInput.value;
|
||||
const lastComma = val.lastIndexOf(',');
|
||||
const currentTag = val.substring(lastComma + 1).trim().toLowerCase();
|
||||
|
||||
if (!currentTag) {
|
||||
tagsAutocomplete.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
const allTags = this.getAllTags();
|
||||
const suggestions = allTags.filter(t => t.startsWith(currentTag)).slice(0, 8);
|
||||
|
||||
if (suggestions.length === 0) {
|
||||
tagsAutocomplete.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
tagsAutocomplete.empty();
|
||||
selectedTagIdx = -1;
|
||||
suggestions.forEach((tag, idx) => {
|
||||
const item = tagsAutocomplete.createDiv('sc-modal-autocomplete-item');
|
||||
item.textContent = tag;
|
||||
item.addEventListener('click', () => {
|
||||
const before = val.substring(0, lastComma + 1);
|
||||
tagsInput.value = (before ? before + ' ' : '') + tag + ', ';
|
||||
tagsAutocomplete.style.display = 'none';
|
||||
tagsInput.focus();
|
||||
});
|
||||
});
|
||||
tagsAutocomplete.style.display = 'block';
|
||||
};
|
||||
|
||||
tagsInput.addEventListener('input', updateAutocomplete);
|
||||
tagsInput.addEventListener('keydown', (e) => {
|
||||
if (tagsAutocomplete.style.display === 'none') return;
|
||||
const items = tagsAutocomplete.querySelectorAll('.sc-modal-autocomplete-item');
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
selectedTagIdx = (selectedTagIdx + 1) % items.length;
|
||||
items.forEach((it, i) => it.toggleClass('is-selected', i === selectedTagIdx));
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
selectedTagIdx = (selectedTagIdx - 1 + items.length) % items.length;
|
||||
items.forEach((it, i) => it.toggleClass('is-selected', i === selectedTagIdx));
|
||||
} else if (e.key === 'Enter' && selectedTagIdx >= 0) {
|
||||
e.preventDefault();
|
||||
(items[selectedTagIdx] as HTMLElement).click();
|
||||
}
|
||||
});
|
||||
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
// Category Section
|
||||
contentEl.createEl('h3', { text: 'Apply Category', cls: 'sc-modal-section-title' });
|
||||
const select = contentEl.createEl('select', { cls: 'sc-modal-select' });
|
||||
this.getAvailableFilters().forEach(f => {
|
||||
const opt = select.createEl('option', { value: f.value, text: f.label });
|
||||
// @ts-ignore
|
||||
opt.dataset.type = f.type;
|
||||
});
|
||||
|
||||
// Action Buttons
|
||||
const btnContainer = contentEl.createDiv('sc-modal-buttons');
|
||||
const cancelBtn = btnContainer.createEl('button', { text: 'Cancel' });
|
||||
cancelBtn.addEventListener('click', () => this.close());
|
||||
|
||||
const createBtn = btnContainer.createEl('button', { text: 'Create Card', cls: 'mod-cta' });
|
||||
const handleCreate = async () => {
|
||||
const content = editorEl.textContent?.trim();
|
||||
if (!content) {
|
||||
new Notice('Content cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
const tags = tagsInput.value.split(',').map(t => t.trim()).filter(t => !!t);
|
||||
const category = select.value === 'all' ? null : select.value;
|
||||
|
||||
const card = new Card({ content, color: selectedColor, tags, category });
|
||||
await this.store.add(card);
|
||||
|
||||
// If we chose a specific category, try to filter the sidebar to it
|
||||
if (category) {
|
||||
const view = this.app.workspace.getLeavesOfType('card-sidebar')[0]?.view as any;
|
||||
if (view) {
|
||||
view.activeFilters.category = category;
|
||||
await view.renderCards();
|
||||
}
|
||||
}
|
||||
|
||||
this.close();
|
||||
};
|
||||
|
||||
createBtn.addEventListener('click', handleCreate);
|
||||
|
||||
// Keyboard Shortcuts
|
||||
editorEl.addEventListener('keydown', (e) => {
|
||||
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();
|
||||
createBtn.click();
|
||||
handleCreate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private resolveColor(colorVar: string): string {
|
||||
const root = document.documentElement;
|
||||
const clean = colorVar.replace('var(', '').replace(')', '');
|
||||
return getComputedStyle(root).getPropertyValue(clean).trim() || colorVar;
|
||||
}
|
||||
|
||||
private getAllTags(): string[] {
|
||||
const tags = new Set<string>();
|
||||
this.store.getAll().forEach(c => {
|
||||
if (c.tags) c.tags.forEach(t => tags.add(t.toLowerCase()));
|
||||
});
|
||||
return Array.from(tags).sort();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,14 +22,14 @@ export class SearchModal extends Modal {
|
|||
contentEl.empty();
|
||||
this.titleEl.setText('Search Cards');
|
||||
|
||||
const searchWrapper = contentEl.createDiv('card-search-wrapper');
|
||||
const searchWrapper = contentEl.createDiv('sc-search-wrapper');
|
||||
this.searchInput = searchWrapper.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Search cards...',
|
||||
cls: 'card-search-input'
|
||||
cls: 'sc-search-input'
|
||||
});
|
||||
|
||||
this.resultsContainer = contentEl.createDiv('card-search-results');
|
||||
this.resultsContainer = contentEl.createDiv('sc-search-results');
|
||||
this.resultsContainer.style.maxHeight = '400px';
|
||||
this.resultsContainer.style.overflow = 'auto';
|
||||
|
||||
|
|
@ -47,7 +47,7 @@ export class SearchModal extends Modal {
|
|||
this.selectedIndex = -1;
|
||||
|
||||
this.searchResults.forEach((card, idx) => {
|
||||
const result = this.resultsContainer.createDiv('card-search-result');
|
||||
const result = this.resultsContainer.createDiv('sc-search-result');
|
||||
result.textContent = card.content.substring(0, 100) + (card.content.length > 100 ? '...' : '');
|
||||
result.style.padding = '8px';
|
||||
result.style.cursor = 'pointer';
|
||||
|
|
@ -63,7 +63,7 @@ export class SearchModal extends Modal {
|
|||
}
|
||||
|
||||
private updateSelection() {
|
||||
const els = Array.from(this.resultsContainer.querySelectorAll('.card-search-result')) as HTMLElement[];
|
||||
const els = Array.from(this.resultsContainer.querySelectorAll('.sc-search-result')) as HTMLElement[];
|
||||
els.forEach((el, idx) => {
|
||||
if (idx === this.selectedIndex) {
|
||||
el.addClass('selected');
|
||||
|
|
|
|||
660
styles.css
660
styles.css
|
|
@ -1,93 +1,418 @@
|
|||
/* --- == SC == --- */
|
||||
|
||||
.sc-home-category-bar {
|
||||
margin: 0 !important;
|
||||
gap: 10px !important;
|
||||
}
|
||||
|
||||
.card-sidebar-header .category-group {
|
||||
.sc-color-dots.two-row {
|
||||
gap: 8px !important;
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
.sidecards-home, .sc-home-main {
|
||||
margin-top: 70px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sc-home-right,
|
||||
.sc-home-toolbar,
|
||||
.sc-home-category-bar,
|
||||
.sc-home-card-list {
|
||||
width: 100% !important;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sc-content p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sc-sidebar-header .sc-category-group {
|
||||
background: var(--background-primary);
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
padding: 6px 8px;
|
||||
padding: 10px;
|
||||
scrollbar-width: none;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
.card-category-btn {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 3px;
|
||||
.sc-sidebar-header .sc-category-group::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sc-category-btn {
|
||||
/* border: 2px solid var(--background-modifier-border); */
|
||||
border-radius: var(--button-radius);
|
||||
background: var(--background-primary);
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
transition: border-width 0s ease;
|
||||
}
|
||||
.card-category-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
.sc-category-btn:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.card-search-wrap {
|
||||
.sc-category-btn.active {
|
||||
border: 2px solid var(--interactive-accent);
|
||||
transition: border-width 0s ease;
|
||||
}
|
||||
|
||||
.sc-search-wrap {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
padding: 10px;
|
||||
background: var(--background-primary);
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.card-search-row {
|
||||
.sc-search-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-search-input-icon {
|
||||
.sc-search-input-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.card-search-input {
|
||||
.sc-search-input {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 8px 10px 8px 36px;
|
||||
padding: 8px 36px 8px 34px !important;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--input-radius);
|
||||
background: var(--background-primary-alt);
|
||||
color: var(--text-normal);
|
||||
font-size: 14px;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.card-search-input:focus {
|
||||
.sc-search-input::placeholder {
|
||||
color: var(--text-muted) !important;
|
||||
}
|
||||
|
||||
.sc-search-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--interactive-accent);
|
||||
background: var(--background-primary);
|
||||
box-shadow: 0 0 0 2px rgba(var(--interactive-accent), 0.1);
|
||||
}
|
||||
|
||||
.card-search-clear-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
.sc-search-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.card-search-clear-btn:hover {
|
||||
background: var(--background-modifier-hover);
|
||||
.sc-sidebar-input {
|
||||
width: 100%;
|
||||
min-height: 36px;
|
||||
max-height: 200px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--input-radius);
|
||||
background-color: var(--background-modifier-form-field);
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sc-sidebar-input.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--text-muted);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Home View Layout */
|
||||
.sc-home-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: var(--background-primary);
|
||||
}
|
||||
|
||||
.sc-home-main {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.sc-home-top {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.sc-home-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 20px 0;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.card-sidebar-cards-container {
|
||||
.sc-home-palette-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sc-home-category-btn {
|
||||
background-color: var(--background-modifier-form-field);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sc-home-separator {
|
||||
color: var(--background-modifier-border);
|
||||
margin: 0 4px;
|
||||
font-size: 18px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sc-home-color-dot {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: transform 0.1s ease, border-color 0.1s ease;
|
||||
}
|
||||
|
||||
.sc-home-color-dot:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
.sc-home-color-dot.is-selected {
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.sc-home-editor {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--input-radius);
|
||||
background-color: var(--background-modifier-form-field);
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sc-home-editor.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--text-muted);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.sc-home-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 280px 1fr;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.sc-home-section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px 0;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.sc-home-file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.sc-home-file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background-color: var(--background-modifier-form-field);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s ease;
|
||||
}
|
||||
|
||||
.sc-home-file-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.sc-home-file-icon {
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sc-home-file-name {
|
||||
font-size: 14px;
|
||||
color: var(--text-normal);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sc-home-empty-msg {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: 4px 12px;
|
||||
}
|
||||
|
||||
/* Right Column Toolbar */
|
||||
.sc-home-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sc-home-toolbar-left,
|
||||
.sc-home-toolbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sc-home-search-wrap {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--background-modifier-form-field);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.sc-home-search-icon {
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sc-home-search-input {
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
padding: 6px 8px !important;
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sc-home-category-bar {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 16px;
|
||||
margin-bottom: 16px;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.sc-home-category-bar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sc-home-card-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sc-home-card-list .sc-card {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
||||
.workspace-leaf-content .sc-home-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
@container (max-width: 768px) {
|
||||
.sc-home-left {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sc-home-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sc-home-main {
|
||||
padding: 20px 12px;
|
||||
}
|
||||
|
||||
.sidecards-home, .sc-home-main {
|
||||
margin: 50px 10px !important;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.sc-home-right,
|
||||
.sc-home-toolbar,
|
||||
.sc-home-category-bar,
|
||||
.sc-home-card-list {
|
||||
width: 100% !important;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sc-search-clear-btn {
|
||||
font-size: 20px;
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
padding: 4px 10px;
|
||||
border-radius: 100%;
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
color: var(--text-error) !important;
|
||||
background: transparent !important;
|
||||
/* background-color: transparent !important; */
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sc-search-clear-btn:hover {
|
||||
color: var(--text-normal);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sc-sidebar-cards-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
|
|
@ -98,7 +423,18 @@
|
|||
-ms-overflow-style: auto;
|
||||
}
|
||||
|
||||
.card-sidebar-cards-container.grid-mode {
|
||||
.sc-sidebar-cards-container.hide-scrollbar {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.sc-sidebar-cards-container.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.sc-sidebar-cards-container.grid-mode {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
||||
grid-auto-rows: 1px;
|
||||
|
|
@ -106,17 +442,16 @@
|
|||
align-items: start;
|
||||
}
|
||||
|
||||
.card-sidebar-cards-container.grid-mode .card-sidebar-card {
|
||||
.sc-sidebar-cards-container.grid-mode .sc-card {
|
||||
margin: 0 !important;
|
||||
align-self: start;
|
||||
height: auto;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
.card-sidebar-card {
|
||||
.sc-card {
|
||||
background: var(--background-primary);
|
||||
padding: 12px;
|
||||
padding-top: 6px;
|
||||
position: relative;
|
||||
/* font-size: 14px; */
|
||||
line-height: 1.4;
|
||||
|
|
@ -124,7 +459,7 @@
|
|||
border-radius: var(--card-border-radius, 6px);
|
||||
}
|
||||
|
||||
.card-sidebar-card .card-copy-btn {
|
||||
.sc-card .sc-copy-btn {
|
||||
color: var(--text-normal);
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
|
|
@ -141,19 +476,264 @@
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.card-sidebar-card:hover .card-copy-btn {
|
||||
.sc-card:hover .sc-copy-btn {
|
||||
display: flex;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.card-sidebar-card .card-copy-btn:hover {
|
||||
.sc-card .sc-copy-btn:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.card-sidebar-card .card-copy-btn:active {
|
||||
.sc-card .sc-copy-btn:active {
|
||||
transform: scale(0.8);
|
||||
}
|
||||
|
||||
.category-row {
|
||||
.sc-category-row {
|
||||
border-radius: var(--setting-items-radius) !important;
|
||||
}
|
||||
|
||||
.sc-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sc-tag {
|
||||
font-size: 11px;
|
||||
color: var(--text-accent);
|
||||
background-color: hsla(var(--interactive-accent-hsl), 0.2);
|
||||
border-radius: var(--tag-radius);
|
||||
padding: 1px 5px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
/* .sc-tag:hover {
|
||||
color: var(--text-accent);
|
||||
} */
|
||||
|
||||
.sc-card .sc-content.is-editing {
|
||||
outline: none;
|
||||
background: var(--background-primary-alt);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
min-height: 1.4em;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.sc-card .sc-content {
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.sc-timestamp {
|
||||
font-size: 11px;
|
||||
color: var(--text-normal) !important;
|
||||
opacity: 0.7 !important;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sc-footer {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.sc-color-dots {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: nowrap;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
/* .sc-color-dots.two-row {
|
||||
flex-wrap: wrap;
|
||||
width: 114px;
|
||||
} */
|
||||
|
||||
.sc-color-dot {
|
||||
width: 22px !important;
|
||||
height: 22px !important;
|
||||
border-radius: 10px !important;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
de
|
||||
.sc-home-color-dot {
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid var(--background-primary);
|
||||
}
|
||||
|
||||
.sc-icon-btn {
|
||||
padding: 6px;
|
||||
background-color: var(--background-modifier-form-field);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.sc-icon-btn:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.sc-icon-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.sc-color-dot:hover {
|
||||
transform: scale(1.15);
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sidecards-home-palette-row {
|
||||
gap: 4px !important;
|
||||
}
|
||||
.sidecards-home-category-btn {
|
||||
font-size: 10px;
|
||||
}
|
||||
.sidecards-home-separator {
|
||||
margin: 0px 2px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sc-status-pill {
|
||||
display: inline;
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sc-pill-bar {
|
||||
margin-bottom: 6px !important;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.sc-quick-card-modal .sc-modal-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
padding: 10px;
|
||||
border-radius: var(--input-radius);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
background-color: var(--background-modifier-form-field);
|
||||
font-family: var(--font-text);
|
||||
resize: vertical;
|
||||
white-space: pre-wrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-textarea.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
color: var(--text-muted);
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-color-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-color-swatch {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
border: 2px solid transparent;
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-color-swatch:hover {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-color-swatch.is-selected {
|
||||
border-color: var(--text-accent);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-tags-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-tags-input {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: var(--input-radius);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-tags-autocomplete {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: var(--input-radius);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
margin-top: 4px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-autocomplete-item {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-autocomplete-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-autocomplete-item:hover,
|
||||
.sc-quick-card-modal .sc-modal-autocomplete-item.is-selected {
|
||||
background: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border-radius: var(--input-radius);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
}
|
||||
|
||||
.sc-quick-card-modal .sc-modal-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.sidecards-home-separator {
|
||||
height: 24px;
|
||||
width: 1px;
|
||||
background-color: var(--background-modifier-border);
|
||||
}
|
||||
Loading…
Reference in a new issue