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', `
- `,
+ 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;