diff --git a/main.ts b/main.ts index 508853f..2aea67e 100644 --- a/main.ts +++ b/main.ts @@ -1,703 +1,1081 @@ import { App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile, Menu, Modal, View, WorkspaceLeaf, addIcon } from 'obsidian'; -addIcon('status-pane', ` - +// Define icons as constants +const ICONS = { + statusPane: ` - -`); + `, + search: ` + + + `, + clear: ` + + + `, + standardView: ``, + compactView: ``, + refresh: ``, + collapseDown: ``, + collapseRight: ``, + file: `` +}; + +// Add custom icons to Obsidian +addIcon('status-pane', ICONS.statusPane); + +// TypeScript interfaces +interface Status { + name: string; + icon: string; +} interface NoteStatusSettings { - mySetting: string; - statusColors: Record; - showStatusDropdown: boolean; - showStatusBar: boolean; - dropdownPosition: 'top' | 'bottom'; - statusBarPosition: 'left' | 'right'; - autoHideStatusBar: boolean; - customStatuses: { name: string; icon: string }[]; - showStatusIconsInExplorer: boolean; - collapsedStatuses: Record; + statusColors: Record; + showStatusDropdown: boolean; + showStatusBar: boolean; + dropdownPosition: 'top' | 'bottom'; + statusBarPosition: 'left' | 'right'; + autoHideStatusBar: boolean; + customStatuses: Status[]; + showStatusIconsInExplorer: boolean; + collapsedStatuses: Record; + compactView?: boolean; } const DEFAULT_SETTINGS: NoteStatusSettings = { - mySetting: 'default', - statusColors: { - active: 'var(--text-success)', - onHold: 'var(--text-warning)', - completed: 'var(--text-accent)', - dropped: 'var(--text-error)', - unknown: 'var(--text-muted)' - }, - showStatusDropdown: true, - showStatusBar: true, - dropdownPosition: 'top', - statusBarPosition: 'right', - autoHideStatusBar: false, - customStatuses: [ - { name: 'active', icon: '▶️' }, - { name: 'onHold', icon: '⏸️' }, - { name: 'completed', icon: '✅' }, - { name: 'dropped', icon: '❌' }, - { name: 'unknown', icon: '❓' } - ], - showStatusIconsInExplorer: true, - collapsedStatuses: {} -} + statusColors: { + active: 'var(--text-success)', + onHold: 'var(--text-warning)', + completed: 'var(--text-accent)', + dropped: 'var(--text-error)', + unknown: 'var(--text-muted)' + }, + showStatusDropdown: true, + showStatusBar: true, + dropdownPosition: 'top', + statusBarPosition: 'right', + autoHideStatusBar: false, + customStatuses: [ + { name: 'active', icon: '▶️' }, + { name: 'onHold', icon: '⏸️' }, + { name: 'completed', icon: '✅' }, + { name: 'dropped', icon: '❌' }, + { name: 'unknown', icon: '❓' } + ], + showStatusIconsInExplorer: true, + collapsedStatuses: {}, + compactView: false +}; +// Default colors in hexadecimal format +const DEFAULT_COLORS: Record = { + active: '#00ff00', // Green for success + onHold: '#ffaa00', // Orange for warning + completed: '#00aaff', // Blue for accent + dropped: '#ff0000', // Red for error + unknown: '#888888' // Gray for muted +}; + +/** + * Status Pane View for managing note statuses + */ class StatusPaneView extends View { - plugin: NoteStatus; - searchInput: HTMLInputElement | null = null; + plugin: NoteStatus; + searchInput: HTMLInputElement | null = null; - constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { - super(leaf); - this.plugin = plugin; - } + constructor(leaf: WorkspaceLeaf, plugin: NoteStatus) { + super(leaf); + this.plugin = plugin; + } - getViewType() { return 'status-pane'; } - getDisplayText() { return 'Status Pane'; } - getIcon() { return 'status-pane'; } + getViewType() { return 'status-pane'; } + getDisplayText() { return 'Status Pane'; } + getIcon() { return 'status-pane'; } - async onOpen() { - await this.setupPane(); - await this.renderGroups(''); - } + async onOpen() { + await this.setupPane(); + await this.renderGroups(''); + } - async setupPane() { - const { containerEl } = this; - containerEl.empty(); - containerEl.addClass('note-status-pane', 'nav-files-container'); + async setupPane() { + const { containerEl } = this; + containerEl.empty(); + containerEl.addClass('note-status-pane', 'nav-files-container'); - const searchContainer = containerEl.createDiv({ cls: 'note-status-search search-input-container' }); - this.searchInput = searchContainer.createEl('input', { - type: 'text', - placeholder: 'Search notes...', - cls: 'note-status-search-input search-input' - }); - this.searchInput.addEventListener('input', () => { - this.renderGroups(this.searchInput!.value.toLowerCase()); - }); + // Add a header container for better layout + const headerContainer = containerEl.createDiv({ cls: 'note-status-header' }); - const actionsContainer = containerEl.createDiv({ cls: 'status-pane-actions-container' }); - const refreshButton = actionsContainer.createEl('button', { - type: 'button', - title: 'Refresh Statuses', - text: 'Refresh', - cls: 'note-status-actions-refresh clickable-icon mod-cta' - }); - refreshButton.addEventListener('click', async () => { - await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); - new Notice('Status pane refreshed'); - }); - } + // Search container with search input + this.createSearchInput(headerContainer); + + // Actions toolbar (view toggle, refresh) + this.createActionToolbar(headerContainer); - async renderGroups(searchQuery = '') { - const { containerEl } = this; - const existingGroups = containerEl.querySelectorAll('.status-group'); - existingGroups.forEach(group => group.remove()); + // Set initial compact view state + containerEl.toggleClass('compact-view', this.plugin.settings.compactView); - const statusGroups: Record = {}; - this.plugin.settings.customStatuses.forEach(status => { - statusGroups[status.name] = []; - }); + // Add a container for the groups + containerEl.createDiv({ cls: 'note-status-groups-container' }); + } - const files = this.app.vault.getMarkdownFiles(); - for (const file of files) { - const cachedMetadata = this.app.metadataCache.getFileCache(file); - let status = 'unknown'; - if (cachedMetadata?.frontmatter?.status) { - const frontmatterStatus = cachedMetadata.frontmatter.status.toLowerCase(); - const matchingStatus = this.plugin.settings.customStatuses.find(s => s.name.toLowerCase() === frontmatterStatus); - if (matchingStatus) status = matchingStatus.name; - } - statusGroups[status].push(file); - } + private createSearchInput(container: HTMLElement) { + const searchContainer = container.createDiv({ cls: 'note-status-search search-input-container' }); + const searchWrapper = searchContainer.createDiv({ cls: 'search-input-wrapper' }); - Object.entries(statusGroups).forEach(([status, files]) => { - const filteredFiles = files.filter(file => file.basename.toLowerCase().includes(searchQuery)); - if (filteredFiles.length > 0) { - const groupEl = containerEl.createDiv({ cls: 'status-group nav-folder' }); - const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); - const titleSpan = titleEl.createSpan({ - text: `${status} ${this.plugin.getStatusIcon(status)} (${filteredFiles.length})`, - cls: `status-${status}` - }); - titleEl.style.cursor = 'pointer'; - const isCollapsed = this.plugin.settings.collapsedStatuses[status] ?? false; - if (isCollapsed) groupEl.addClass('is-collapsed'); + // Search icon + searchWrapper.createEl('span', { cls: 'search-input-icon' }).innerHTML = ICONS.search; - titleEl.addEventListener('click', (e) => { - e.preventDefault(); - groupEl.classList.toggle('is-collapsed'); - this.plugin.settings.collapsedStatuses[status] = !this.plugin.settings.collapsedStatuses[status]; - this.plugin.saveSettings(); - }); + // Create the search input + this.searchInput = searchWrapper.createEl('input', { + type: 'text', + placeholder: 'Search notes...', + cls: 'note-status-search-input search-input' + }); - const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' }); - filteredFiles.sort((a, b) => a.basename.localeCompare(b.basename)).forEach(file => { - const fileEl = childrenEl.createDiv({ cls: 'nav-file' }); - const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' }); - fileTitleEl.createSpan({ - text: file.basename, - cls: 'nav-file-title-content' - }); - fileEl.addEventListener('click', (e) => { - e.preventDefault(); - this.app.workspace.openLinkText(file.path, file.path, true); - }); - }); - } - }); - } + // Add search event listener + this.searchInput.addEventListener('input', () => { + this.renderGroups(this.searchInput!.value.toLowerCase()); + this.toggleClearButton(); + }); - onClose() { - this.containerEl.empty(); - return Promise.resolve(); - } + // Clear search button (hidden by default) + const clearSearchBtn = searchWrapper.createEl('span', { cls: 'search-input-clear-button' }); + clearSearchBtn.innerHTML = ICONS.clear; + + clearSearchBtn.addEventListener('click', () => { + if (this.searchInput) { + this.searchInput.value = ''; + this.renderGroups(''); + this.toggleClearButton(); + } + }); + } + + private toggleClearButton() { + const clearBtn = this.containerEl.querySelector('.search-input-clear-button'); + if (clearBtn && this.searchInput) { + clearBtn.toggleClass('is-visible', !!this.searchInput.value); + } + } + + private createActionToolbar(container: HTMLElement) { + const actionsContainer = container.createDiv({ cls: 'status-pane-actions-container' }); + + // Toggle compact view button + const viewToggleButton = actionsContainer.createEl('button', { + type: 'button', + title: this.plugin.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View', + cls: 'note-status-view-toggle clickable-icon' + }); + + viewToggleButton.innerHTML = this.plugin.settings.compactView ? ICONS.compactView : ICONS.standardView; + + viewToggleButton.addEventListener('click', async () => { + this.plugin.settings.compactView = !this.plugin.settings.compactView; + viewToggleButton.title = this.plugin.settings.compactView ? 'Switch to Standard View' : 'Switch to Compact View'; + viewToggleButton.innerHTML = this.plugin.settings.compactView ? ICONS.compactView : ICONS.standardView; + + await this.plugin.saveSettings(); + this.containerEl.toggleClass('compact-view', this.plugin.settings.compactView); + await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + }); + + // Refresh button + const refreshButton = actionsContainer.createEl('button', { + type: 'button', + title: 'Refresh Statuses', + cls: 'note-status-actions-refresh clickable-icon' + }); + + refreshButton.innerHTML = ICONS.refresh; + + refreshButton.addEventListener('click', async () => { + await this.renderGroups(this.searchInput?.value.toLowerCase() || ''); + new Notice('Status pane refreshed'); + }); + } + + async renderGroups(searchQuery = '') { + const { containerEl } = this; + const groupsContainer = containerEl.querySelector('.note-status-groups-container'); + if (!groupsContainer) return; + + groupsContainer.empty(); + + // Group files by status + const statusGroups = this.groupFilesByStatus(searchQuery); + + // Render each status group + Object.entries(statusGroups).forEach(([status, files]) => { + if (files.length > 0) { + this.renderStatusGroup(groupsContainer, status, files); + } + }); + } + + private groupFilesByStatus(searchQuery: string): Record { + const statusGroups: Record = {}; + + // Initialize groups for each status + this.plugin.settings.customStatuses.forEach(status => { + statusGroups[status.name] = []; + }); + + // Get all markdown files and filter by search query + const files = this.app.vault.getMarkdownFiles(); + + for (const file of files) { + if (!searchQuery || file.basename.toLowerCase().includes(searchQuery)) { + const status = this.plugin.getFileStatus(file); + statusGroups[status].push(file); + } + } + + return statusGroups; + } + + private renderStatusGroup(container: HTMLElement, status: string, files: TFile[]) { + const groupEl = container.createDiv({ cls: 'status-group nav-folder' }); + const titleEl = groupEl.createDiv({ cls: 'nav-folder-title' }); + + // Create a container for the collapse button and title + const collapseContainer = titleEl.createDiv({ cls: 'collapse-indicator' }); + collapseContainer.innerHTML = ICONS.collapseDown; + + // Create a container for the title content + const titleContentContainer = titleEl.createDiv({ cls: 'nav-folder-title-content' }); + + const statusIcon = this.plugin.getStatusIcon(status); + titleContentContainer.createSpan({ + text: `${status} ${statusIcon} (${files.length})`, + cls: `status-${status}` + }); + + // Handle collapsing/expanding behavior + titleEl.style.cursor = 'pointer'; + const isCollapsed = this.plugin.settings.collapsedStatuses[status] ?? false; + + if (isCollapsed) { + groupEl.addClass('is-collapsed'); + collapseContainer.innerHTML = ICONS.collapseRight; + } + + titleEl.addEventListener('click', (e) => { + e.preventDefault(); + groupEl.toggleClass('is-collapsed'); + this.plugin.settings.collapsedStatuses[status] = groupEl.hasClass('is-collapsed'); + + // Update the collapse icon + collapseContainer.innerHTML = groupEl.hasClass('is-collapsed') + ? ICONS.collapseRight + : ICONS.collapseDown; + + this.plugin.saveSettings(); + }); + + // Create and populate child elements + const childrenEl = groupEl.createDiv({ cls: 'nav-folder-children' }); + + // Sort files by name + files.sort((a, b) => a.basename.localeCompare(b.basename)); + + // Create file list items + files.forEach(file => { + this.createFileListItem(childrenEl, file, status); + }); + } + + private createFileListItem(container: HTMLElement, file: TFile, status: string) { + const fileEl = container.createDiv({ cls: 'nav-file' }); + const fileTitleEl = fileEl.createDiv({ cls: 'nav-file-title' }); + + // Add file icon if in standard view + if (!this.plugin.settings.compactView) { + const fileIcon = fileTitleEl.createDiv({ cls: 'nav-file-icon' }); + fileIcon.innerHTML = ICONS.file; + } + + // Add file name + fileTitleEl.createSpan({ + text: file.basename, + cls: 'nav-file-title-content' + }); + + // Add status indicator + fileTitleEl.createSpan({ + cls: `note-status-icon nav-file-tag status-${status}`, + text: this.plugin.getStatusIcon(status) + }); + + // Add click handler to open the file + fileEl.addEventListener('click', (e) => { + e.preventDefault(); + this.app.workspace.openLinkText(file.path, file.path, true); + }); + + // Add context menu + fileEl.addEventListener('contextmenu', (e) => { + e.preventDefault(); + this.showFileContextMenu(e, file); + }); + } + + private showFileContextMenu(e: MouseEvent, file: TFile) { + const menu = new Menu(); + + // Add status change options + menu.addItem((item) => + item.setTitle('Change Status') + .setIcon('tag') + .onClick(() => { + this.plugin.showStatusContextMenu([file]); + }) + ); + + // Add open options + menu.addItem((item) => + item.setTitle('Open in New Tab') + .setIcon('lucide-external-link') + .onClick(() => { + this.app.workspace.openLinkText(file.path, file.path, 'tab'); + }) + ); + + menu.showAtMouseEvent(e); + } + + onClose() { + this.containerEl.empty(); + return Promise.resolve(); + } } +/** + * Main plugin class + */ export default class NoteStatus extends Plugin { - settings: NoteStatusSettings; - statusBarItem: HTMLElement; - currentStatus = 'unknown'; - statusDropdownContainer?: HTMLElement; - private statusPaneLeaf: WorkspaceLeaf | null = null; - private dynamicStyleEl?: HTMLStyleElement; // Para los estilos dinámicos + settings: NoteStatusSettings; + statusBarItem: HTMLElement; + currentStatus = 'unknown'; + statusDropdownContainer?: HTMLElement; + private statusPaneLeaf: WorkspaceLeaf | null = null; + private dynamicStyleEl?: HTMLStyleElement; - // Colores por defecto en formato hexadecimal (aproximados para temas comunes de Obsidian) - private readonly defaultColors: Record = { - active: '#00ff00', // Verde para success - onHold: '#ffaa00', // Naranja para warning - completed: '#00aaff', // Azul para accent - dropped: '#ff0000', // Rojo para error - unknown: '#888888' // Gris para muted - }; + async onload() { + // Load settings and set defaults + await this.loadSettings(); + this.initializeDefaultColors(); - async onload() { - await this.loadSettings(); - // Asegurar que los colores por defecto estén inicializados - for (const [status, color] of Object.entries(this.defaultColors)) { - if (!this.settings.statusColors[status]) { - this.settings.statusColors[status] = color; - } - } - await this.saveSettings(); + // Initialize dynamic styles + this.updateDynamicStyles(); - // Registrar CSS - this.register(() => { - const styleEl = document.createElement('style'); - styleEl.textContent = require('./styles.css').default; - document.head.appendChild(styleEl); - }); + // Register the status pane view + this.registerView('status-pane', (leaf) => { + this.statusPaneLeaf = leaf; + return new StatusPaneView(leaf, this); + }); - // Inicializar estilos dinámicos - this.updateDynamicStyles(); + // Add ribbon icon + this.addRibbonIcon('status-pane', 'Open Status Pane', () => { + this.openStatusPane(); + }); - this.registerView('status-pane', (leaf) => { - this.statusPaneLeaf = leaf; - return new StatusPaneView(leaf, this); - }); + // Add status bar item + this.initializeStatusBar(); - this.addRibbonIcon('status-pane', 'Open Status Pane', () => { - this.openStatusPane(); - }); + // Register commands + this.registerCommands(); - this.statusBarItem = this.addStatusBarItem(); - this.statusBarItem.addClass('note-status-bar'); - this.statusBarItem.addEventListener('click', () => { - this.settings.showStatusDropdown = !this.settings.showStatusDropdown; - this.updateStatusDropdown(); - this.saveSettings(); - new Notice(`Status dropdown ${this.settings.showStatusDropdown ? 'shown' : 'hidden'}`); - }); - this.updateStatusBar(); + // Register file menu and context menu handlers + this.registerMenuHandlers(); - this.addCommand({ - id: 'refresh-status', - name: 'Refresh Status', - callback: () => { - this.checkNoteStatus(); - new Notice('Note status refreshed!'); - } - }); + // Register workspace and vault events + this.registerEvents(); - this.addCommand({ - id: 'batch-update-status', - name: 'Batch Update Status', - callback: () => this.showBatchStatusModal() - }); + // Initialize UI on layout ready + this.app.workspace.onLayoutReady(async () => { + // Small delay to ensure everything is loaded + await new Promise(resolve => setTimeout(resolve, 500)); + this.checkNoteStatus(); + this.updateStatusDropdown(); + this.updateAllFileExplorerIcons(); + }); - this.registerEvent( - this.app.workspace.on('file-menu', (menu, file, source) => { - if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { - menu.addItem((item) => - item - .setTitle('Change Status of Selected Files') - .setIcon('tag') - .onClick(() => { - const selectedFiles = this.getSelectedFiles(); - if (selectedFiles.length > 1) { - this.showBatchStatusContextMenu(selectedFiles); - } else { - this.showBatchStatusContextMenu([file]); - } - }) - ); - } - }) - ); + // Add settings tab + this.addSettingTab(new NoteStatusSettingTab(this.app, this)); + } - this.registerEvent( - this.app.workspace.on('files-menu', (menu, files) => { - const mdFiles = files.filter(file => file instanceof TFile && file.extension === 'md') as TFile[]; - if (mdFiles.length > 0) { - menu.addItem((item) => - item - .setTitle('Change Status of Selected Files') - .setIcon('tag') - .onClick(() => { - this.showBatchStatusContextMenu(mdFiles); - }) - ); - } - }) - ); + private initializeDefaultColors() { + // Ensure default colors are set for all statuses + for (const [status, color] of Object.entries(DEFAULT_COLORS)) { + if (!this.settings.statusColors[status]) { + this.settings.statusColors[status] = color; + } + } + } - this.addCommand({ - id: 'insert-status-metadata', - name: 'Insert Status Metadata', - editorCallback: (editor: Editor, view: MarkdownView) => { - const content = editor.getValue(); - const statusMetadata = 'status: unknown'; - const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/); - if (frontMatterMatch) { - const frontMatter = frontMatterMatch[1]; - let updatedFrontMatter = frontMatter; - if (/^status:/.test(frontMatter)) { - updatedFrontMatter = frontMatter.replace(/^status: .*/m, statusMetadata); - } else { - updatedFrontMatter = `${frontMatter}\n${statusMetadata}`; - } - const updatedContent = content.replace(/^---\n([\s\S]+?)\n---/, `---\n${updatedFrontMatter}\n---`); - editor.setValue(updatedContent); - } else { - const newFrontMatter = `---\n${statusMetadata}\n---\n${content}`; - editor.setValue(newFrontMatter); - } - } - }); + private initializeStatusBar() { + this.statusBarItem = this.addStatusBarItem(); + this.statusBarItem.addClass('note-status-bar'); + + // Add click handler to toggle status dropdown + this.statusBarItem.addEventListener('click', () => { + this.settings.showStatusDropdown = !this.settings.showStatusDropdown; + this.updateStatusDropdown(); + this.saveSettings(); + new Notice(`Status dropdown ${this.settings.showStatusDropdown ? 'shown' : 'hidden'}`); + }); + + this.updateStatusBar(); + } - this.addCommand({ - id: 'open-status-pane', - name: 'Open Status Pane', - callback: () => this.openStatusPane() - }); + private registerCommands() { + // Refresh status command + this.addCommand({ + id: 'refresh-status', + name: 'Refresh Status', + callback: () => { + this.checkNoteStatus(); + new Notice('Note status refreshed!'); + } + }); - this.registerEvent( - this.app.workspace.on('editor-menu', (menu, editor, view) => { - if (view instanceof MarkdownView) { - menu.addItem((item) => - item - .setTitle('Change Note Status') - .setIcon('tag') - .onClick(() => this.showStatusDropdown(editor, view)) - ); - } - }) - ); + // Batch update status command + this.addCommand({ + id: 'batch-update-status', + name: 'Batch Update Status', + callback: () => this.showBatchStatusModal() + }); - this.registerEvent(this.app.workspace.on('file-open', () => { - this.checkNoteStatus(); - this.updateStatusDropdown(); - })); - this.registerEvent(this.app.workspace.on('editor-change', () => this.checkNoteStatus())); - this.registerEvent(this.app.workspace.on('active-leaf-change', () => { - this.updateStatusDropdown(); - this.updateStatusPane(); - })); - this.registerEvent(this.app.vault.on('modify', (file) => { - if (file instanceof TFile) { - this.updateFileExplorerIcons(file); - this.updateStatusPane(); - } - })); - this.registerEvent(this.app.vault.on('create', () => { - this.updateAllFileExplorerIcons(); - this.updateStatusPane(); - })); - this.registerEvent(this.app.vault.on('delete', () => { - this.updateAllFileExplorerIcons(); - this.updateStatusPane(); - })); - this.registerEvent(this.app.vault.on('rename', () => { - this.updateAllFileExplorerIcons(); - this.updateStatusPane(); - })); - this.registerEvent(this.app.metadataCache.on('changed', (file) => { - this.checkNoteStatus(); - this.updateFileExplorerIcons(file); - this.updateStatusPane(); - })); - this.registerEvent(this.app.metadataCache.on('resolved', () => { - this.updateAllFileExplorerIcons(); - })); + // Insert status metadata command + this.addCommand({ + id: 'insert-status-metadata', + name: 'Insert Status Metadata', + editorCallback: (editor: Editor, view: MarkdownView) => { + this.insertStatusMetadataInEditor(editor); + } + }); - this.app.workspace.onLayoutReady(async () => { - await new Promise(resolve => setTimeout(resolve, 500)); - this.checkNoteStatus(); - this.updateStatusDropdown(); - this.updateAllFileExplorerIcons(); - }); + // Open status pane command + this.addCommand({ + id: 'open-status-pane', + name: 'Open Status Pane', + callback: () => this.openStatusPane() + }); + } - this.addSettingTab(new NoteStatusSettingTab(this.app, this)); - } + private registerMenuHandlers() { + // File explorer context menu + this.registerEvent( + this.app.workspace.on('file-menu', (menu, file, source) => { + if (source === 'file-explorer-context-menu' && file instanceof TFile && file.extension === 'md') { + menu.addItem((item) => + item + .setTitle('Change Status of Selected Files') + .setIcon('tag') + .onClick(() => { + const selectedFiles = this.getSelectedFiles(); + if (selectedFiles.length > 1) { + this.showBatchStatusContextMenu(selectedFiles); + } else { + this.showBatchStatusContextMenu([file]); + } + }) + ); + } + }) + ); - async openStatusPane() { - const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (existing) { - this.app.workspace.setActiveLeaf(existing); - await this.updateStatusPane(); - } else { - const leaf = this.app.workspace.getLeftLeaf(false); - if (leaf) await leaf.setViewState({ type: 'status-pane', active: true }); - } - } + // Multiple files selection menu + this.registerEvent( + this.app.workspace.on('files-menu', (menu, files) => { + const mdFiles = files.filter(file => file instanceof TFile && file.extension === 'md') as TFile[]; + if (mdFiles.length > 0) { + menu.addItem((item) => + item + .setTitle('Change Status of Selected Files') + .setIcon('tag') + .onClick(() => { + this.showBatchStatusContextMenu(mdFiles); + }) + ); + } + }) + ); - private getSelectedFiles(): TFile[] { - const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (!fileExplorer || !fileExplorer.view || !('fileItems' in fileExplorer.view)) { - console.log('File explorer not found or no file items'); - return []; - } + // Editor context menu + this.registerEvent( + this.app.workspace.on('editor-menu', (menu, editor, view) => { + if (view instanceof MarkdownView) { + menu.addItem((item) => + item + .setTitle('Change Note Status') + .setIcon('tag') + .onClick(() => this.showStatusDropdown(editor, view)) + ); + } + }) + ); + } - const fileItems = fileExplorer.view.fileItems as Record; - const selectedFiles: TFile[] = []; - Object.entries(fileItems).forEach(([_, item]) => { - if (item.el?.classList.contains('is-selected') && item.file instanceof TFile && item.file.extension === 'md') { - selectedFiles.push(item.file); - } - }); - return selectedFiles; - } + private registerEvents() { + // File open event + this.registerEvent(this.app.workspace.on('file-open', () => { + this.checkNoteStatus(); + this.updateStatusDropdown(); + })); - private showBatchStatusContextMenu(files: TFile[]) { - const menu = new Menu(); - this.settings.customStatuses - .filter(status => status.name !== 'unknown') - .forEach(status => { - menu.addItem((item) => - item - .setTitle(`${status.name} ${status.icon}`) - .setIcon('tag') - .onClick(async () => { - for (const file of files) { - await this.updateNoteStatus(status.name, file); - } - new Notice(`Updated status of ${files.length} file${files.length === 1 ? '' : 's'} to ${status.name}`); - }) - ); - }); - menu.showAtMouseEvent(new MouseEvent('contextmenu')); - } + // Editor change event + this.registerEvent(this.app.workspace.on('editor-change', () => this.checkNoteStatus())); - async updateNoteStatus(newStatus: string, file?: TFile) { - const targetFile = file || this.app.workspace.getActiveFile(); - if (!targetFile || targetFile.extension !== 'md') return; + // Active leaf change event + this.registerEvent(this.app.workspace.on('active-leaf-change', () => { + this.updateStatusDropdown(); + this.updateStatusPane(); + })); - const content = await this.app.vault.read(targetFile); - let newContent = content; - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?/); - if (frontmatterMatch) { - const frontmatter = frontmatterMatch[1]; - if (frontmatter.includes('status:')) { - newContent = content.replace( - /^---\n([\s\S]*?)status:\s*\w+([\s\S]*?)\n---\n?/, - `---\n$1status: ${newStatus}$2\n---\n` - ); - } else { - newContent = content.replace( - /^---\n([\s\S]*?)\n---\n?/, - `---\n$1\nstatus: ${newStatus}\n---\n` - ); - } - } else { - newContent = `---\nstatus: ${newStatus}\n---\n${content.trim()}`; - } - newContent = newContent.replace(/\n{3,}/g, '\n\n'); - await this.app.vault.modify(targetFile, newContent); + // File modification events + this.registerEvent(this.app.vault.on('modify', (file) => { + if (file instanceof TFile) { + this.updateFileExplorerIcons(file); + this.updateStatusPane(); + } + })); - if (targetFile === this.app.workspace.getActiveFile()) { - this.currentStatus = newStatus; - this.updateStatusBar(); - this.updateStatusDropdown(); - } - this.updateFileExplorerIcons(targetFile); - this.updateStatusPane(); - } + // File creation event + this.registerEvent(this.app.vault.on('create', () => { + this.updateAllFileExplorerIcons(); + this.updateStatusPane(); + })); - async checkNoteStatus() { - const activeFile = this.app.workspace.getActiveFile(); - if (!activeFile || activeFile.extension !== 'md') { - this.currentStatus = 'unknown'; - this.updateStatusBar(); - this.updateStatusDropdown(); - return; - } + // File deletion event + this.registerEvent(this.app.vault.on('delete', () => { + this.updateAllFileExplorerIcons(); + this.updateStatusPane(); + })); - const cachedMetadata = this.app.metadataCache.getFileCache(activeFile); - this.currentStatus = 'unknown'; - if (cachedMetadata?.frontmatter?.status) { - const status = cachedMetadata.frontmatter.status.toLowerCase(); - const matchingStatus = this.settings.customStatuses.find(s => s.name.toLowerCase() === status); - if (matchingStatus) this.currentStatus = matchingStatus.name; - } - this.updateStatusBar(); - this.updateStatusDropdown(); - this.updateFileExplorerIcons(activeFile); - } + // File rename event + this.registerEvent(this.app.vault.on('rename', () => { + this.updateAllFileExplorerIcons(); + this.updateStatusPane(); + })); - async showBatchStatusModal() { - const modal = new Modal(this.app); - modal.contentEl.createEl('h2', { text: 'Batch Update Note Status' }); + // Metadata change events + this.registerEvent(this.app.metadataCache.on('changed', (file) => { + this.checkNoteStatus(); + this.updateFileExplorerIcons(file); + this.updateStatusPane(); + })); - const fileSelect = modal.contentEl.createEl('select', { - cls: 'batch-file-select', - attr: { multiple: 'true' } - }); - const mdFiles = this.app.vault.getMarkdownFiles(); - mdFiles.forEach(file => { - fileSelect.createEl('option', { text: file.path, value: file.path }); - }); + // Metadata resolved event + this.registerEvent(this.app.metadataCache.on('resolved', () => { + this.updateAllFileExplorerIcons(); + })); + } - const statusSelect = modal.contentEl.createEl('select', { cls: 'batch-status-select' }); - this.settings.customStatuses.forEach(status => { - statusSelect.createEl('option', { text: `${status.name} ${status.icon}`, value: status.name }); - }); + async openStatusPane() { + const existing = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (existing) { + this.app.workspace.setActiveLeaf(existing); + await this.updateStatusPane(); + } else { + const leaf = this.app.workspace.getLeftLeaf(false); + if (leaf) await leaf.setViewState({ type: 'status-pane', active: true }); + } + } - const applyButton = modal.contentEl.createEl('button', { - text: 'Apply Status', - cls: 'mod-cta' - }); - applyButton.addEventListener('click', async () => { - const selectedFiles = Array.from(fileSelect.selectedOptions) - .map(opt => mdFiles.find(f => f.path === opt.value)) - .filter(Boolean) as TFile[]; - const newStatus = statusSelect.value; - for (const file of selectedFiles) { - await this.updateNoteStatus(newStatus, file); - } - new Notice(`Updated status of ${selectedFiles.length} notes to ${newStatus}`); - modal.close(); - }); + getSelectedFiles(): TFile[] { + const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (!fileExplorer || !fileExplorer.view || !('fileItems' in fileExplorer.view)) { + console.log('File explorer not found or no file items'); + return []; + } - modal.open(); - } + const fileItems = fileExplorer.view.fileItems as Record; + const selectedFiles: TFile[] = []; + + Object.entries(fileItems).forEach(([_, item]) => { + if (item.el?.classList.contains('is-selected') && item.file instanceof TFile && item.file.extension === 'md') { + selectedFiles.push(item.file); + } + }); + + return selectedFiles; + } - updateStatusBar() { - this.statusBarItem.empty(); - this.statusBarItem.removeClass('left', 'hidden', 'auto-hide', 'visible'); - this.statusBarItem.addClass('note-status-bar'); + showStatusContextMenu(files: TFile[]) { + this.showBatchStatusContextMenu(files); + } - if (!this.settings.showStatusBar) { - this.statusBarItem.addClass('hidden'); - return; - } + showBatchStatusContextMenu(files: TFile[]) { + const menu = new Menu(); + + this.settings.customStatuses + .filter(status => status.name !== 'unknown') + .forEach(status => { + menu.addItem((item) => + item + .setTitle(`${status.name} ${status.icon}`) + .setIcon('tag') + .onClick(async () => { + for (const file of files) { + await this.updateNoteStatus(status.name, file); + } + new Notice(`Updated status of ${files.length} file${files.length === 1 ? '' : 's'} to ${status.name}`); + }) + ); + }); + + menu.showAtMouseEvent(new MouseEvent('contextmenu')); + } - if (this.settings.statusBarPosition === 'left') { - this.statusBarItem.addClass('left'); - } + async updateNoteStatus(newStatus: string, file?: TFile) { + const targetFile = file || this.app.workspace.getActiveFile(); + if (!targetFile || targetFile.extension !== 'md') return; - this.statusBarItem.createEl('span', { - text: `Status: ${this.currentStatus}`, - cls: `note-status-${this.currentStatus}` - }); - this.statusBarItem.createEl('span', { - text: this.getStatusIcon(this.currentStatus), - cls: `note-status-icon status-${this.currentStatus}` - }); + const content = await this.app.vault.read(targetFile); + let newContent = content; + + // Handle frontmatter update + const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n?/); + if (frontmatterMatch) { + const frontmatter = frontmatterMatch[1]; + if (frontmatter.includes('status:')) { + newContent = content.replace( + /^---\n([\s\S]*?)status:\s*[^\n]+([\s\S]*?)\n---\n?/, + `---\n$1status: ${newStatus}$2\n---\n` + ); + } else { + newContent = content.replace( + /^---\n([\s\S]*?)\n---\n?/, + `---\n$1\nstatus: ${newStatus}\n---\n` + ); + } + } else { + newContent = `---\nstatus: ${newStatus}\n---\n${content.trim()}`; + } + + // Clean up excess newlines + newContent = newContent.replace(/\n{3,}/g, '\n\n'); + + // Update the file + await this.app.vault.modify(targetFile, newContent); - if (this.settings.autoHideStatusBar && this.currentStatus === 'unknown') { - this.statusBarItem.addClass('auto-hide'); - setTimeout(() => { - if (this.currentStatus === 'unknown' && this.settings.showStatusBar) { - this.statusBarItem.addClass('hidden'); - } - }, 500); - } else { - this.statusBarItem.addClass('visible'); - } - } + // Update UI if this is the active file + if (targetFile === this.app.workspace.getActiveFile()) { + this.currentStatus = newStatus; + this.updateStatusBar(); + this.updateStatusDropdown(); + } + + this.updateFileExplorerIcons(targetFile); + this.updateStatusPane(); + } - getStatusIcon(status: string): string { - const customStatus = this.settings.customStatuses.find(s => s.name.toLowerCase() === status.toLowerCase()); - return customStatus ? customStatus.icon : '❓'; - } + /** + * Get the status of a file from its metadata + */ + getFileStatus(file: TFile): string { + const cachedMetadata = this.app.metadataCache.getFileCache(file); + let status = 'unknown'; + + if (cachedMetadata?.frontmatter?.status) { + const frontmatterStatus = cachedMetadata.frontmatter.status.toLowerCase(); + const matchingStatus = this.settings.customStatuses.find(s => + s.name.toLowerCase() === frontmatterStatus); + + if (matchingStatus) status = matchingStatus.name; + } + + return status; + } - async updateStatusPane() { - if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { - const searchQuery = (this.statusPaneLeaf.view as StatusPaneView).searchInput?.value.toLowerCase() || ''; - await (this.statusPaneLeaf.view as StatusPaneView).renderGroups(searchQuery); - } - } + checkNoteStatus() { + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile || activeFile.extension !== 'md') { + this.currentStatus = 'unknown'; + this.updateStatusBar(); + this.updateStatusDropdown(); + return; + } - showStatusDropdown(editor: Editor, view: MarkdownView) { - const menu = new Menu(); - this.settings.customStatuses - .filter(status => status.name !== 'unknown') - .forEach(status => { - menu.addItem((item) => - item - .setTitle(`${status.name} ${status.icon}`) - .setIcon('tag') - .onClick(async () => { - await this.updateNoteStatus(status.name); - }) - ); - }); + this.currentStatus = this.getFileStatus(activeFile); + this.updateStatusBar(); + this.updateStatusDropdown(); + this.updateFileExplorerIcons(activeFile); + } - const cursor = editor.getCursor('to'); - editor.posToOffset(cursor); - const editorEl = view.contentEl.querySelector('.cm-content'); - const rect = editorEl?.getBoundingClientRect(); - if (rect) { - menu.showAtPosition({ x: rect.left, y: rect.bottom }); - } else { - menu.showAtPosition({ x: 0, y: 0 }); - } - } + async showBatchStatusModal() { + const modal = new Modal(this.app); + modal.contentEl.createEl('h2', { text: 'Batch Update Note Status' }); + modal.contentEl.addClass('note-status-batch-modal'); - updateStatusDropdown() { - if (!this.settings.showStatusDropdown) { - if (this.statusDropdownContainer) { - this.statusDropdownContainer.remove(); - this.statusDropdownContainer = undefined; - } - return; - } + // Create file selection with search filter + const searchContainer = modal.contentEl.createDiv({ cls: 'note-status-modal-search' }); + const searchInput = searchContainer.createEl('input', { + type: 'text', + placeholder: 'Filter files...', + cls: 'note-status-modal-search-input' + }); - const view = this.app.workspace.getActiveViewOfType(MarkdownView); - if (!view) { - if (this.statusDropdownContainer) { - this.statusDropdownContainer.remove(); - this.statusDropdownContainer = undefined; - } - return; - } + // File selection container + const fileSelectContainer = modal.contentEl.createDiv({ cls: 'note-status-file-select-container' }); + const fileSelect = fileSelectContainer.createEl('select', { + cls: 'note-status-file-select', + attr: { multiple: 'true', size: '10' } + }); - const contentEl = view.contentEl; - if (!this.statusDropdownContainer) { - this.statusDropdownContainer = this.settings.dropdownPosition === 'top' - ? contentEl.insertBefore(document.createElement('div'), contentEl.firstChild) - : contentEl.appendChild(document.createElement('div')); - this.statusDropdownContainer.addClass('note-status-dropdown', this.settings.dropdownPosition); - } + // Get all markdown files and populate select + const mdFiles = this.app.vault.getMarkdownFiles(); + const populateFiles = (filter = '') => { + fileSelect.empty(); + mdFiles + .filter(file => !filter || file.path.toLowerCase().includes(filter.toLowerCase())) + .sort((a, b) => a.path.localeCompare(b.path)) + .forEach(file => { + const status = this.getFileStatus(file); + const option = fileSelect.createEl('option', { + text: `${file.path} ${this.getStatusIcon(status)}`, + value: file.path + }); + option.classList.add(`status-${status}`); + }); + }; - this.statusDropdownContainer.empty(); - this.statusDropdownContainer.createEl('span', { text: 'Status:', cls: 'note-status-label' }); - const select = this.statusDropdownContainer.createEl('select', { cls: 'note-status-select dropdown' }); + populateFiles(); - this.settings.customStatuses.forEach(status => { - const option = select.createEl('option', { text: `${status.name} ${status.icon}`, value: status.name }); - if (status.name === this.currentStatus) option.selected = true; - }); + // Add search functionality + searchInput.addEventListener('input', () => { + populateFiles(searchInput.value); + }); - select.addEventListener('change', async (e) => { - const newStatus = (e.target as HTMLSelectElement).value; - if (newStatus !== 'unknown') await this.updateNoteStatus(newStatus); - }); + // Status selection + const statusSelectContainer = modal.contentEl.createDiv({ cls: 'note-status-status-select-container' }); + const statusSelect = statusSelectContainer.createEl('select', { cls: 'note-status-status-select' }); + + // Add status options + this.settings.customStatuses.forEach(status => { + const option = statusSelect.createEl('option', { + text: `${status.name} ${status.icon}`, + value: status.name + }); + option.classList.add(`status-${status.name}`); + }); - const hideButton = this.statusDropdownContainer.createEl('button', { - text: 'Hide Bar', - cls: 'note-status-hide-button clickable-icon mod-cta' - }); - hideButton.addEventListener('click', () => { - this.settings.showStatusDropdown = false; - this.updateStatusDropdown(); - this.saveSettings(); - new Notice('Status dropdown hidden'); - }); - } + // Add action buttons + const buttonContainer = modal.contentEl.createDiv({ cls: 'note-status-modal-buttons' }); + + // Select all button + const selectAllButton = buttonContainer.createEl('button', { + text: 'Select All', + cls: 'note-status-select-all' + }); + + selectAllButton.addEventListener('click', () => { + for (const option of Array.from(fileSelect.options)) { + option.selected = true; + } + }); + + // Apply button + const applyButton = buttonContainer.createEl('button', { + text: 'Apply Status', + cls: 'mod-cta' + }); + + applyButton.addEventListener('click', async () => { + const selectedFiles = Array.from(fileSelect.selectedOptions) + .map(opt => mdFiles.find(f => f.path === opt.value)) + .filter(Boolean) as TFile[]; + + if (selectedFiles.length === 0) { + new Notice('No files selected'); + return; + } + + const newStatus = statusSelect.value; + for (const file of selectedFiles) { + await this.updateNoteStatus(newStatus, file); + } + + new Notice(`Updated status of ${selectedFiles.length} note${selectedFiles.length !== 1 ? 's' : ''} to ${newStatus}`); + modal.close(); + }); - async updateFileExplorerIcons(file: TFile) { - if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; + modal.open(); + } - const cachedMetadata = this.app.metadataCache.getFileCache(file); - let status = 'unknown'; - if (cachedMetadata?.frontmatter?.status) { - const frontmatterStatus = cachedMetadata.frontmatter.status.toLowerCase(); - const matchingStatus = this.settings.customStatuses.find(s => s.name.toLowerCase() === frontmatterStatus); - if (matchingStatus) status = matchingStatus.name; - } + private insertStatusMetadataInEditor(editor: Editor) { + const content = editor.getValue(); + const statusMetadata = 'status: unknown'; + + // Check if frontmatter exists + const frontMatterMatch = content.match(/^---\n([\s\S]+?)\n---/); + + if (frontMatterMatch) { + const frontMatter = frontMatterMatch[1]; + let updatedFrontMatter = frontMatter; + + // Check if status already exists in frontmatter + if (/^status:/.test(frontMatter)) { + updatedFrontMatter = frontMatter.replace(/^status: .*/m, statusMetadata); + } else { + updatedFrontMatter = `${frontMatter}\n${statusMetadata}`; + } + + const updatedContent = content.replace(/^---\n([\s\S]+?)\n---/, `---\n${updatedFrontMatter}\n---`); + editor.setValue(updatedContent); + } else { + // Create new frontmatter if it doesn't exist + const newFrontMatter = `---\n${statusMetadata}\n---\n${content}`; + editor.setValue(newFrontMatter); + } + } - const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (fileExplorer && fileExplorer.view && 'fileItems' in fileExplorer.view) { - const fileItems = fileExplorer.view.fileItems as Record; - const fileItem = fileItems[file.path]; - if (fileItem) { - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (titleEl) { - const existingIcon = titleEl.querySelector('.note-status-icon'); - if (existingIcon) existingIcon.remove(); + updateStatusBar() { + this.statusBarItem.empty(); + this.statusBarItem.removeClass('left', 'hidden', 'auto-hide', 'visible'); + this.statusBarItem.addClass('note-status-bar'); - titleEl.createEl('span', { - cls: `note-status-icon nav-file-tag status-${status}`, - text: this.getStatusIcon(status) - }); - } - } - } - } + if (!this.settings.showStatusBar) { + this.statusBarItem.addClass('hidden'); + return; + } - updateAllFileExplorerIcons() { - if (!this.settings.showStatusIconsInExplorer) { - const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (fileExplorer && fileExplorer.view && 'fileItems' in fileExplorer.view) { - const fileItems = fileExplorer.view.fileItems as Record; - Object.values(fileItems).forEach((fileItem) => { - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (titleEl) { - const existingIcon = titleEl.querySelector('.note-status-icon'); - if (existingIcon) existingIcon.remove(); - } - }); - } - return; - } + // Add left class if needed + if (this.settings.statusBarPosition === 'left') { + this.statusBarItem.addClass('left'); + } - const files = this.app.vault.getMarkdownFiles(); - files.forEach(file => this.updateFileExplorerIcons(file)); - } + // Create status text + this.statusBarItem.createEl('span', { + text: `Status: ${this.currentStatus}`, + cls: `note-status-${this.currentStatus}` + }); + + // Create status icon + this.statusBarItem.createEl('span', { + text: this.getStatusIcon(this.currentStatus), + cls: `note-status-icon status-${this.currentStatus}` + }); - onunload() { - this.statusBarItem.remove(); - if (this.statusDropdownContainer) this.statusDropdownContainer.remove(); - const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; - if (fileExplorer && fileExplorer.view && 'fileItems' in fileExplorer.view) { - const fileItems = fileExplorer.view.fileItems as Record; - Object.values(fileItems).forEach((fileItem) => { - const titleEl = fileItem.titleEl || fileItem.selfEl; - if (titleEl) { - const existingIcon = titleEl.querySelector('.note-status-icon'); - if (existingIcon) existingIcon.remove(); - } - }); - } - const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPane) statusPane.detach(); - } + // Handle auto-hide behavior + if (this.settings.autoHideStatusBar && this.currentStatus === 'unknown') { + this.statusBarItem.addClass('auto-hide'); + setTimeout(() => { + if (this.currentStatus === 'unknown' && this.settings.showStatusBar) { + this.statusBarItem.addClass('hidden'); + } + }, 500); + } else { + this.statusBarItem.addClass('visible'); + } + } - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } + getStatusIcon(status: string): string { + const customStatus = this.settings.customStatuses.find(s => s.name.toLowerCase() === status.toLowerCase()); + return customStatus ? customStatus.icon : '❓'; + } - private updateDynamicStyles() { - if (!this.dynamicStyleEl) { - this.dynamicStyleEl = document.createElement('style'); - document.head.appendChild(this.dynamicStyleEl); - } + async updateStatusPane() { + if (this.statusPaneLeaf && this.statusPaneLeaf.view instanceof StatusPaneView) { + const searchQuery = (this.statusPaneLeaf.view as StatusPaneView).searchInput?.value.toLowerCase() || ''; + await (this.statusPaneLeaf.view as StatusPaneView).renderGroups(searchQuery); + } + } - let css = ''; - for (const [status, color] of Object.entries(this.settings.statusColors)) { - css += ` + showStatusDropdown(editor: Editor, view: MarkdownView) { + const menu = new Menu(); + + // Add status options to menu + this.settings.customStatuses + .filter(status => status.name !== 'unknown') + .forEach(status => { + menu.addItem((item) => + item + .setTitle(`${status.name} ${status.icon}`) + .setIcon('tag') + .onClick(async () => { + await this.updateNoteStatus(status.name); + }) + ); + }); + + // Position menu near cursor + const cursor = editor.getCursor('to'); + editor.posToOffset(cursor); + const editorEl = view.contentEl.querySelector('.cm-content'); + + if (editorEl) { + const rect = editorEl.getBoundingClientRect(); + menu.showAtPosition({ x: rect.left, y: rect.bottom }); + } else { + menu.showAtPosition({ x: 0, y: 0 }); + } + } + + updateStatusDropdown() { + // Remove existing dropdown if setting is turned off + if (!this.settings.showStatusDropdown) { + if (this.statusDropdownContainer) { + this.statusDropdownContainer.remove(); + this.statusDropdownContainer = undefined; + } + return; + } + + // Check for active markdown view + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + if (!view) { + if (this.statusDropdownContainer) { + this.statusDropdownContainer.remove(); + this.statusDropdownContainer = undefined; + } + return; + } + + // Create or update the dropdown container + const contentEl = view.contentEl; + if (!this.statusDropdownContainer) { + this.statusDropdownContainer = this.settings.dropdownPosition === 'top' + ? contentEl.insertBefore(document.createElement('div'), contentEl.firstChild) + : contentEl.appendChild(document.createElement('div')); + + this.statusDropdownContainer.addClass('note-status-dropdown', this.settings.dropdownPosition); + } + + // Populate the dropdown + this.statusDropdownContainer.empty(); + + // Add label + this.statusDropdownContainer.createEl('span', { text: 'Status:', cls: 'note-status-label' }); + + // Add select element + const select = this.statusDropdownContainer.createEl('select', { cls: 'note-status-select dropdown' }); + + // Add status options + this.settings.customStatuses.forEach(status => { + const option = select.createEl('option', { + text: `${status.name} ${status.icon}`, + value: status.name + }); + + if (status.name === this.currentStatus) option.selected = true; + }); + + // Add change event listener + select.addEventListener('change', async (e) => { + const newStatus = (e.target as HTMLSelectElement).value; + if (newStatus !== 'unknown') await this.updateNoteStatus(newStatus); + }); + + // Add hide button + const hideButton = this.statusDropdownContainer.createEl('button', { + text: 'Hide Bar', + cls: 'note-status-hide-button clickable-icon mod-cta' + }); + + hideButton.addEventListener('click', () => { + this.settings.showStatusDropdown = false; + this.updateStatusDropdown(); + this.saveSettings(); + new Notice('Status dropdown hidden'); + }); + } + + async updateFileExplorerIcons(file: TFile) { + if (!this.settings.showStatusIconsInExplorer || file.extension !== 'md') return; + + const status = this.getFileStatus(file); + const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; + + if (fileExplorer && fileExplorer.view && 'fileItems' in fileExplorer.view) { + const fileItems = fileExplorer.view.fileItems as Record; + const fileItem = fileItems[file.path]; + + if (fileItem) { + const titleEl = fileItem.titleEl || fileItem.selfEl; + if (titleEl) { + // Remove existing icon if present + const existingIcon = titleEl.querySelector('.note-status-icon'); + if (existingIcon) existingIcon.remove(); + + // Add new icon + titleEl.createEl('span', { + cls: `note-status-icon nav-file-tag status-${status}`, + text: this.getStatusIcon(status) + }); + } + } + } + } + + updateAllFileExplorerIcons() { + // Remove all icons if setting is turned off + if (!this.settings.showStatusIconsInExplorer) { + this.removeAllFileExplorerIcons(); + return; + } + + // Update icons for all markdown files + const files = this.app.vault.getMarkdownFiles(); + files.forEach(file => this.updateFileExplorerIcons(file)); + } + + private removeAllFileExplorerIcons() { + const fileExplorer = this.app.workspace.getLeavesOfType('file-explorer')[0]; + if (fileExplorer && fileExplorer.view && 'fileItems' in fileExplorer.view) { + const fileItems = fileExplorer.view.fileItems as Record; + + Object.values(fileItems).forEach((fileItem) => { + const titleEl = fileItem.titleEl || fileItem.selfEl; + if (titleEl) { + const existingIcon = titleEl.querySelector('.note-status-icon'); + if (existingIcon) existingIcon.remove(); + } + }); + } + } + + onunload() { + // Clean up UI elements + this.statusBarItem.remove(); + + if (this.statusDropdownContainer) { + this.statusDropdownContainer.remove(); + } + + // Remove file explorer icons + this.removeAllFileExplorerIcons(); + + // Close status pane if open + const statusPane = this.app.workspace.getLeavesOfType('status-pane')[0]; + if (statusPane) statusPane.detach(); + + // Remove dynamic styles + if (this.dynamicStyleEl) { + this.dynamicStyleEl.remove(); + } + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + private updateDynamicStyles() { + // Create style element if it doesn't exist + if (!this.dynamicStyleEl) { + this.dynamicStyleEl = document.createElement('style'); + document.head.appendChild(this.dynamicStyleEl); + } + + // Generate CSS for status colors + let css = ''; + for (const [status, color] of Object.entries(this.settings.statusColors)) { + css += ` .status-${status} { color: ${color} !important; } @@ -706,185 +1084,259 @@ export default class NoteStatus extends Plugin { color: ${color} !important; } `; - } - this.dynamicStyleEl.textContent = css; - } + } + + // Add modern styling for the batch modal + css += ` + .note-status-batch-modal { + max-width: 500px; + } + + .note-status-modal-search { + margin-bottom: 10px; + } + + .note-status-modal-search-input { + width: 100%; + padding: var(--input-padding); + border-radius: var(--input-radius); + } + + .note-status-file-select, + .note-status-status-select { + width: 100%; + margin-bottom: 10px; + border-radius: var(--input-radius); + } + + .note-status-modal-buttons { + display: flex; + justify-content: space-between; + margin-top: 15px; + } + `; + + this.dynamicStyleEl.textContent = css; + } - async saveSettings() { - await this.saveData(this.settings); - this.updateStatusPane(); - this.updateDynamicStyles(); - this.updateStatusPane(); - // this.updateStatusBar(); - this.updateStatusDropdown(); - this.updateAllFileExplorerIcons(); - } + async saveSettings() { + await this.saveData(this.settings); + this.updateDynamicStyles(); + this.updateStatusPane(); + this.updateStatusBar(); + this.updateStatusDropdown(); + this.updateAllFileExplorerIcons(); + } - async resetDefaultColors() { - const defaultStatuses = ['active', 'onHold', 'completed', 'dropped', 'unknown']; - for (const status of defaultStatuses) { - if (this.settings.customStatuses.some(s => s.name === status)) { - this.settings.statusColors[status] = this.defaultColors[status]; - } - } - await this.saveSettings(); - } + async resetDefaultColors() { + // Reset colors for default statuses + const defaultStatuses = ['active', 'onHold', 'completed', 'dropped', 'unknown']; + + for (const status of defaultStatuses) { + if (this.settings.customStatuses.some(s => s.name === status)) { + this.settings.statusColors[status] = DEFAULT_COLORS[status]; + } + } + + await this.saveSettings(); + } } +/** + * Settings tab for the Note Status plugin + */ class NoteStatusSettingTab extends PluginSettingTab { - plugin: NoteStatus; + plugin: NoteStatus; - constructor(app: App, plugin: NoteStatus) { - super(app, plugin); - this.plugin = plugin; - } + constructor(app: App, plugin: NoteStatus) { + super(app, plugin); + this.plugin = plugin; + } - display(): void { - const { containerEl } = this; - containerEl.empty(); - containerEl.createEl('h2', { text: 'Note Status Settings' }); + display(): void { + const { containerEl } = this; + containerEl.empty(); + + // Header + containerEl.createEl('h2', { text: 'Note Status Settings' }); - new Setting(containerEl) - .setName('Show status dropdown') - .setDesc('Display status dropdown in notes') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusDropdown) - .onChange(async (value) => { - this.plugin.settings.showStatusDropdown = value; - await this.plugin.saveSettings(); - this.plugin.updateStatusDropdown(); - })); + // UI section + containerEl.createEl('h3', { text: 'UI Settings' }); + + // Status dropdown settings + new Setting(containerEl) + .setName('Show status dropdown') + .setDesc('Display status dropdown in notes') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showStatusDropdown) + .onChange(async (value) => { + this.plugin.settings.showStatusDropdown = value; + await this.plugin.saveSettings(); + })); - new Setting(containerEl) - .setName('Dropdown position') - .setDesc('Where to place the status dropdown') - .addDropdown(dropdown => dropdown - .addOption('top', 'Top') - .addOption('bottom', 'Bottom') - .setValue(this.plugin.settings.dropdownPosition) - .onChange(async (value: 'top' | 'bottom') => { - this.plugin.settings.dropdownPosition = value; - await this.plugin.saveSettings(); - this.plugin.updateStatusDropdown(); - })); + new Setting(containerEl) + .setName('Dropdown position') + .setDesc('Where to place the status dropdown') + .addDropdown(dropdown => dropdown + .addOption('top', 'Top') + .addOption('bottom', 'Bottom') + .setValue(this.plugin.settings.dropdownPosition) + .onChange(async (value: 'top' | 'bottom') => { + this.plugin.settings.dropdownPosition = value; + await this.plugin.saveSettings(); + })); - new Setting(containerEl) - .setName('Show status bar') - .setDesc('Display the status bar') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusBar) - .onChange(async (value) => { - this.plugin.settings.showStatusBar = value; - await this.plugin.saveSettings(); - this.plugin.updateStatusBar(); - })); + // Status bar settings + new Setting(containerEl) + .setName('Show status bar') + .setDesc('Display the status bar') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showStatusBar) + .onChange(async (value) => { + this.plugin.settings.showStatusBar = value; + await this.plugin.saveSettings(); + })); - new Setting(containerEl) - .setName('Status bar position') - .setDesc('Align the status bar text') - .addDropdown(dropdown => dropdown - .addOption('left', 'Left') - .addOption('right', 'Right') - .setValue(this.plugin.settings.statusBarPosition) - .onChange(async (value: 'left' | 'right') => { - this.plugin.settings.statusBarPosition = value; - await this.plugin.saveSettings(); - this.plugin.updateStatusBar(); - })); + new Setting(containerEl) + .setName('Status bar position') + .setDesc('Align the status bar text') + .addDropdown(dropdown => dropdown + .addOption('left', 'Left') + .addOption('right', 'Right') + .setValue(this.plugin.settings.statusBarPosition) + .onChange(async (value: 'left' | 'right') => { + this.plugin.settings.statusBarPosition = value; + await this.plugin.saveSettings(); + })); - new Setting(containerEl) - .setName('Auto-hide status bar') - .setDesc('Hide the status bar when status is unknown') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.autoHideStatusBar) - .onChange(async (value) => { - this.plugin.settings.autoHideStatusBar = value; - await this.plugin.saveSettings(); - this.plugin.updateStatusBar(); - })); + new Setting(containerEl) + .setName('Auto-hide status bar') + .setDesc('Hide the status bar when status is unknown') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.autoHideStatusBar) + .onChange(async (value) => { + this.plugin.settings.autoHideStatusBar = value; + await this.plugin.saveSettings(); + })); - new Setting(containerEl) - .setName('Show status icons in file explorer') - .setDesc('Display status icons in the file explorer') - .addToggle(toggle => toggle - .setValue(this.plugin.settings.showStatusIconsInExplorer) - .onChange(async (value) => { - this.plugin.settings.showStatusIconsInExplorer = value; - await this.plugin.saveSettings(); - this.plugin.updateAllFileExplorerIcons(); - })); + // File explorer settings + new Setting(containerEl) + .setName('Show status icons in file explorer') + .setDesc('Display status icons in the file explorer') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.showStatusIconsInExplorer) + .onChange(async (value) => { + this.plugin.settings.showStatusIconsInExplorer = value; + await this.plugin.saveSettings(); + })); - containerEl.createEl('h3', { text: 'Custom Statuses' }); - const statusList = containerEl.createDiv({ cls: 'custom-status-list' }); - const renderStatuses = () => { - statusList.empty(); - this.plugin.settings.customStatuses.forEach((status, index) => { - const setting = new Setting(statusList).setName(status.name); - setting - .addText(text => text - .setPlaceholder('Status Name') - .setValue(status.name) - .onChange(async (value) => { - if (value && !this.plugin.settings.customStatuses.some(s => s.name === value && s !== status)) { - const oldName = status.name; - status.name = value; - if (this.plugin.settings.statusColors[oldName]) { - this.plugin.settings.statusColors[value] = this.plugin.settings.statusColors[oldName]; - delete this.plugin.settings.statusColors[oldName]; - } - await this.plugin.saveSettings(); - renderStatuses(); // Refrescar la lista inmediatamente - } - })) - .addText(text => text - .setPlaceholder('Icon') - .setValue(status.icon) - .onChange(async (value) => { - status.icon = value || '❓'; - await this.plugin.saveSettings(); - })) - .addColorPicker(colorPicker => colorPicker - .setValue(this.plugin.settings.statusColors[status.name] || '#ffffff') - .onChange(async (value) => { - this.plugin.settings.statusColors[status.name] = value; - await this.plugin.saveSettings(); - })) - .addButton(button => button - .setButtonText('Remove') - .setWarning() - .onClick(async () => { - this.plugin.settings.customStatuses.splice(index, 1); - delete this.plugin.settings.statusColors[status.name]; - await this.plugin.saveSettings(); - renderStatuses(); - })); - }); - }; - renderStatuses(); + // Compact view setting + new Setting(containerEl) + .setName('Default to compact view') + .setDesc('Start the status pane in compact view by default') + .addToggle(toggle => toggle + .setValue(this.plugin.settings.compactView || false) + .onChange(async (value) => { + this.plugin.settings.compactView = value; + await this.plugin.saveSettings(); + })); - new Setting(containerEl) - .setName('Add new status') - .setDesc('Add a custom status with a name, icon, and color') - .addButton(button => button - .setButtonText('Add Status') - .setCta() - .onClick(async () => { - const newStatus = { name: `status${this.plugin.settings.customStatuses.length + 1}`, icon: '⭐' }; - this.plugin.settings.customStatuses.push(newStatus); - this.plugin.settings.statusColors[newStatus.name] = '#ffffff'; // Valor inicial blanco - await this.plugin.saveSettings(); - renderStatuses(); - })); - new Setting(containerEl) - .setName('Reset default status colors') - .setDesc('Restore the default colors for predefined statuses (active, onHold, completed, dropped, unknown)') - .addButton(button => button - .setButtonText('Reset Colors') - .setWarning() - .onClick(async () => { - await this.plugin.resetDefaultColors(); - renderStatuses(); - new Notice('Default status colors restored'); - })); - } + // Status management section + containerEl.createEl('h3', { text: 'Custom Statuses' }); + const statusList = containerEl.createDiv({ cls: 'custom-status-list' }); + + const renderStatuses = () => { + statusList.empty(); + + this.plugin.settings.customStatuses.forEach((status, index) => { + const setting = new Setting(statusList) + .setName(status.name) + .setClass('status-item'); + + // Name field + setting.addText(text => text + .setPlaceholder('Status Name') + .setValue(status.name) + .onChange(async (value) => { + if (value && !this.plugin.settings.customStatuses.some(s => s.name === value && s !== status)) { + const oldName = status.name; + status.name = value; + + // Update color mapping + if (this.plugin.settings.statusColors[oldName]) { + this.plugin.settings.statusColors[value] = this.plugin.settings.statusColors[oldName]; + delete this.plugin.settings.statusColors[oldName]; + } + + await this.plugin.saveSettings(); + renderStatuses(); // Refresh the list + } + })); + + // Icon field + setting.addText(text => text + .setPlaceholder('Icon') + .setValue(status.icon) + .onChange(async (value) => { + status.icon = value || '❓'; + await this.plugin.saveSettings(); + })); + + // Color picker + setting.addColorPicker(colorPicker => colorPicker + .setValue(this.plugin.settings.statusColors[status.name] || '#ffffff') + .onChange(async (value) => { + this.plugin.settings.statusColors[status.name] = value; + await this.plugin.saveSettings(); + })); + + // Remove button + setting.addButton(button => button + .setButtonText('Remove') + .setClass('status-remove-button') + .setWarning() + .onClick(async () => { + this.plugin.settings.customStatuses.splice(index, 1); + delete this.plugin.settings.statusColors[status.name]; + await this.plugin.saveSettings(); + renderStatuses(); + })); + }); + }; + + renderStatuses(); + + // Add new status + new Setting(containerEl) + .setName('Add new status') + .setDesc('Add a custom status with a name, icon, and color') + .addButton(button => button + .setButtonText('Add Status') + .setCta() + .onClick(async () => { + const newStatus = { + name: `status${this.plugin.settings.customStatuses.length + 1}`, + icon: '⭐' + }; + + this.plugin.settings.customStatuses.push(newStatus); + this.plugin.settings.statusColors[newStatus.name] = '#ffffff'; // Initial white color + + await this.plugin.saveSettings(); + renderStatuses(); + })); + + // Reset colors + new Setting(containerEl) + .setName('Reset default status colors') + .setDesc('Restore the default colors for predefined statuses') + .addButton(button => button + .setButtonText('Reset Colors') + .setWarning() + .onClick(async () => { + await this.plugin.resetDefaultColors(); + renderStatuses(); + new Notice('Default status colors restored'); + })); + } } diff --git a/styles.css b/styles.css index 985ae2d..1dbce05 100644 --- a/styles.css +++ b/styles.css @@ -1,135 +1,315 @@ +/* Base styles and variables */ +:root { + --status-transition-time: 0.2s; + --status-border-radius: 4px; +} + /* Status Pane */ .note-status-pane { - padding: var(--size-4-2); - background: var(--background-secondary); - color: var(--text-normal); - font-family: var(--font-interface); - overflow-y: auto; - height: 100%; - } - - .note-status-search { - margin-bottom: var(--size-4-2); - } - - .note-status-search-input { - width: 100%; - padding: var(--input-padding); - border: var(--input-border-width) solid var(--background-modifier-border); - border-radius: var(--input-radius); - background: var(--background-primary); - color: var(--text-normal); - outline: none; - transition: border-color 0.2s ease; - } - - .note-status-search-input:focus { - border-color: var(--interactive-accent); - } - - .note-status-actions-refresh { - padding: var(--size-4-1) var(--size-4-3); - border: var(--input-border-width) solid var(--background-modifier-border); - border-radius: var(--button-radius); - background: var(--background-primary); - color: var(--text-normal); - cursor: pointer; - } - - .note-status-actions-refresh:hover { - background: var(--background-modifier-hover); - } - - /* Status Group Titles */ - .status-group .nav-folder-title .status-active, - .status-group .nav-folder-title .status-onHold, - .status-group .nav-folder-title .status-completed, - .status-group .nav-folder-title .status-dropped, - .status-group .nav-folder-title .status-unknown { - font-weight: 600; - padding: var(--size-4-2) var(--size-4-3); - display: flex; - align-items: center; - gap: var(--size-4-1); - transition: background 0.2s ease; - } - - .status-group .nav-folder-title:hover span { - background: var(--background-modifier-hover); - } - - /* Status Dropdown */ + padding: var(--size-4-2); + background: var(--background-secondary); + color: var(--text-normal); + font-family: var(--font-interface); + overflow-y: auto; + height: 100%; + display: flex; + flex-direction: column; +} + +/* Header elements */ +.note-status-header { + position: sticky; + top: 0; + z-index: 10; + background: var(--background-secondary); + padding-bottom: var(--size-4-2); + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: var(--size-4-2); + display: flex; + flex-direction: column; + gap: var(--size-4-2); +} + +/* Search input improvements */ +.note-status-search { + width: 100%; + position: relative; +} + +.search-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.search-input-icon { + position: absolute; + left: var(--size-4-1); + color: var(--text-muted); + display: flex; + align-items: center; + pointer-events: none; +} + +.note-status-search-input { + width: 100%; + padding: var(--input-padding); + padding-left: calc(var(--size-4-1) * 3 + 18px); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--input-radius); + background: var(--background-primary); + color: var(--text-normal); + outline: none; + transition: all var(--status-transition-time) ease; +} + +.note-status-search-input:focus { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); +} + +.search-input-clear-button { + opacity: 0; + position: absolute; + right: var(--size-4-1); + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + transition: opacity var(--status-transition-time) ease; +} + +.search-input-clear-button.is-visible { + opacity: 1; +} + +.search-input-clear-button:hover { + color: var(--text-normal); +} + +/* Actions toolbar */ +.status-pane-actions-container { + display: flex; + gap: var(--size-4-2); + justify-content: flex-end; + margin-top: var(--size-4-1); +} + +.note-status-view-toggle, +.note-status-actions-refresh { + padding: var(--size-4-1) var(--size-4-2); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--button-radius); + background: var(--background-primary); + color: var(--text-normal); + cursor: pointer; + transition: all var(--status-transition-time) ease; + display: flex; + align-items: center; + justify-content: center; +} + +.note-status-view-toggle:hover, +.note-status-actions-refresh:hover { + background: var(--background-modifier-hover); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Status Groups Container */ +.note-status-groups-container { + flex: 1; + overflow-y: auto; +} + +/* Status Group Styling */ +.status-group { + margin-bottom: var(--size-4-3); + border-radius: var(--status-border-radius); + background: var(--background-primary-alt); + overflow: hidden; +} + +.status-group .nav-folder-title { + padding: var(--size-4-1); + background: var(--background-secondary-alt); + transition: background var(--status-transition-time) ease; +} + +.status-group .nav-folder-title:hover { + background: var(--background-modifier-hover); +} + +.status-group .nav-folder-title-content { + font-weight: 600; + display: flex; + align-items: center; + gap: var(--size-4-1); +} + +/* Collapse indicators */ +.collapse-indicator { + margin-right: var(--size-4-2); + display: flex; + align-items: center; + color: var(--text-muted); + transition: transform var(--status-transition-time) ease; +} + +.is-collapsed .collapse-indicator { + transform: rotate(-90deg); +} + +/* File items */ +.status-group .nav-folder-children { + padding: var(--size-4-1); + background: var(--background-primary); +} + +.nav-file { + border-radius: var(--status-border-radius); + transition: background var(--status-transition-time) ease; + margin-bottom: 2px; + position: relative; +} + +.nav-file:hover { + background: var(--background-modifier-hover); +} + +.nav-file-title { + display: flex; + align-items: center; + padding: var(--size-4-1); +} + +.nav-file-icon { + color: var(--text-muted); + margin-right: var(--size-4-2); + display: flex; + align-items: center; +} + +.nav-file-title-content { + flex: 1; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* Compact view */ +.compact-view .nav-file-title { + padding: var(--size-4-1) var(--size-4-2); + font-size: 0.9em; +} + +.compact-view .nav-folder-children { + padding: 0; +} + +.compact-view .nav-file { + margin-bottom: 0; + border-radius: 0; + border-bottom: 1px solid var(--background-modifier-border); +} + +.compact-view .nav-file:last-child { + border-bottom: none; +} + +/* Status Dropdown */ .note-status-dropdown { - padding: var(--size-4-1); - background: var(--background-secondary); - position: sticky; - z-index: var(--layer-popover); - display: flex; - align-items: center; - gap: var(--size-4-2); - } - - .note-status-dropdown.top { - border-bottom: var(--border-width) solid var(--background-modifier-border); - top: 0; - } - - .note-status-dropdown.bottom { - border-top: var(--border-width) solid var(--background-modifier-border); - bottom: 0; - } - - .note-status-label { - font-weight: bold; - } - - .note-status-select { - padding: var(--input-padding); - border: var(--input-border-width) solid var(--background-modifier-border); - border-radius: var(--input-radius); - background: var(--background-primary); /* Base background */ - background-image: none !important; /* Override theme's SVG */ - color: var(--text-normal); - cursor: pointer; - outline: none; - transition: all 0.2s ease; - appearance: auto; - min-width: 120px; - } - - .note-status-select:hover { - box-shadow: 0 2px 4px var(--background-modifier-box-shadow); - } - - .note-status-select option { - background: var(--background-primary); /* Clean option background */ - color: var(--text-normal); - } - - .note-status-hide-button { - padding: var(--size-4-1) var(--size-4-2); - border: var(--input-border-width) solid var(--background-modifier-border); - border-radius: var(--button-radius); - background: var(--background-primary); - color: var(--text-normal); - cursor: pointer; - transition: all 0.2s ease; - } - - .note-status-hide-button:hover { - background: var(--background-modifier-hover); - } - - /* File Explorer Icons */ - .note-status-icon { - margin-left: var(--size-4-1); - font-size: var(--font-ui-small); - } - + padding: var(--size-4-2); + background: var(--background-secondary); + position: sticky; + z-index: var(--layer-popover); + display: flex; + align-items: center; + gap: var(--size-4-2); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.note-status-dropdown.top { + border-bottom: var(--border-width) solid var(--background-modifier-border); + top: 0; +} + +.note-status-dropdown.bottom { + border-top: var(--border-width) solid var(--background-modifier-border); + bottom: 0; +} + +.note-status-label { + font-weight: bold; + white-space: nowrap; +} + +.note-status-select { + padding: var(--input-padding); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--input-radius); + background: var(--background-primary); + background-image: none !important; /* Override theme's SVG */ + color: var(--text-normal); + cursor: pointer; + outline: none; + transition: all var(--status-transition-time) ease; + appearance: auto; + min-width: 150px; + flex: 1; +} + +.note-status-select:hover, +.note-status-select:focus { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); +} + +.note-status-select option { + background: var(--background-primary); + color: var(--text-normal); + padding: var(--size-4-1) var(--size-4-2); +} + +.note-status-hide-button { + padding: var(--size-4-1) var(--size-4-2); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--button-radius); + background: var(--background-primary); + color: var(--text-normal); + cursor: pointer; + transition: all var(--status-transition-time) ease; + white-space: nowrap; +} + +.note-status-hide-button:hover { + background: var(--background-modifier-hover); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* File Explorer Icons */ +.note-status-icon { + margin-left: var(--size-4-1); + font-size: var(--font-ui-small); +} + /* Status Bar */ .note-status-bar { display: flex; - justify-content: flex-end; + align-items: center; + padding: 0 var(--size-4-2); + height: 22px; + gap: var(--size-4-1); + background: var(--background-secondary); + border-radius: var(--status-border-radius); + margin: 0 var(--size-4-1); + transition: opacity var(--status-transition-time) ease; + cursor: pointer; + border: 1px solid var(--background-modifier-border); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.note-status-bar:hover { + background: var(--background-modifier-hover); } .note-status-bar.left { @@ -149,45 +329,129 @@ opacity: 1; } -/* Los colores de estado se manejan dinámicamente en JS, no aquí */ +/* Batch Modal Styling */ +.note-status-batch-modal { + max-width: 500px; +} +.note-status-batch-modal h2 { + margin-bottom: var(--size-4-3); + text-align: center; + padding-bottom: var(--size-4-2); + border-bottom: 1px solid var(--background-modifier-border); +} -/* Colores de los estados -.status-active { - color: var(--text-success); +.note-status-modal-search { + margin-bottom: var(--size-4-2); + position: relative; } -.status-onHold { - color: var(--text-warning); -} -.status-completed { - color: var(--text-accent); -} -.status-dropped { - color: var(--text-error); -} -.status-unknown { - color: var(--text-muted); -} -*/ -/* Aplicar colores a los íconos y textos en el status bar y explorador */ -.note-status-bar .note-status-active, -.nav-file-title .note-status-active { - color: var(--text-success); +.note-status-modal-search-input { + width: 100%; + padding: var(--input-padding); + padding-left: calc(var(--size-4-1) * 3); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--input-radius); + background: var(--background-primary); + color: var(--text-normal); + outline: none; + transition: all var(--status-transition-time) ease; } -.note-status-bar .note-status-onHold, -.nav-file-title .note-status-onHold { - color: var(--text-warning); + +.note-status-modal-search-input:focus { + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px rgba(var(--interactive-accent-rgb), 0.2); } -.note-status-bar .note-status-completed, -.nav-file-title .note-status-completed { - color: var(--text-accent); + +.note-status-file-select-container, +.note-status-status-select-container { + margin-bottom: var(--size-4-2); } -.note-status-bar .note-status-dropped, -.nav-file-title .note-status-dropped { - color: var(--text-error); + +.note-status-file-select, +.note-status-status-select { + width: 100%; + padding: var(--input-padding); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--input-radius); + background: var(--background-primary); + color: var(--text-normal); + outline: none; + transition: all var(--status-transition-time) ease; } -.note-status-bar .note-status-unknown, -.nav-file-title .note-status-unknown { - color: var(--text-muted); + +.note-status-file-select { + min-height: 200px; } + +.note-status-file-select option, +.note-status-status-select option { + padding: var(--size-4-1); +} + +.note-status-modal-buttons { + display: flex; + justify-content: space-between; + margin-top: var(--size-4-3); + gap: var(--size-4-2); +} + +.note-status-modal-buttons button { + flex: 1; + padding: var(--size-4-2); + border: var(--input-border-width) solid var(--background-modifier-border); + border-radius: var(--button-radius); + background: var(--background-primary); + color: var(--text-normal); + cursor: pointer; + transition: all var(--status-transition-time) ease; +} + +.note-status-modal-buttons button:hover { + background: var(--background-modifier-hover); +} + +.note-status-modal-buttons button.mod-cta { + background: var(--interactive-accent); + color: var(--text-on-accent); + border-color: var(--interactive-accent); +} + +.note-status-modal-buttons button.mod-cta:hover { + background: var(--interactive-accent-hover); +} + +/* Settings Tab Styling */ +.custom-status-list { + margin-bottom: var(--size-4-3); + border: 1px solid var(--background-modifier-border); + border-radius: var(--status-border-radius); + background: var(--background-primary-alt); + padding: var(--size-4-2); +} + +.status-item { + display: flex; + align-items: center; + padding: var(--size-4-1) var(--size-4-2); + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: var(--size-4-1); +} + +.status-item:last-child { + border-bottom: none; + margin-bottom: 0; +} + +.status-remove-button { + margin-left: auto; +} + +/* Animations */ +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.status-group { + animation: fadeIn 0.3s ease;