diff --git a/README.md b/README.md index 8afa876..7f6ea8e 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Enhance your Obsidian workflow with a powerful status tracking system for your n ![Hello World Screenshot](images/hello-world.png) ## Table of Contents + - [Features](#features) - [Installation](#installation) - [User Guide](#user-guide) @@ -17,6 +18,7 @@ Enhance your Obsidian workflow with a powerful status tracking system for your n - [Support](#support-the-development) ## Features + - **Status Assignment**: Mark notes with statuses (active, on hold, completed, dropped) - **Multiple Statuses**: Apply more than one status to a single note - **File Explorer Integration**: View status icons directly in your file explorer @@ -32,12 +34,14 @@ Enhance your Obsidian workflow with a powerful status tracking system for your n ## Installation ### Marketplace Installation (Recommended) + 1. Open Obsidian → Settings → Community plugins 2. Disable Safe mode 3. Click "Browse" and search for "Note Status" 4. Click Install and Enable ### Manual Installation + 1. **Download the Plugin**: - Grab the latest release from the [GitHub Releases page](https://github.com/devonthesofa/obsidian-note-status/releases). - Download the files `main.js`, `styles.css` and `manifest.json` @@ -51,8 +55,11 @@ Enhance your Obsidian workflow with a powerful status tracking system for your n - Find "Note Status" in the list and toggle it on. ## User Guide + ### Basic Usage + #### 1. Assign Status from the Toolbar + The simplest way to set a note's status is using the toolbar button: ![Status From Toolbar](images/status-from-toolbar.png) @@ -63,6 +70,7 @@ The simplest way to set a note's status is using the toolbar button: 4. The status will be applied and visible in both toolbar and status bar #### 2. Status Bar + The status bar at the bottom of your editor shows the current status of your note: ![Status Bar](images/status-bar.png) @@ -70,14 +78,16 @@ The status bar at the bottom of your editor shows the current status of your not - When multiple statuses are enabled, all applied statuses will be displayed here #### 3. Status Pane + The Status Pane provides an overview of all your notes grouped by status: ![Status Pane](images/status-pane.png) To open the Status Pane: + - Click the status pane icon in the left sidebar - Use the command palette: "Open status pane" -In the Status Pane you can: + In the Status Pane you can: - View all notes grouped by status - Click on any note to open it - Search for specific notes @@ -85,6 +95,7 @@ In the Status Pane you can: - Right-click on notes for more options #### 4. File Explorer Integration + Status icons appear directly in your file explorer: ![File Explorer](images/file-explorer.png) @@ -93,7 +104,9 @@ Status icons appear directly in your file explorer: - Select multiple files to batch update their statuses ### Advanced Usage + #### Multiple Statuses + When enabled in settings, you can assign multiple statuses to a single note: ![Multiple Statuses Selector](images/multiple-statuses-selector.png) @@ -103,6 +116,7 @@ When enabled in settings, you can assign multiple statuses to a single note: ![Multiple Statuses Status Bar](images/multiple-statuses-status-bar.png) To add additional statuses: + 1. Open the status dropdown 2. Click on another status to add it 3. Click on an active status to remove it @@ -110,6 +124,7 @@ To add additional statuses: #### Batch Updates To update multiple files at once: + 1. Select multiple files in the file explorer (using Ctrl/Cmd or Shift) 2. Right-click and choose "Change status" 3. Select whether to replace or add the status @@ -118,6 +133,7 @@ To update multiple files at once: ![Batch Updates](images/batch-updates.png) #### Large Vault Performance + If you have a large vault with thousands of notes, use these features for better performance: 1. Enable "Exclude unassigned notes from status pane" in settings @@ -125,7 +141,9 @@ If you have a large vault with thousands of notes, use these features for better 3. Use pagination controls to navigate through large status groups ![Pagination](images/pagination.png) ## Configuration + ### Status Management + Access plugin settings via Settings → Note Status #### Status Templates @@ -150,7 +168,9 @@ Create your own statuses by: ![Custom Statuses](images/custom-statuses.png) ### Display Options + Configure how statuses are displayed: + - Show/hide status bar - Auto-hide status bar when status is unknown - Show/hide status icons in file explorer @@ -161,6 +181,7 @@ Configure how statuses are displayed: - Customize frontmatter tag name ## Performance Recommendations + If you have a large vault (1000+ notes), consider these settings for optimal performance: 1. Enable "Exclude unassigned notes from status pane" @@ -168,9 +189,10 @@ If you have a large vault (1000+ notes), consider these settings for optimal per 3. Use specific searches rather than browsing all notes 4. Consider using "Compact view" in the status pane - ## Commands + The plugin provides several commands accessible via the Command Palette: + - `Open status pane` - Opens the status view - `Refresh status` - Refreshes current note's status - `Add status to current note` - Shows status menu @@ -178,24 +200,33 @@ The plugin provides several commands accessible via the Command Palette: - `Force refresh user interface` - Complete UI refresh ## Technical Details + ### Frontmatter Format + Status information is stored in your note's frontmatter using the following format: + ```yaml --- obsidian-note-status: ["active"] --- ``` + Multiple statuses: + ```yaml --- obsidian-note-status: ["active", "inProgress"] --- ``` + The frontmatter tag name can be customized in settings. ## Development + ### Project Structure + The plugin has been recently restructured with a modern, modular architecture: + ``` note-status/ ├── src/ @@ -237,10 +268,12 @@ note-status/ ``` ### Prerequisites + - Node.js and npm installed. - Obsidian API knowledge (TypeScript-based). ### CSS Modularization + The plugin's CSS has been modularized to improve maintainability: - **Component-based**: Each UI component has its own CSS file @@ -248,44 +281,58 @@ The plugin's CSS has been modularized to improve maintainability: - **Development workflow**: CSS changes are hot-reloaded during development When making CSS changes: + 1. Modify the appropriate file in the `styles/` directory 2. For new components, create a dedicated CSS file and import it in `styles/index.css` 3. The build process will automatically bundle everything into `styles.css` ### Building the Plugin + 1. Clone this repository: + ```bash git clone https://github.com/devonthesofa/obsidian-note-status.git cd obsidian-note-status ``` + 2. Install dependencies: + ```bash npm install ``` + 3. Build the plugin: + ```bash npm run build ``` + 4. For development with auto-rebuilding: + ```bash npm run dev ``` + 5. The compiled plugin will be in the root directory, ready to copy into `.obsidian/plugins/`. ## Contributing + Contributions welcome! Please feel free to submit a Pull Request. + 1. Fork the repository 2. Create your feature branch: `git checkout -b feature/my-new-feature` 3. Commit your changes: `git commit -am 'Add some feature'` 4. Push to the branch: `git push origin feature/my-new-feature` 5. Submit a pull request - + - Report issues or suggest features via the Issues tab. ## Roadmap + The following features and improvements are planned for upcoming releases: ### Short-term + - **Batch Modification Enhancement**: Implement a modal dialog with file preview and status selection for more intuitive batch operations - **Dropdown Refinement**: Restructure status options to be grouped by categories (workflow, templates, custom) with visual separators - **Obsidian API Compliance**: Refactor code to follow Obsidian guidelines for better performance and compatibility: @@ -294,14 +341,17 @@ The following features and improvements are planned for upcoming releases: - Improve plugin teardown process to prevent memory leaks ### Medium-term + - **Export/import configurations**: Share your status setups with others ### Long-term + - **Canvas integration**: Show status on canvas cards - **Graph view integration**: Visualize notes by status in graph view - **Mobile optimization**: Improved experience on mobile devices ## Support the Development + If you find this plugin useful and would like to support its development, you can make a donation through my PayPal account. Any contribution is greatly appreciated and helps me continue improving the plugin! PayPal: https://paypal.me/aleixsoler diff --git a/components/status-bar/index.ts b/components/status-bar/index.ts index 64d38cb..6338305 100644 --- a/components/status-bar/index.ts +++ b/components/status-bar/index.ts @@ -1,4 +1,4 @@ -import { StatusBarController } from './status-bar-controller'; +import { StatusBarController } from "./status-bar-controller"; -export { StatusBarController as StatusBar}; -export default StatusBarController; \ No newline at end of file +export { StatusBarController as StatusBar }; +export default StatusBarController; diff --git a/components/status-bar/status-bar-controller.ts b/components/status-bar/status-bar-controller.ts index c6838f2..8c526ec 100644 --- a/components/status-bar/status-bar-controller.ts +++ b/components/status-bar/status-bar-controller.ts @@ -1,94 +1,103 @@ -import { NoteStatusSettings } from '../../models/types'; -import { StatusService } from '../../services/status-service'; -import { StatusBarView } from './status-bar-view'; +import { NoteStatusSettings } from "../../models/types"; +import { StatusService } from "../../services/status-service"; +import { StatusBarView } from "./status-bar-view"; /** * Controller for the status bar */ export class StatusBarController { - private view: StatusBarView; - private settings: NoteStatusSettings; - private statusService: StatusService; - private currentStatuses: string[] = ['unknown']; + private view: StatusBarView; + private settings: NoteStatusSettings; + private statusService: StatusService; + private currentStatuses: string[] = ["unknown"]; - constructor(statusBarContainer: HTMLElement, settings: NoteStatusSettings, statusService: StatusService) { - this.view = new StatusBarView(statusBarContainer); - this.settings = settings; - this.statusService = statusService; - - this.update(['unknown']); - } + constructor( + statusBarContainer: HTMLElement, + settings: NoteStatusSettings, + statusService: StatusService, + ) { + this.view = new StatusBarView(statusBarContainer); + this.settings = settings; + this.statusService = statusService; - /** - * Update the status bar with new statuses - */ - public update(statuses: string[]): void { - this.currentStatuses = statuses; - this.render(); - } + this.update(["unknown"]); + } - /** - * Render the status bar - */ - private render(): void { - this.view.reset(); - - if (!this.settings.showStatusBar) { - this.view.hide(); - return; - } - - if (!this.settings.useMultipleStatuses) { - this.renderStatuses([this.currentStatuses[0]]); - } else { - this.renderStatuses(this.currentStatuses); - } - - this.handleAutoHide(); - } - - /** - * Render statuses - handles both single and multiple status cases - */ - private renderStatuses(statuses: string[]): void { - const statusDetails = statuses.map(status => { - const statusObj = this.statusService.getAllStatuses().find(s => s.name === status); - return { - name: status, - icon: this.statusService.getStatusIcon(status), - tooltipText: statusObj?.description ? `${status} - ${statusObj.description}` : status - }; - }); + /** + * Update the status bar with new statuses + */ + public update(statuses: string[]): void { + this.currentStatuses = statuses; + this.render(); + } - this.view.renderStatuses(statusDetails); - } - - /** - * Handle auto-hide behavior - */ - private handleAutoHide(): void { - const onlyUnknown = this.currentStatuses.length === 1 && - this.currentStatuses[0] === 'unknown'; - - if (this.settings.autoHideStatusBar && onlyUnknown) { - this.view.hide(); - } else { - this.view.show(); - } - } + /** + * Render the status bar + */ + private render(): void { + this.view.reset(); - /** - * Update settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.render(); - } + if (!this.settings.showStatusBar) { + this.view.hide(); + return; + } - /** - * Clean up when plugin is unloaded - */ - public unload(): void { - this.view.destroy(); - } -} \ No newline at end of file + if (!this.settings.useMultipleStatuses) { + this.renderStatuses([this.currentStatuses[0]]); + } else { + this.renderStatuses(this.currentStatuses); + } + + this.handleAutoHide(); + } + + /** + * Render statuses - handles both single and multiple status cases + */ + private renderStatuses(statuses: string[]): void { + const statusDetails = statuses.map((status) => { + const statusObj = this.statusService + .getAllStatuses() + .find((s) => s.name === status); + return { + name: status, + icon: this.statusService.getStatusIcon(status), + tooltipText: statusObj?.description + ? `${status} - ${statusObj.description}` + : status, + }; + }); + + this.view.renderStatuses(statusDetails); + } + + /** + * Handle auto-hide behavior + */ + private handleAutoHide(): void { + const onlyUnknown = + this.currentStatuses.length === 1 && + this.currentStatuses[0] === "unknown"; + + if (this.settings.autoHideStatusBar && onlyUnknown) { + this.view.hide(); + } else { + this.view.show(); + } + } + + /** + * Update settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.render(); + } + + /** + * Clean up when plugin is unloaded + */ + public unload(): void { + this.view.destroy(); + } +} diff --git a/components/status-bar/status-bar-view.ts b/components/status-bar/status-bar-view.ts index 8dc46f3..f3f745d 100644 --- a/components/status-bar/status-bar-view.ts +++ b/components/status-bar/status-bar-view.ts @@ -1,107 +1,123 @@ -import { setTooltip } from 'obsidian'; +import { setTooltip } from "obsidian"; /** * Renders the status bar UI */ export class StatusBarView { - private element: HTMLElement; + private element: HTMLElement; - constructor(element: HTMLElement) { - this.element = element; - this.element.addClass('note-status-bar'); - } + constructor(element: HTMLElement) { + this.element = element; + this.element.addClass("note-status-bar"); + } - /** - * Clears the element and resets CSS classes - */ - reset(): void { - this.element.empty(); - this.element.removeClass('left', 'hidden', 'auto-hide', 'visible'); - this.element.addClass('note-status-bar'); - } + /** + * Clears the element and resets CSS classes + */ + reset(): void { + this.element.empty(); + this.element.removeClass("left", "hidden", "auto-hide", "visible"); + this.element.addClass("note-status-bar"); + } - /** - * Hide the status bar - */ - hide(): void { - this.element.addClass('hidden'); - this.element.removeClass('visible'); - } + /** + * Hide the status bar + */ + hide(): void { + this.element.addClass("hidden"); + this.element.removeClass("visible"); + } - /** - * Show the status bar - */ - show(): void { - this.element.removeClass('hidden'); - this.element.addClass('visible'); - } + /** + * Show the status bar + */ + show(): void { + this.element.removeClass("hidden"); + this.element.addClass("visible"); + } + renderStatuses( + statuses: Array<{ name: string; icon: string; tooltipText: string }>, + ): void { + if (statuses.length === 1) { + this.renderSingleStatus( + statuses[0].name, + statuses[0].icon, + statuses[0].tooltipText, + ); + } else { + this.renderMultipleStatuses(statuses); + } + } - renderStatuses(statuses: Array<{name: string, icon: string, tooltipText: string}>): void { - if (statuses.length === 1) { - this.renderSingleStatus(statuses[0].name, statuses[0].icon, statuses[0].tooltipText); - } else { - this.renderMultipleStatuses(statuses); - } - } + /** + * Render a single status + */ + private renderSingleStatus( + status: string, + icon: string, + tooltipText: string, + ): void { + const statusText = this.element.createEl("span", { + text: `Status: ${status}`, + cls: `note-status-${status}`, + }); + setTooltip(statusText, tooltipText); - /** - * Render a single status - */ - private renderSingleStatus(status: string, icon: string, tooltipText: string): void { - const statusText = this.element.createEl('span', { - text: `Status: ${status}`, - cls: `note-status-${status}` - }); - setTooltip(statusText, tooltipText); - - const statusIcon = this.element.createEl('span', { - text: icon, - cls: `note-status-icon status-${status}` - }); - setTooltip(statusIcon, tooltipText); - } + const statusIcon = this.element.createEl("span", { + text: icon, + cls: `note-status-icon status-${status}`, + }); + setTooltip(statusIcon, tooltipText); + } - /** - * Render multiple statuses - */ - private renderMultipleStatuses(statuses: Array<{name: string, icon: string, tooltipText: string}>): void { - this.element.createEl('span', { - text: 'Statuses: ', - cls: 'note-status-label' - }); - - const badgesContainer = this.element.createEl('span', { - cls: 'note-status-badges' - }); - - statuses.forEach(status => this.createStatusBadge(badgesContainer, status)); - } + /** + * Render multiple statuses + */ + private renderMultipleStatuses( + statuses: Array<{ name: string; icon: string; tooltipText: string }>, + ): void { + this.element.createEl("span", { + text: "Statuses: ", + cls: "note-status-label", + }); - /** - * Create a status badge for multiple status display - */ - private createStatusBadge(container: HTMLElement, status: {name: string, icon: string, tooltipText: string}): void { - const badge = container.createEl('span', { - cls: `note-status-badge status-${status.name}` - }); - setTooltip(badge, status.tooltipText); - - badge.createEl('span', { - text: status.icon, - cls: 'note-status-badge-icon' - }); - - badge.createEl('span', { - text: status.name, - cls: 'note-status-badge-text' - }); - } + const badgesContainer = this.element.createEl("span", { + cls: "note-status-badges", + }); - /** - * Clean up the element - */ - destroy(): void { - this.element.empty(); - } -} \ No newline at end of file + statuses.forEach((status) => + this.createStatusBadge(badgesContainer, status), + ); + } + + /** + * Create a status badge for multiple status display + */ + private createStatusBadge( + container: HTMLElement, + status: { name: string; icon: string; tooltipText: string }, + ): void { + const badge = container.createEl("span", { + cls: `note-status-badge status-${status.name}`, + }); + setTooltip(badge, status.tooltipText); + + badge.createEl("span", { + text: status.icon, + cls: "note-status-badge-icon", + }); + + badge.createEl("span", { + text: status.name, + cls: "note-status-badge-text", + }); + } + + /** + * Clean up the element + */ + destroy(): void { + this.element.empty(); + } +} diff --git a/components/status-dropdown/dropdown-events.ts b/components/status-dropdown/dropdown-events.ts index d0468f4..55942df 100644 --- a/components/status-dropdown/dropdown-events.ts +++ b/components/status-dropdown/dropdown-events.ts @@ -6,24 +6,24 @@ * Setup event handlers for the dropdown */ export function setupDropdownEvents(options: { - clickOutsideHandler: (e: MouseEvent) => void, - escapeKeyHandler: (e: KeyboardEvent) => void + clickOutsideHandler: (e: MouseEvent) => void; + escapeKeyHandler: (e: KeyboardEvent) => void; }): void { - const { clickOutsideHandler, escapeKeyHandler } = options; - - document.addEventListener('click', clickOutsideHandler); - document.addEventListener('keydown', escapeKeyHandler); + const { clickOutsideHandler, escapeKeyHandler } = options; + + document.addEventListener("click", clickOutsideHandler); + document.addEventListener("keydown", escapeKeyHandler); } /** * Remove dropdown event handlers */ export function removeDropdownEvents(options: { - clickOutsideHandler: (e: MouseEvent) => void, - escapeKeyHandler: (e: KeyboardEvent) => void + clickOutsideHandler: (e: MouseEvent) => void; + escapeKeyHandler: (e: KeyboardEvent) => void; }): void { - const { clickOutsideHandler, escapeKeyHandler } = options; - - document.removeEventListener('click', clickOutsideHandler); - document.removeEventListener('keydown', escapeKeyHandler); -} \ No newline at end of file + const { clickOutsideHandler, escapeKeyHandler } = options; + + document.removeEventListener("click", clickOutsideHandler); + document.removeEventListener("keydown", escapeKeyHandler); +} diff --git a/components/status-dropdown/dropdown-manager.ts b/components/status-dropdown/dropdown-manager.ts index 49cd5ce..fae9b4d 100644 --- a/components/status-dropdown/dropdown-manager.ts +++ b/components/status-dropdown/dropdown-manager.ts @@ -1,255 +1,273 @@ -import { MarkdownView, Editor, Notice, TFile, App } from 'obsidian'; -import { DropdownUI } from './dropdown-ui'; -import { DropdownOptions, DropdownDependencies } from './types'; -import { createDummyTarget } from './dropdown-position'; -import { StatusService } from 'services/status-service'; -import { NoteStatusSettings } from 'models/types'; +import { MarkdownView, Editor, Notice, TFile, App } from "obsidian"; +import { DropdownUI } from "./dropdown-ui"; +import { DropdownOptions, DropdownDependencies } from "./types"; +import { createDummyTarget } from "./dropdown-position"; +import { StatusService } from "services/status-service"; +import { NoteStatusSettings } from "models/types"; /** * High-level manager for status dropdown interactions */ export class DropdownManager { - private app: App; - private settings: NoteStatusSettings; - private statusService: StatusService; - private currentStatuses: string[] = ['unknown']; - private toolbarButton?: HTMLElement; - private dropdownUI: DropdownUI; - - constructor(app: App, settings: NoteStatusSettings, statusService: StatusService) { - this.app = app; - this.settings = settings; - this.statusService = statusService; - - const deps: DropdownDependencies = { app, settings, statusService }; - this.dropdownUI = new DropdownUI(deps); - this.setupDropdownCallbacks(); - } + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private currentStatuses: string[] = ["unknown"]; + private toolbarButton?: HTMLElement; + private dropdownUI: DropdownUI; - /** - * Set up dropdown callbacks - */ - private setupDropdownCallbacks(): void { + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + ) { + this.app = app; + this.settings = settings; + this.statusService = statusService; - this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { - if (!targetFile) return; - - const isMultiple = Array.isArray(targetFile); - - await this.statusService.handleStatusChange({ - files: targetFile, - statuses: status, - operation: 'remove', - showNotice: isMultiple - }); - }); - - this.dropdownUI.setOnSelectStatusHandler(async (status, targetFile) => { - // Check if handling multiple files - const isMultipleFiles = Array.isArray(targetFile) && targetFile.length > 1; - - if (isMultipleFiles) { - const files = targetFile as TFile[]; - // Count how many files already have this status - const filesWithStatus = files.filter(file => - this.statusService.getFileStatuses(file).includes(status) - ); - - // If ALL have the status, remove it. Otherwise, add it - const operation = filesWithStatus.length === files.length ? 'remove' : (!this.settings.useMultipleStatuses) ? 'set':'add'; - - await this.statusService.handleStatusChange({ - files: targetFile, - statuses: status, - isMultipleSelection: true, - operation: operation - }); - } else { - // For individual files, maintain default behavior - await this.statusService.handleStatusChange({ - files: targetFile, - statuses: status - }); - } - }); - } + const deps: DropdownDependencies = { app, settings, statusService }; + this.dropdownUI = new DropdownUI(deps); + this.setupDropdownCallbacks(); + } - /** - * Updates the dropdown UI based on current statuses - */ - public update(currentStatuses: string[] | string, _file?: TFile): void { - this.currentStatuses = Array.isArray(currentStatuses) ? - [...currentStatuses] : [currentStatuses]; - - this.dropdownUI.updateStatuses(this.currentStatuses); - } + /** + * Set up dropdown callbacks + */ + private setupDropdownCallbacks(): void { + this.dropdownUI.setOnRemoveStatusHandler(async (status, targetFile) => { + if (!targetFile) return; - /** - * Updates settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.dropdownUI.updateSettings(settings); - } + const isMultiple = Array.isArray(targetFile); - /** - * Get position from cursor or fallback - */ - private getCursorPosition(editor: Editor, view: MarkdownView): { x: number, y: number } { - try { - const cursor = editor.getCursor('head'); - const lineElement = view.contentEl.querySelector(`.cm-line:nth-child(${cursor.line + 1})`); + await this.statusService.handleStatusChange({ + files: targetFile, + statuses: status, + operation: "remove", + showNotice: isMultiple, + }); + }); - if (lineElement) { - const rect = lineElement.getBoundingClientRect(); - return { x: rect.left + 20, y: rect.bottom + 5 }; - } + this.dropdownUI.setOnSelectStatusHandler(async (status, targetFile) => { + // Check if handling multiple files + const isMultipleFiles = + Array.isArray(targetFile) && targetFile.length > 1; - const editorEl = view.contentEl.querySelector('.cm-editor'); - if (editorEl) { - const rect = editorEl.getBoundingClientRect(); - return { x: rect.left + 100, y: rect.top + 100 }; - } - } catch (error) { - console.error('Error getting position for dropdown:', error); - } + if (isMultipleFiles) { + const files = targetFile as TFile[]; + // Count how many files already have this status + const filesWithStatus = files.filter((file) => + this.statusService.getFileStatuses(file).includes(status), + ); - return { x: window.innerWidth / 2, y: window.innerHeight / 3 }; - } + // If ALL have the status, remove it. Otherwise, add it + const operation = + filesWithStatus.length === files.length + ? "remove" + : !this.settings.useMultipleStatuses + ? "set" + : "add"; + await this.statusService.handleStatusChange({ + files: targetFile, + statuses: status, + isMultipleSelection: true, + operation: operation, + }); + } else { + // For individual files, maintain default behavior + await this.statusService.handleStatusChange({ + files: targetFile, + statuses: status, + }); + } + }); + } - /** - * Universal function to open the status dropdown - */ - public openStatusDropdown(options: DropdownOptions): void { - const activeFile = this.app.workspace.getActiveFile(); - const files = options.files || (activeFile ? [activeFile] : []); - if (!files.length) { - new Notice('No files selected'); - return; - } - if (!files.length || !files[0]) { - new Notice('No files selected'); - return; - } + /** + * Updates the dropdown UI based on current statuses + */ + public update(currentStatuses: string[] | string, _file?: TFile): void { + this.currentStatuses = Array.isArray(currentStatuses) + ? [...currentStatuses] + : [currentStatuses]; - if (this.dropdownUI.isOpen) { - this.resetDropdown(); - return - } - - const isSingleFile = files.length === 1; - - // Set up target files appropriately - if (isSingleFile) { - const targetFile = files[0]; - this.dropdownUI.setTargetFile(targetFile); - const currentStatuses = this.statusService.getFileStatuses(targetFile); - this.dropdownUI.updateStatuses(currentStatuses); - } else { - // For multiple files, set the whole collection - this.dropdownUI.setTargetFiles(files); - const commonStatuses = this.findCommonStatuses(files); - this.dropdownUI.updateStatuses(commonStatuses); - } - - this.positionAndOpenDropdown(options); - } + this.dropdownUI.updateStatuses(this.currentStatuses); + } - - /** - * Reset dropdown state before opening - */ - public resetDropdown(): void { - this.dropdownUI.close(); - this.dropdownUI.setTargetFile(null); - } + /** + * Updates settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.dropdownUI.updateSettings(settings); + } - /** - * Position and open the dropdown - */ - private positionAndOpenDropdown(options: { - target?: HTMLElement; - position?: { x: number, y: number }; - editor?: Editor; - view?: MarkdownView; - }): void { - if (options.editor && options.view) { - const position = this.getCursorPosition(options.editor, options.view); - this.openWithPosition(position); - return; - } + /** + * Get position from cursor or fallback + */ + private getCursorPosition( + editor: Editor, + view: MarkdownView, + ): { x: number; y: number } { + try { + const cursor = editor.getCursor("head"); + const lineElement = view.contentEl.querySelector( + `.cm-line:nth-child(${cursor.line + 1})`, + ); - if (options.target) { - if (options.position) { - this.dropdownUI.open(options.target, options.position); - } else { - const rect = options.target.getBoundingClientRect(); - this.dropdownUI.open(options.target, { - x: rect.left, - y: rect.bottom + 5 - }); - } - return; - } + if (lineElement) { + const rect = lineElement.getBoundingClientRect(); + return { x: rect.left + 20, y: rect.bottom + 5 }; + } - if (options.position) { - this.openWithPosition(options.position); - return; - } + const editorEl = view.contentEl.querySelector(".cm-editor"); + if (editorEl) { + const rect = editorEl.getBoundingClientRect(); + return { x: rect.left + 100, y: rect.top + 100 }; + } + } catch (error) { + console.error("Error getting position for dropdown:", error); + } - this.openWithPosition({ - x: window.innerWidth / 2, - y: window.innerHeight / 3 - }); - } - - /** - * Open dropdown at a specific position using dummy target - */ - private openWithPosition(position: { x: number, y: number }): void { - const dummyTarget = createDummyTarget(position); - this.dropdownUI.open(dummyTarget, position); - - setTimeout(() => { - if (dummyTarget.parentNode) { - dummyTarget.parentNode.removeChild(dummyTarget); - } - }, 100); - } + return { x: window.innerWidth / 2, y: window.innerHeight / 3 }; + } - /** - * Find common statuses across multiple files - */ - private findCommonStatuses(files: TFile[]): string[] { - if (files.length === 0) return ['unknown']; - - const firstFileStatuses = this.statusService.getFileStatuses(files[0]); - - return firstFileStatuses.filter(status => - status !== 'unknown' && - files.every(file => this.statusService.getFileStatuses(file).includes(status)) - ); - } + /** + * Universal function to open the status dropdown + */ + public openStatusDropdown(options: DropdownOptions): void { + const activeFile = this.app.workspace.getActiveFile(); + const files = options.files || (activeFile ? [activeFile] : []); + if (!files.length) { + new Notice("No files selected"); + return; + } + if (!files.length || !files[0]) { + new Notice("No files selected"); + return; + } - - /** - * Render method (kept for compatibility) - */ - public render(): void { - // No-op - dropdown component handles rendering internally - } + if (this.dropdownUI.isOpen) { + this.resetDropdown(); + return; + } - /** - * Remove dropdown when plugin is unloaded - */ - public unload(): void { - this.dropdownUI.dispose(); - - if (this.toolbarButton) { - this.toolbarButton.remove(); - this.toolbarButton = undefined; - } - } + const isSingleFile = files.length === 1; + // Set up target files appropriately + if (isSingleFile) { + const targetFile = files[0]; + this.dropdownUI.setTargetFile(targetFile); + const currentStatuses = + this.statusService.getFileStatuses(targetFile); + this.dropdownUI.updateStatuses(currentStatuses); + } else { + // For multiple files, set the whole collection + this.dropdownUI.setTargetFiles(files); + const commonStatuses = this.findCommonStatuses(files); + this.dropdownUI.updateStatuses(commonStatuses); + } + + this.positionAndOpenDropdown(options); + } + + /** + * Reset dropdown state before opening + */ + public resetDropdown(): void { + this.dropdownUI.close(); + this.dropdownUI.setTargetFile(null); + } + + /** + * Position and open the dropdown + */ + private positionAndOpenDropdown(options: { + target?: HTMLElement; + position?: { x: number; y: number }; + editor?: Editor; + view?: MarkdownView; + }): void { + if (options.editor && options.view) { + const position = this.getCursorPosition( + options.editor, + options.view, + ); + this.openWithPosition(position); + return; + } + + if (options.target) { + if (options.position) { + this.dropdownUI.open(options.target, options.position); + } else { + const rect = options.target.getBoundingClientRect(); + this.dropdownUI.open(options.target, { + x: rect.left, + y: rect.bottom + 5, + }); + } + return; + } + + if (options.position) { + this.openWithPosition(options.position); + return; + } + + this.openWithPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 3, + }); + } + + /** + * Open dropdown at a specific position using dummy target + */ + private openWithPosition(position: { x: number; y: number }): void { + const dummyTarget = createDummyTarget(position); + this.dropdownUI.open(dummyTarget, position); + + setTimeout(() => { + if (dummyTarget.parentNode) { + dummyTarget.parentNode.removeChild(dummyTarget); + } + }, 100); + } + + /** + * Find common statuses across multiple files + */ + private findCommonStatuses(files: TFile[]): string[] { + if (files.length === 0) return ["unknown"]; + + const firstFileStatuses = this.statusService.getFileStatuses(files[0]); + + return firstFileStatuses.filter( + (status) => + status !== "unknown" && + files.every((file) => + this.statusService.getFileStatuses(file).includes(status), + ), + ); + } + + /** + * Render method (kept for compatibility) + */ + public render(): void { + // No-op - dropdown component handles rendering internally + } + + /** + * Remove dropdown when plugin is unloaded + */ + public unload(): void { + this.dropdownUI.dispose(); + + if (this.toolbarButton) { + this.toolbarButton.remove(); + this.toolbarButton = undefined; + } + } } diff --git a/components/status-dropdown/dropdown-position.ts b/components/status-dropdown/dropdown-position.ts index 1601581..751b38f 100644 --- a/components/status-dropdown/dropdown-position.ts +++ b/components/status-dropdown/dropdown-position.ts @@ -6,100 +6,118 @@ * Position the dropdown */ export function positionDropdown(options: { - dropdownElement: HTMLElement, - targetEl: HTMLElement, - position?: { x: number, y: number } + dropdownElement: HTMLElement; + targetEl: HTMLElement; + position?: { x: number; y: number }; }): void { - const { dropdownElement, targetEl, position } = options; - - if (position) { - positionAt(dropdownElement, position.x, position.y); - } else { - positionRelativeTo(dropdownElement, targetEl); - } + const { dropdownElement, targetEl, position } = options; + + if (position) { + positionAt(dropdownElement, position.x, position.y); + } else { + positionRelativeTo(dropdownElement, targetEl); + } } /** * Position the dropdown at specific coordinates */ function positionAt(dropdownElement: HTMLElement, x: number, y: number): void { - dropdownElement.addClass('note-status-popover-fixed'); - dropdownElement.style.setProperty('--pos-x-px', `${x}px`); - dropdownElement.style.setProperty('--pos-y-px', `${y}px`); - - setTimeout(() => adjustPositionToViewport(dropdownElement), 0); + dropdownElement.addClass("note-status-popover-fixed"); + dropdownElement.style.setProperty("--pos-x-px", `${x}px`); + dropdownElement.style.setProperty("--pos-y-px", `${y}px`); + + setTimeout(() => adjustPositionToViewport(dropdownElement), 0); } /** * Adjust the dropdown position to ensure it's visible in the viewport */ function adjustPositionToViewport(dropdownElement: HTMLElement): void { - const rect = dropdownElement.getBoundingClientRect(); - - if (rect.right > window.innerWidth) { - dropdownElement.addClass('note-status-popover-right'); - dropdownElement.style.setProperty('--right-offset-px', '10px'); - } else { - dropdownElement.removeClass('note-status-popover-right'); - } - - if (rect.bottom > window.innerHeight) { - dropdownElement.addClass('note-status-popover-bottom'); - dropdownElement.style.setProperty('--bottom-offset-px', '10px'); - } else { - dropdownElement.removeClass('note-status-popover-bottom'); - } - - const maxHeight = window.innerHeight - rect.top - 20; - dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); + const rect = dropdownElement.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + dropdownElement.addClass("note-status-popover-right"); + dropdownElement.style.setProperty("--right-offset-px", "10px"); + } else { + dropdownElement.removeClass("note-status-popover-right"); + } + + if (rect.bottom > window.innerHeight) { + dropdownElement.addClass("note-status-popover-bottom"); + dropdownElement.style.setProperty("--bottom-offset-px", "10px"); + } else { + dropdownElement.removeClass("note-status-popover-bottom"); + } + + const maxHeight = window.innerHeight - rect.top - 20; + dropdownElement.style.setProperty("--max-height-px", `${maxHeight}px`); } /** * Position the dropdown relative to a target element */ -function positionRelativeTo(dropdownElement: HTMLElement, targetEl: HTMLElement): void { - dropdownElement.addClass('note-status-popover-fixed'); - - const targetRect = targetEl.getBoundingClientRect(); - - dropdownElement.style.setProperty('--pos-y-px', `${targetRect.bottom + 5}px`); - dropdownElement.style.setProperty('--pos-x-px', `${targetRect.left}px`); - - setTimeout(() => adjustRelativePosition(dropdownElement, targetRect), 0); +function positionRelativeTo( + dropdownElement: HTMLElement, + targetEl: HTMLElement, +): void { + dropdownElement.addClass("note-status-popover-fixed"); + + const targetRect = targetEl.getBoundingClientRect(); + + dropdownElement.style.setProperty( + "--pos-y-px", + `${targetRect.bottom + 5}px`, + ); + dropdownElement.style.setProperty("--pos-x-px", `${targetRect.left}px`); + + setTimeout(() => adjustRelativePosition(dropdownElement, targetRect), 0); } /** * Adjust position when positioned relative to an element */ -function adjustRelativePosition(dropdownElement: HTMLElement, targetRect: DOMRect): void { - const rect = dropdownElement.getBoundingClientRect(); - - if (rect.right > window.innerWidth) { - dropdownElement.addClass('note-status-popover-right'); - dropdownElement.style.setProperty('--right-offset-px', `${window.innerWidth - targetRect.right}px`); - } else { - dropdownElement.removeClass('note-status-popover-right'); - } - - if (rect.bottom > window.innerHeight) { - dropdownElement.addClass('note-status-popover-bottom'); - dropdownElement.style.setProperty('--bottom-offset-px', `${window.innerHeight - targetRect.top + 5}px`); - } else { - dropdownElement.removeClass('note-status-popover-bottom'); - } - - const maxHeight = window.innerHeight - rect.top - 20; - dropdownElement.style.setProperty('--max-height-px', `${maxHeight}px`); +function adjustRelativePosition( + dropdownElement: HTMLElement, + targetRect: DOMRect, +): void { + const rect = dropdownElement.getBoundingClientRect(); + + if (rect.right > window.innerWidth) { + dropdownElement.addClass("note-status-popover-right"); + dropdownElement.style.setProperty( + "--right-offset-px", + `${window.innerWidth - targetRect.right}px`, + ); + } else { + dropdownElement.removeClass("note-status-popover-right"); + } + + if (rect.bottom > window.innerHeight) { + dropdownElement.addClass("note-status-popover-bottom"); + dropdownElement.style.setProperty( + "--bottom-offset-px", + `${window.innerHeight - targetRect.top + 5}px`, + ); + } else { + dropdownElement.removeClass("note-status-popover-bottom"); + } + + const maxHeight = window.innerHeight - rect.top - 20; + dropdownElement.style.setProperty("--max-height-px", `${maxHeight}px`); } /** * Create a dummy target element for positioning */ -export function createDummyTarget(position: { x: number, y: number }): HTMLElement { - const dummyTarget = document.createElement('div'); - dummyTarget.addClass('note-status-dummy-target'); - dummyTarget.style.setProperty('--pos-x-px', `${position.x}px`); - dummyTarget.style.setProperty('--pos-y-px', `${position.y}px`); - document.body.appendChild(dummyTarget); - return dummyTarget; -} \ No newline at end of file +export function createDummyTarget(position: { + x: number; + y: number; +}): HTMLElement { + const dummyTarget = document.createElement("div"); + dummyTarget.addClass("note-status-dummy-target"); + dummyTarget.style.setProperty("--pos-x-px", `${position.x}px`); + dummyTarget.style.setProperty("--pos-y-px", `${position.y}px`); + document.body.appendChild(dummyTarget); + return dummyTarget; +} diff --git a/components/status-dropdown/dropdown-ui.ts b/components/status-dropdown/dropdown-ui.ts index d3b9deb..975e5ef 100644 --- a/components/status-dropdown/dropdown-ui.ts +++ b/components/status-dropdown/dropdown-ui.ts @@ -1,220 +1,241 @@ -import { App, TFile } from 'obsidian'; -import { DropdownDependencies, StatusRemoveHandler, StatusSelectHandler } from './types'; -import { positionDropdown } from './dropdown-position'; -import { renderDropdownContent } from './dropdown-render'; -import { setupDropdownEvents } from './dropdown-events'; -import { StatusService } from 'services/status-service'; -import { NoteStatusSettings } from 'models/types'; +import { App, TFile } from "obsidian"; +import { + DropdownDependencies, + StatusRemoveHandler, + StatusSelectHandler, +} from "./types"; +import { positionDropdown } from "./dropdown-position"; +import { renderDropdownContent } from "./dropdown-render"; +import { setupDropdownEvents } from "./dropdown-events"; +import { StatusService } from "services/status-service"; +import { NoteStatusSettings } from "models/types"; /** * Core UI component for the status dropdown */ export class DropdownUI { - private app: App; - private statusService: StatusService; - private settings: NoteStatusSettings; - - private dropdownElement: HTMLElement | null = null; - private currentStatuses: string[] = ['unknown']; - private targetFile: TFile | null = null; - private targetFiles: TFile[] = []; - private animationDuration = 220; - - public isOpen = false; - private onRemoveStatus: StatusRemoveHandler = async () => {}; - private onSelectStatus: StatusSelectHandler = async () => {}; - - // Event handlers - private clickOutsideHandler: (e: MouseEvent) => void; - private escapeKeyHandler: (e: KeyboardEvent) => void; + private app: App; + private statusService: StatusService; + private settings: NoteStatusSettings; - constructor({ app, settings, statusService }: DropdownDependencies) { - this.app = app; - this.statusService = statusService; - this.settings = settings; - - // Bind methods - this.clickOutsideHandler = this.handleClickOutside.bind(this); - this.escapeKeyHandler = this.handleEscapeKey.bind(this); - } - - /** - * Set the target file for status updates - */ - public setTargetFile(file: TFile | null): void { - this.targetFile = file; - this.targetFiles = file ? [file] : []; - } + private dropdownElement: HTMLElement | null = null; + private currentStatuses: string[] = ["unknown"]; + private targetFile: TFile | null = null; + private targetFiles: TFile[] = []; + private animationDuration = 220; - /** - * Set multiple target files for status updates - */ - public setTargetFiles(files: TFile[]): void { - this.targetFiles = [...files]; - this.targetFile = files.length === 1 ? files[0] : null; - } + public isOpen = false; + private onRemoveStatus: StatusRemoveHandler = async () => {}; + private onSelectStatus: StatusSelectHandler = async () => {}; - /** - * Register handler for removing a status - */ - public setOnRemoveStatusHandler(handler: StatusRemoveHandler): void { - this.onRemoveStatus = handler; - } - - /** - * Register handler for selecting a status - */ - public setOnSelectStatusHandler(handler: StatusSelectHandler): void { - this.onSelectStatus = handler; - } + // Event handlers + private clickOutsideHandler: (e: MouseEvent) => void; + private escapeKeyHandler: (e: KeyboardEvent) => void; - /** - * Updates the current statuses - */ - public updateStatuses(statuses: string[] | string): void { - this.currentStatuses = Array.isArray(statuses) ? [...statuses] : [statuses]; - - if (this.isOpen && this.dropdownElement) { - this.refreshDropdownContent(); - } - } - - /** - * Updates settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - - if (this.isOpen && this.dropdownElement) { - this.refreshDropdownContent(); - } - } - - /** - * Toggle the dropdown visibility - */ - public toggle(targetEl: HTMLElement, position?: { x: number, y: number }): void { - if (this.isOpen) { - this.close(); - setTimeout(() => { - if (!this.isOpen && !this.dropdownElement) { - this.open(targetEl, position); - } - }, 50); - } else { - this.open(targetEl, position); - } - } - - /** - * Open the dropdown - */ - public open(targetEl: HTMLElement, position?: { x: number, y: number }): void { - if (this.isOpen || this.dropdownElement) { - this.close(); - setTimeout(() => this.actuallyOpen(targetEl, position), 10); - return; - } - - this.actuallyOpen(targetEl, position); - } + constructor({ app, settings, statusService }: DropdownDependencies) { + this.app = app; + this.statusService = statusService; + this.settings = settings; - /** - * Actually open the dropdown (internal method) - */ - private actuallyOpen(targetEl: HTMLElement, position?: { x: number, y: number }): void { - this.isOpen = true; - - // Create dropdown element - this.createDropdownElement(); - this.refreshDropdownContent(); - - // Position the dropdown - positionDropdown({ - dropdownElement: this.dropdownElement!, - targetEl, - position - }); - - this.dropdownElement?.addClass('note-status-popover-animate-in'); - - // Add event listeners with slight delay to prevent immediate triggering - setTimeout(() => { - setupDropdownEvents({ - clickOutsideHandler: this.clickOutsideHandler, - escapeKeyHandler: this.escapeKeyHandler - }); - }, 10); - } - - /** - * Create the dropdown element - */ - private createDropdownElement(): void { - this.dropdownElement = document.createElement('div'); - this.dropdownElement.addClass('note-status-popover', 'note-status-unified-dropdown'); - document.body.appendChild(this.dropdownElement); - } - - /** - * Close the dropdown - */ - public close(): void { - if (!this.dropdownElement || !this.isOpen) return; - - this.dropdownElement.addClass('note-status-popover-animate-out'); - - document.removeEventListener('click', this.clickOutsideHandler); - document.removeEventListener('keydown', this.escapeKeyHandler); - - this.isOpen = false; - - if (this.dropdownElement) { - this.dropdownElement.remove(); - this.dropdownElement = null; - } - } - - /** - * Refresh the dropdown content - */ - private refreshDropdownContent(): void { - if (!this.dropdownElement) return; - - renderDropdownContent({ - dropdownElement: this.dropdownElement, - settings: this.settings, - statusService: this.statusService, - currentStatuses: this.currentStatuses, - targetFile: this.targetFile, - targetFiles: this.targetFiles, - onRemoveStatus: this.onRemoveStatus, - onSelectStatus: this.onSelectStatus - }); - } - - /** - * Handle click outside the dropdown - */ - private handleClickOutside(e: MouseEvent): void { - if (this.dropdownElement && !this.dropdownElement.contains(e.target as Node)) { - this.close(); - } - } - - /** - * Handle escape key to close dropdown - */ - private handleEscapeKey(e: KeyboardEvent): void { - if (e.key === 'Escape') { - this.close(); - } - } - - /** - * Dispose of resources - */ - public dispose(): void { - this.close(); - } -} \ No newline at end of file + // Bind methods + this.clickOutsideHandler = this.handleClickOutside.bind(this); + this.escapeKeyHandler = this.handleEscapeKey.bind(this); + } + + /** + * Set the target file for status updates + */ + public setTargetFile(file: TFile | null): void { + this.targetFile = file; + this.targetFiles = file ? [file] : []; + } + + /** + * Set multiple target files for status updates + */ + public setTargetFiles(files: TFile[]): void { + this.targetFiles = [...files]; + this.targetFile = files.length === 1 ? files[0] : null; + } + + /** + * Register handler for removing a status + */ + public setOnRemoveStatusHandler(handler: StatusRemoveHandler): void { + this.onRemoveStatus = handler; + } + + /** + * Register handler for selecting a status + */ + public setOnSelectStatusHandler(handler: StatusSelectHandler): void { + this.onSelectStatus = handler; + } + + /** + * Updates the current statuses + */ + public updateStatuses(statuses: string[] | string): void { + this.currentStatuses = Array.isArray(statuses) + ? [...statuses] + : [statuses]; + + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Updates settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + + if (this.isOpen && this.dropdownElement) { + this.refreshDropdownContent(); + } + } + + /** + * Toggle the dropdown visibility + */ + public toggle( + targetEl: HTMLElement, + position?: { x: number; y: number }, + ): void { + if (this.isOpen) { + this.close(); + setTimeout(() => { + if (!this.isOpen && !this.dropdownElement) { + this.open(targetEl, position); + } + }, 50); + } else { + this.open(targetEl, position); + } + } + + /** + * Open the dropdown + */ + public open( + targetEl: HTMLElement, + position?: { x: number; y: number }, + ): void { + if (this.isOpen || this.dropdownElement) { + this.close(); + setTimeout(() => this.actuallyOpen(targetEl, position), 10); + return; + } + + this.actuallyOpen(targetEl, position); + } + + /** + * Actually open the dropdown (internal method) + */ + private actuallyOpen( + targetEl: HTMLElement, + position?: { x: number; y: number }, + ): void { + this.isOpen = true; + + // Create dropdown element + this.createDropdownElement(); + this.refreshDropdownContent(); + + // Position the dropdown + positionDropdown({ + dropdownElement: this.dropdownElement!, + targetEl, + position, + }); + + this.dropdownElement?.addClass("note-status-popover-animate-in"); + + // Add event listeners with slight delay to prevent immediate triggering + setTimeout(() => { + setupDropdownEvents({ + clickOutsideHandler: this.clickOutsideHandler, + escapeKeyHandler: this.escapeKeyHandler, + }); + }, 10); + } + + /** + * Create the dropdown element + */ + private createDropdownElement(): void { + this.dropdownElement = document.createElement("div"); + this.dropdownElement.addClass( + "note-status-popover", + "note-status-unified-dropdown", + ); + document.body.appendChild(this.dropdownElement); + } + + /** + * Close the dropdown + */ + public close(): void { + if (!this.dropdownElement || !this.isOpen) return; + + this.dropdownElement.addClass("note-status-popover-animate-out"); + + document.removeEventListener("click", this.clickOutsideHandler); + document.removeEventListener("keydown", this.escapeKeyHandler); + + this.isOpen = false; + + if (this.dropdownElement) { + this.dropdownElement.remove(); + this.dropdownElement = null; + } + } + + /** + * Refresh the dropdown content + */ + private refreshDropdownContent(): void { + if (!this.dropdownElement) return; + + renderDropdownContent({ + dropdownElement: this.dropdownElement, + settings: this.settings, + statusService: this.statusService, + currentStatuses: this.currentStatuses, + targetFile: this.targetFile, + targetFiles: this.targetFiles, + onRemoveStatus: this.onRemoveStatus, + onSelectStatus: this.onSelectStatus, + }); + } + + /** + * Handle click outside the dropdown + */ + private handleClickOutside(e: MouseEvent): void { + if ( + this.dropdownElement && + !this.dropdownElement.contains(e.target as Node) + ) { + this.close(); + } + } + + /** + * Handle escape key to close dropdown + */ + private handleEscapeKey(e: KeyboardEvent): void { + if (e.key === "Escape") { + this.close(); + } + } + + /** + * Dispose of resources + */ + public dispose(): void { + this.close(); + } +} diff --git a/components/status-dropdown/index.ts b/components/status-dropdown/index.ts index 2e49ef2..b65b013 100644 --- a/components/status-dropdown/index.ts +++ b/components/status-dropdown/index.ts @@ -1,6 +1,6 @@ -import { DropdownManager } from './dropdown-manager'; -import { DropdownOptions } from './types'; +import { DropdownManager } from "./dropdown-manager"; +import { DropdownOptions } from "./types"; export type { DropdownOptions }; export { DropdownManager as StatusDropdown }; -export default DropdownManager; \ No newline at end of file +export default DropdownManager; diff --git a/components/status-dropdown/types.ts b/components/status-dropdown/types.ts index da57db6..08af411 100644 --- a/components/status-dropdown/types.ts +++ b/components/status-dropdown/types.ts @@ -1,36 +1,42 @@ // # Tipos comunes -import { NoteStatusSettings } from 'models/types'; -import { App, TFile, Editor, MarkdownView } from 'obsidian'; -import { StatusService } from 'services/status-service'; +import { NoteStatusSettings } from "models/types"; +import { App, TFile, Editor, MarkdownView } from "obsidian"; +import { StatusService } from "services/status-service"; /** * Options for opening status dropdown */ export interface DropdownOptions { - target?: HTMLElement; - position?: { x: number, y: number }; - files?: TFile[]; - editor?: Editor; - view?: MarkdownView; - mode?: 'replace' | 'add' | 'remove' | 'toggle'; - onStatusChange?: (statuses: string[]) => void; + target?: HTMLElement; + position?: { x: number; y: number }; + files?: TFile[]; + editor?: Editor; + view?: MarkdownView; + mode?: "replace" | "add" | "remove" | "toggle"; + onStatusChange?: (statuses: string[]) => void; } /** * Status removal handler function type */ -export type StatusRemoveHandler = (status: string, targetFile?: TFile | TFile[]) => Promise; +export type StatusRemoveHandler = ( + status: string, + targetFile?: TFile | TFile[], +) => Promise; /** * Status selection handler function type */ -export type StatusSelectHandler = (status: string, targetFile: TFile | TFile[]) => Promise; +export type StatusSelectHandler = ( + status: string, + targetFile: TFile | TFile[], +) => Promise; /** * Common dependencies for dropdown components */ export interface DropdownDependencies { - app: App; - settings: NoteStatusSettings; - statusService: StatusService; + app: App; + settings: NoteStatusSettings; + statusService: StatusService; } diff --git a/components/toolbar-button.ts b/components/toolbar-button.ts index af204bb..9e78c3d 100644 --- a/components/toolbar-button.ts +++ b/components/toolbar-button.ts @@ -2,74 +2,84 @@ import { NoteStatusSettings } from "models/types"; import { StatusService } from "services/status-service"; export class ToolbarButton { - private element: HTMLElement | null = null; - private settings: NoteStatusSettings; - private statusService: StatusService; - - constructor(settings: NoteStatusSettings, statusService: StatusService) { - this.settings = settings; - this.statusService = statusService; - } - - public createElement(): HTMLElement { - const button = document.createElement('button'); - button.addClass('note-status-toolbar-button', 'clickable-icon', 'view-action'); - button.setAttribute('aria-label', 'Note status'); - - this.element = button; - return button; - } - - public updateDisplay(statuses: string[]): void { - if (!this.element) return; - - this.element.empty(); - - const hasValidStatus = statuses.length > 0 && statuses[0] !== 'unknown'; - const badgeContainer = document.createElement('div'); - badgeContainer.addClass('note-status-toolbar-badge-container'); - - if (hasValidStatus) { - this.renderStatusBadge(badgeContainer, statuses); - } else { - this.renderUnknownBadge(badgeContainer); - } - - this.element.appendChild(badgeContainer); - } - - private renderStatusBadge(container: HTMLElement, statuses: string[]): void { - const primaryStatus = statuses[0]; - const icon = this.statusService.getStatusIcon(primaryStatus); - - const iconSpan = document.createElement('span'); - iconSpan.addClass(`note-status-toolbar-icon`, `status-${primaryStatus}`); - iconSpan.textContent = icon; - container.appendChild(iconSpan); - - if (this.settings.useMultipleStatuses && statuses.length > 1) { - const countBadge = document.createElement('span'); - countBadge.addClass('note-status-count-badge'); - countBadge.textContent = `${statuses.length}`; - container.appendChild(countBadge); - } - } - - private renderUnknownBadge(container: HTMLElement): void { - const iconSpan = document.createElement('span'); - iconSpan.addClass('note-status-toolbar-icon', 'status-unknown'); - iconSpan.textContent = this.statusService.getStatusIcon('unknown'); - container.appendChild(iconSpan); - } - - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - } - - public destroy(): void { - if (this.element) { - this.element.remove(); - this.element = null; - } - } -} \ No newline at end of file + private element: HTMLElement | null = null; + private settings: NoteStatusSettings; + private statusService: StatusService; + + constructor(settings: NoteStatusSettings, statusService: StatusService) { + this.settings = settings; + this.statusService = statusService; + } + + public createElement(): HTMLElement { + const button = document.createElement("button"); + button.addClass( + "note-status-toolbar-button", + "clickable-icon", + "view-action", + ); + button.setAttribute("aria-label", "Note status"); + + this.element = button; + return button; + } + + public updateDisplay(statuses: string[]): void { + if (!this.element) return; + + this.element.empty(); + + const hasValidStatus = statuses.length > 0 && statuses[0] !== "unknown"; + const badgeContainer = document.createElement("div"); + badgeContainer.addClass("note-status-toolbar-badge-container"); + + if (hasValidStatus) { + this.renderStatusBadge(badgeContainer, statuses); + } else { + this.renderUnknownBadge(badgeContainer); + } + + this.element.appendChild(badgeContainer); + } + + private renderStatusBadge( + container: HTMLElement, + statuses: string[], + ): void { + const primaryStatus = statuses[0]; + const icon = this.statusService.getStatusIcon(primaryStatus); + + const iconSpan = document.createElement("span"); + iconSpan.addClass( + `note-status-toolbar-icon`, + `status-${primaryStatus}`, + ); + iconSpan.textContent = icon; + container.appendChild(iconSpan); + + if (this.settings.useMultipleStatuses && statuses.length > 1) { + const countBadge = document.createElement("span"); + countBadge.addClass("note-status-count-badge"); + countBadge.textContent = `${statuses.length}`; + container.appendChild(countBadge); + } + } + + private renderUnknownBadge(container: HTMLElement): void { + const iconSpan = document.createElement("span"); + iconSpan.addClass("note-status-toolbar-icon", "status-unknown"); + iconSpan.textContent = this.statusService.getStatusIcon("unknown"); + container.appendChild(iconSpan); + } + + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } + + public destroy(): void { + if (this.element) { + this.element.remove(); + this.element = null; + } + } +} diff --git a/constants/defaults.ts b/constants/defaults.ts index c3de4b1..71c1fbb 100644 --- a/constants/defaults.ts +++ b/constants/defaults.ts @@ -1,20 +1,20 @@ -import { NoteStatusSettings } from '../models/types'; -import { DEFAULT_ENABLED_TEMPLATES } from '../constants/status-templates'; +import { NoteStatusSettings } from "../models/types"; +import { DEFAULT_ENABLED_TEMPLATES } from "../constants/status-templates"; /** * Default plugin settings */ export const DEFAULT_SETTINGS: NoteStatusSettings = { statusColors: { - active: 'var(--text-success)', - onHold: 'var(--text-warning)', - completed: 'var(--text-accent)', - dropped: 'var(--text-error)', - unknown: 'var(--text-muted)' + active: "var(--text-success)", + onHold: "var(--text-warning)", + completed: "var(--text-accent)", + dropped: "var(--text-error)", + unknown: "var(--text-muted)", }, showStatusBar: true, autoHideStatusBar: false, - customStatuses: [], + customStatuses: [], showStatusIconsInExplorer: true, hideUnknownStatusInExplorer: false, // Default to show unknown status collapsedStatuses: {}, @@ -22,19 +22,19 @@ export const DEFAULT_SETTINGS: NoteStatusSettings = { enabledTemplates: DEFAULT_ENABLED_TEMPLATES, useCustomStatusesOnly: false, useMultipleStatuses: true, - tagPrefix: 'obsidian-note-status', + tagPrefix: "obsidian-note-status", strictStatuses: false, // Default to show all statuses from frontmatter excludeUnknownStatus: true, // Default to exclude unknown status files for better performance - quickStatusCommands: ['active', 'completed'], // Add default quick commands + quickStatusCommands: ["active", "completed"], // Add default quick commands }; /** * Default colors in hexadecimal format for backup and reset */ export 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 + 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 }; diff --git a/constants/icons.ts b/constants/icons.ts index 6472992..00d4571 100644 --- a/constants/icons.ts +++ b/constants/icons.ts @@ -20,5 +20,5 @@ export const ICONS = { refresh: ``, collapseDown: ``, collapseRight: ``, - file: `` + file: ``, }; diff --git a/constants/status-templates.ts b/constants/status-templates.ts index 811f2dc..2cceac3 100644 --- a/constants/status-templates.ts +++ b/constants/status-templates.ts @@ -1,78 +1,79 @@ -import { Status } from '../models/types'; +import { Status } from "../models/types"; /** * Status Template interface */ export interface StatusTemplate { - id: string; - name: string; - description: string; - statuses: Status[]; + id: string; + name: string; + description: string; + statuses: Status[]; } /** * Predefined status templates */ export const PREDEFINED_TEMPLATES: StatusTemplate[] = [ - { - id: 'colorful', - name: 'Colorful workflow', - description: 'A colorful set of workflow statuses with descriptive icons', - statuses: [ - { name: 'idea', icon: '💡', color: '#FFEB3B' }, - { name: 'draft', icon: '📝', color: '#E0E0E0' }, - { name: 'inProgress', icon: '🔧', color: '#FFC107' }, - { name: 'editing', icon: '🖊️', color: '#2196F3' }, - { name: 'pending', icon: '⏳', color: '#9C27B0' }, - { name: 'onHold', icon: '⏸', color: '#9E9E9E' }, - { name: 'needsUpdate', icon: '🔄', color: '#FF5722' }, - { name: 'completed', icon: '✅', color: '#4CAF50' }, - { name: 'archived', icon: '📦', color: '#795548' } - ] - }, - { - id: 'minimal', - name: 'Minimal workflow', - description: 'A simplified set of essential workflow statuses', - statuses: [ - { name: 'todo', icon: '📌', color: '#F44336' }, - { name: 'inProgress', icon: '⚙️', color: '#2196F3' }, - { name: 'review', icon: '👀', color: '#9C27B0' }, - { name: 'done', icon: '✓', color: '#4CAF50' } - ] - }, - { - id: 'academic', - name: 'Academic research', - description: 'Status workflow for academic research and writing', - statuses: [ - { name: 'research', icon: '🔍', color: '#2196F3' }, - { name: 'outline', icon: '📑', color: '#9E9E9E' }, - { name: 'draft', icon: '✏️', color: '#FFC107' }, - { name: 'review', icon: '🔬', color: '#9C27B0' }, - { name: 'revision', icon: '📝', color: '#FF5722' }, - { name: 'final', icon: '📚', color: '#4CAF50' }, - { name: 'published', icon: '🎓', color: '#795548' } - ] - }, - { - id: 'project', - name: 'Project management', - description: 'Status workflow for project management and tracking', - statuses: [ - { name: 'planning', icon: '🗓️', color: '#9E9E9E' }, - { name: 'backlog', icon: '📋', color: '#E0E0E0' }, - { name: 'ready', icon: '🚦', color: '#8BC34A' }, - { name: 'inDevelopment', icon: '👨‍💻', color: '#2196F3' }, - { name: 'testing', icon: '🧪', color: '#9C27B0' }, - { name: 'review', icon: '👁️', color: '#FFC107' }, - { name: 'approved', icon: '👍', color: '#4CAF50' }, - { name: 'live', icon: '🚀', color: '#3F51B5' } - ] - } + { + id: "colorful", + name: "Colorful workflow", + description: + "A colorful set of workflow statuses with descriptive icons", + statuses: [ + { name: "idea", icon: "💡", color: "#FFEB3B" }, + { name: "draft", icon: "📝", color: "#E0E0E0" }, + { name: "inProgress", icon: "🔧", color: "#FFC107" }, + { name: "editing", icon: "🖊️", color: "#2196F3" }, + { name: "pending", icon: "⏳", color: "#9C27B0" }, + { name: "onHold", icon: "⏸", color: "#9E9E9E" }, + { name: "needsUpdate", icon: "🔄", color: "#FF5722" }, + { name: "completed", icon: "✅", color: "#4CAF50" }, + { name: "archived", icon: "📦", color: "#795548" }, + ], + }, + { + id: "minimal", + name: "Minimal workflow", + description: "A simplified set of essential workflow statuses", + statuses: [ + { name: "todo", icon: "📌", color: "#F44336" }, + { name: "inProgress", icon: "⚙️", color: "#2196F3" }, + { name: "review", icon: "👀", color: "#9C27B0" }, + { name: "done", icon: "✓", color: "#4CAF50" }, + ], + }, + { + id: "academic", + name: "Academic research", + description: "Status workflow for academic research and writing", + statuses: [ + { name: "research", icon: "🔍", color: "#2196F3" }, + { name: "outline", icon: "📑", color: "#9E9E9E" }, + { name: "draft", icon: "✏️", color: "#FFC107" }, + { name: "review", icon: "🔬", color: "#9C27B0" }, + { name: "revision", icon: "📝", color: "#FF5722" }, + { name: "final", icon: "📚", color: "#4CAF50" }, + { name: "published", icon: "🎓", color: "#795548" }, + ], + }, + { + id: "project", + name: "Project management", + description: "Status workflow for project management and tracking", + statuses: [ + { name: "planning", icon: "🗓️", color: "#9E9E9E" }, + { name: "backlog", icon: "📋", color: "#E0E0E0" }, + { name: "ready", icon: "🚦", color: "#8BC34A" }, + { name: "inDevelopment", icon: "👨‍💻", color: "#2196F3" }, + { name: "testing", icon: "🧪", color: "#9C27B0" }, + { name: "review", icon: "👁️", color: "#FFC107" }, + { name: "approved", icon: "👍", color: "#4CAF50" }, + { name: "live", icon: "🚀", color: "#3F51B5" }, + ], + }, ]; /** * Default template IDs that should be enabled by default */ -export const DEFAULT_ENABLED_TEMPLATES = ['colorful']; \ No newline at end of file +export const DEFAULT_ENABLED_TEMPLATES = ["colorful"]; diff --git a/integrations/context-menu/status-context-menu.ts b/integrations/context-menu/status-context-menu.ts index 0dbc150..807fc23 100644 --- a/integrations/context-menu/status-context-menu.ts +++ b/integrations/context-menu/status-context-menu.ts @@ -1,130 +1,146 @@ -import { App, Menu, TFile } from 'obsidian'; -import { NoteStatusSettings } from 'models/types'; -import { StatusService } from 'services/status-service'; -import { StatusDropdown } from 'components/status-dropdown'; -import { ExplorerIntegration } from 'integrations/explorer/explorer-integration'; +import { App, Menu, TFile } from "obsidian"; +import { NoteStatusSettings } from "models/types"; +import { StatusService } from "services/status-service"; +import { StatusDropdown } from "components/status-dropdown"; +import { ExplorerIntegration } from "integrations/explorer/explorer-integration"; /** * Gestiona los menús contextuales para cambios de estado */ export class StatusContextMenu { - private app: App; - private settings: NoteStatusSettings; - private statusService: StatusService; - private statusDropdown: StatusDropdown; - private explorerIntegration: ExplorerIntegration; + private app: App; + private settings: NoteStatusSettings; + private statusService: StatusService; + private statusDropdown: StatusDropdown; + private explorerIntegration: ExplorerIntegration; - constructor( - app: App, - settings: NoteStatusSettings, - statusService: StatusService, - statusDropdown: StatusDropdown, - explorerIntegration: ExplorerIntegration - ) { - this.app = app; - this.settings = settings; - this.statusService = statusService; - this.statusDropdown = statusDropdown; - this.explorerIntegration = explorerIntegration; - } + constructor( + app: App, + settings: NoteStatusSettings, + statusService: StatusService, + statusDropdown: StatusDropdown, + explorerIntegration: ExplorerIntegration, + ) { + this.app = app; + this.settings = settings; + this.statusService = statusService; + this.statusDropdown = statusDropdown; + this.explorerIntegration = explorerIntegration; + } - /** - * Actualiza la configuración - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - } - - /** - * Añade ítem de menú para cambiar estado de un archivo - */ - public addStatusMenuItemToSingleFile(menu: Menu, file: TFile, onClick: (file: TFile) => void): void { - menu.addItem(item => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => onClick(file)) - ); - } - - /** - * Añade ítem de menú para cambiar estado de múltiples archivos - */ - public addStatusMenuItemToBatch(menu: Menu, files: TFile[], onClick: (files: TFile[]) => void): void { - menu.addItem(item => - item - .setTitle('Change status') - .setIcon('tag') - .onClick(() => onClick(files)) - ); - } - - /** - * Muestra el menú contextual para cambiar estado de uno o más archivos - * @param files Archivos a los que cambiar el estado - * @param position Posición opcional para mostrar el menú - */ - public showForFiles(files: TFile[], position?: { x: number; y: number }): void { - if (files.length === 0) return; - - if (files.length === 1) { - this.showForSingleFile(files[0], position); - } else { - this.showForMultipleFiles(files, position); - } - } - - /** - * Muestra el menú contextual para un solo archivo - * @param file Archivo al que cambiar el estado - * @param position Posición opcional para mostrar el menú - */ - private showForSingleFile(file: TFile, position?: { x: number; y: number }): void { - if (!(file instanceof TFile) || file.extension !== 'md') return; + /** + * Actualiza la configuración + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + } - this.statusDropdown.openStatusDropdown({ - position, - files: [file] - }); - } - - /** - * Muestra el menú contextual para múltiples archivos - * @param files Archivos a los que cambiar el estado - * @param position Posición opcional para mostrar el menú - */ - private showForMultipleFiles(files: TFile[], position?: { x: number; y: number }): void { - const menu = new Menu(); - - // Elemento de información (deshabilitado) - menu.addItem((item) => { - item.setTitle(`Update ${files.length} files`) - .setDisabled(true); - return item; - }); + /** + * Añade ítem de menú para cambiar estado de un archivo + */ + public addStatusMenuItemToSingleFile( + menu: Menu, + file: TFile, + onClick: (file: TFile) => void, + ): void { + menu.addItem((item) => + item + .setTitle("Change status") + .setIcon("tag") + .onClick(() => onClick(file)), + ); + } - // Opción para gestionar estados - menu.addItem((item) => - item - .setTitle('Manage statuses...') - .setIcon('tag') - .onClick(() => { - this.statusDropdown.openStatusDropdown({ - position, - files - }); - }) - ); - - // Mostrar el menú en la posición adecuada - if (position) { - menu.showAtPosition(position); - } else { - // Use a centered position - menu.showAtPosition({ - x: window.innerWidth / 2, - y: window.innerHeight / 3 - }); - } - } -} \ No newline at end of file + /** + * Añade ítem de menú para cambiar estado de múltiples archivos + */ + public addStatusMenuItemToBatch( + menu: Menu, + files: TFile[], + onClick: (files: TFile[]) => void, + ): void { + menu.addItem((item) => + item + .setTitle("Change status") + .setIcon("tag") + .onClick(() => onClick(files)), + ); + } + + /** + * Muestra el menú contextual para cambiar estado de uno o más archivos + * @param files Archivos a los que cambiar el estado + * @param position Posición opcional para mostrar el menú + */ + public showForFiles( + files: TFile[], + position?: { x: number; y: number }, + ): void { + if (files.length === 0) return; + + if (files.length === 1) { + this.showForSingleFile(files[0], position); + } else { + this.showForMultipleFiles(files, position); + } + } + + /** + * Muestra el menú contextual para un solo archivo + * @param file Archivo al que cambiar el estado + * @param position Posición opcional para mostrar el menú + */ + private showForSingleFile( + file: TFile, + position?: { x: number; y: number }, + ): void { + if (!(file instanceof TFile) || file.extension !== "md") return; + + this.statusDropdown.openStatusDropdown({ + position, + files: [file], + }); + } + + /** + * Muestra el menú contextual para múltiples archivos + * @param files Archivos a los que cambiar el estado + * @param position Posición opcional para mostrar el menú + */ + private showForMultipleFiles( + files: TFile[], + position?: { x: number; y: number }, + ): void { + const menu = new Menu(); + + // Elemento de información (deshabilitado) + menu.addItem((item) => { + item.setTitle(`Update ${files.length} files`).setDisabled(true); + return item; + }); + + // Opción para gestionar estados + menu.addItem((item) => + item + .setTitle("Manage statuses...") + .setIcon("tag") + .onClick(() => { + this.statusDropdown.openStatusDropdown({ + position, + files, + }); + }), + ); + + // Mostrar el menú en la posición adecuada + if (position) { + menu.showAtPosition(position); + } else { + // Use a centered position + menu.showAtPosition({ + x: window.innerWidth / 2, + y: window.innerHeight / 3, + }); + } + } +} diff --git a/integrations/editor/index.ts b/integrations/editor/index.ts index 176fa1c..c0393ff 100644 --- a/integrations/editor/index.ts +++ b/integrations/editor/index.ts @@ -1,2 +1,2 @@ -export { EditorIntegration } from './editor-integration'; -export { ToolbarIntegration } from './toolbar-integration'; \ No newline at end of file +export { EditorIntegration } from "./editor-integration"; +export { ToolbarIntegration } from "./toolbar-integration"; diff --git a/integrations/explorer/index.ts b/integrations/explorer/index.ts index 6720b6d..8cccce5 100644 --- a/integrations/explorer/index.ts +++ b/integrations/explorer/index.ts @@ -1 +1 @@ -export { ExplorerIntegration } from './explorer-integration'; \ No newline at end of file +export { ExplorerIntegration } from "./explorer-integration"; diff --git a/integrations/metadata-cache/index.ts b/integrations/metadata-cache/index.ts index d9eb2a9..92e849e 100644 --- a/integrations/metadata-cache/index.ts +++ b/integrations/metadata-cache/index.ts @@ -1 +1 @@ -export { MetadataIntegration } from './metadata-integration'; \ No newline at end of file +export { MetadataIntegration } from "./metadata-integration"; diff --git a/integrations/settings/settings-tab.ts b/integrations/settings/settings-tab.ts index f59f470..5fc0042 100644 --- a/integrations/settings/settings-tab.ts +++ b/integrations/settings/settings-tab.ts @@ -1,23 +1,27 @@ -import { App, PluginSettingTab } from 'obsidian'; -import NoteStatus from 'main'; -import { StatusService } from 'services/status-service'; -import { NoteStatusSettingsController } from './settings-controller'; +import { App, PluginSettingTab } from "obsidian"; +import NoteStatus from "main"; +import { StatusService } from "services/status-service"; +import { NoteStatusSettingsController } from "./settings-controller"; /** * Settings tab for the Note Status plugin - delegates to controller */ export class NoteStatusSettingTab extends PluginSettingTab { - private controller: NoteStatusSettingsController; + private controller: NoteStatusSettingsController; - constructor(app: App, plugin: NoteStatus, statusService: StatusService) { - super(app, plugin); - this.controller = new NoteStatusSettingsController(app, plugin, statusService); - } + constructor(app: App, plugin: NoteStatus, statusService: StatusService) { + super(app, plugin); + this.controller = new NoteStatusSettingsController( + app, + plugin, + statusService, + ); + } - /** - * Displays the settings interface - */ - display(): void { - this.controller.display(this.containerEl); - } + /** + * Displays the settings interface + */ + display(): void { + this.controller.display(this.containerEl); + } } diff --git a/integrations/workspace/index.ts b/integrations/workspace/index.ts index 6591713..9a37a29 100644 --- a/integrations/workspace/index.ts +++ b/integrations/workspace/index.ts @@ -1 +1 @@ -export { WorkspaceIntegration } from './workspace-integration'; \ No newline at end of file +export { WorkspaceIntegration } from "./workspace-integration"; diff --git a/main.ts b/main.ts index 1f2025f..2e8ad6b 100644 --- a/main.ts +++ b/main.ts @@ -1,219 +1,267 @@ -import { Plugin, Notice } from 'obsidian'; -import { DEFAULT_SETTINGS } from './constants/defaults'; -import { NoteStatusSettings } from './models/types'; -import { StatusService } from 'services/status-service'; -import { StyleService } from 'services/style-service'; +import { Plugin, Notice } from "obsidian"; +import { DEFAULT_SETTINGS } from "./constants/defaults"; +import { NoteStatusSettings } from "./models/types"; +import { StatusService } from "services/status-service"; +import { StyleService } from "services/style-service"; // Importar integraciones -import { ExplorerIntegration } from './integrations/explorer'; -import { EditorIntegration, ToolbarIntegration } from './integrations/editor'; -import { MetadataIntegration } from './integrations/metadata-cache'; -import { WorkspaceIntegration } from './integrations/workspace'; -import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration'; -import { NoteStatusSettingTab } from 'integrations/settings/settings-tab'; -import { CommandIntegration } from 'integrations/commands/command-integration'; +import { ExplorerIntegration } from "./integrations/explorer"; +import { EditorIntegration, ToolbarIntegration } from "./integrations/editor"; +import { MetadataIntegration } from "./integrations/metadata-cache"; +import { WorkspaceIntegration } from "./integrations/workspace"; +import { FileContextMenuIntegration } from "integrations/context-menu/file-context-menu-integration"; +import { NoteStatusSettingTab } from "integrations/settings/settings-tab"; +import { CommandIntegration } from "integrations/commands/command-integration"; // Importar vistas -import { StatusPaneViewController } from './views/status-pane-view'; +import { StatusPaneViewController } from "./views/status-pane-view"; // Importar componentes UI -import { StatusBar } from 'components/status-bar'; -import { StatusDropdown } from 'components/status-dropdown'; -import { StatusContextMenu } from 'integrations/context-menu/status-context-menu'; +import { StatusBar } from "components/status-bar"; +import { StatusDropdown } from "components/status-dropdown"; +import { StatusContextMenu } from "integrations/context-menu/status-context-menu"; export default class NoteStatus extends Plugin { - settings: NoteStatusSettings; - - // Servicios - statusService: StatusService; - styleService: StyleService; - - // Componentes UI - statusBar: StatusBar; - statusDropdown: StatusDropdown - - // Integraciones - explorerIntegration: ExplorerIntegration; - fileContextMenuIntegration: FileContextMenuIntegration; - editorIntegration: EditorIntegration; - toolbarIntegration: ToolbarIntegration; - metadataIntegration: MetadataIntegration; - workspaceIntegration: WorkspaceIntegration; - commandIntegration: CommandIntegration; + settings: NoteStatusSettings; - statusPane: StatusPaneViewController; + // Servicios + statusService: StatusService; + styleService: StyleService; - private boundHandleStatusChanged: (event: CustomEvent) => void; + // Componentes UI + statusBar: StatusBar; + statusDropdown: StatusDropdown; - async onload() { - try { - await this.loadSettings(); - this.initializeServices(); - this.registerViews(); - this.initializeUI(); - this.initializeIntegrations(); - this.setupCustomEvents(); - } catch (error) { - console.error('Error loading Note Status plugin:', error); - new Notice('Error loading Note Status plugin. Check console for details.'); - } - } + // Integraciones + explorerIntegration: ExplorerIntegration; + fileContextMenuIntegration: FileContextMenuIntegration; + editorIntegration: EditorIntegration; + toolbarIntegration: ToolbarIntegration; + metadataIntegration: MetadataIntegration; + workspaceIntegration: WorkspaceIntegration; + commandIntegration: CommandIntegration; - private async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } + statusPane: StatusPaneViewController; - private initializeServices() { - this.statusService = new StatusService(this.app, this.settings); - this.styleService = new StyleService(this.settings); - } + private boundHandleStatusChanged: (event: CustomEvent) => void; - private registerViews() { - // Register status pane view - this.registerView('status-pane', (leaf) => { - this.statusPane = new StatusPaneViewController(leaf, this); - return this.statusPane; - }); - - // Add ribbon icon - this.addRibbonIcon('tag', 'Open status pane', () => { - this.openStatusPane(); - }); - - // Añadir pestaña de configuración - this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService)); - } - - private initializeIntegrations() { - this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService); - this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService, this.statusDropdown); - const statusContextMenu = new StatusContextMenu(this.app, this.settings, this.statusService, this.statusDropdown, this.explorerIntegration); - this.fileContextMenuIntegration = new FileContextMenuIntegration(this.app, this.settings, this.statusService, this.explorerIntegration, statusContextMenu); - - this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService, this.statusDropdown); - this.commandIntegration = new CommandIntegration( - this.app, - this, - this.settings, - this.statusService, - this.statusDropdown - ); - this.metadataIntegration = new MetadataIntegration( - this.app, - this.settings, - this.statusService, - this.explorerIntegration - ); - - this.workspaceIntegration = new WorkspaceIntegration( - this.app, - this.settings, - this.statusService, - this.toolbarIntegration - ); - - // Registrar eventos en cada integración - this.fileContextMenuIntegration.registerFileContextMenuEvents(); - this.editorIntegration.registerEditorMenus(); - this.commandIntegration.registerCommands(); - this.metadataIntegration.registerMetadataEvents(); - this.workspaceIntegration.registerWorkspaceEvents(); - } - - private setupCustomEvents() { - this.boundHandleStatusChanged = this.handleStatusChanged.bind(this); - window.addEventListener('note-status:status-changed', this.boundHandleStatusChanged); - } - - private initializeUI() { - this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService); - - // Inicializar barra de estado - this.statusBar = new StatusBar( - this.addStatusBarItem(), - this.settings, - this.statusService - ); - - // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) - if (this.settings.showStatusIconsInExplorer) { - setTimeout(() => { - this.explorerIntegration.updateAllFileExplorerIcons(); - }, 2000); - } - } - - private handleStatusChanged(event: CustomEvent) { - const { statuses, file } = event.detail; - - // Actualizar barra de estado - this.statusBar.update(statuses); - // Actualizar toolbar - const activeFile = this.app.workspace.getActiveFile(); - if (activeFile?.path === file) { - this.toolbarIntegration.updateStatusDisplay(statuses); - } - // Actualizar dropdown - this.statusDropdown.update(statuses); - // Actualizar status pane si está abierto - const statusPaneLeaf = this.app.workspace.getLeavesOfType('status-pane')[0]; - if (statusPaneLeaf?.view && statusPaneLeaf.view instanceof StatusPaneViewController) { - (statusPaneLeaf.view as StatusPaneViewController).update(); + async onload() { + try { + await this.loadSettings(); + this.initializeServices(); + this.registerViews(); + this.initializeUI(); + this.initializeIntegrations(); + this.setupCustomEvents(); + } catch (error) { + console.error("Error loading Note Status plugin:", error); + new Notice( + "Error loading Note Status plugin. Check console for details.", + ); + } } - // Actualizar explorador si es necesario - if (this.settings.showStatusIconsInExplorer && file) { - const fileObj = this.app.vault.getFileByPath(file); - if (fileObj) { - this.explorerIntegration.updateFileExplorerIcons(fileObj); - } - } - } - private async openStatusPane() { - await StatusPaneViewController.open(this.app); - } + private async loadSettings() { + this.settings = Object.assign( + {}, + DEFAULT_SETTINGS, + await this.loadData(), + ); + } - async saveSettings() { - await this.saveData(this.settings); - - // Actualizar servicios - this.statusService.updateSettings(this.settings); - this.styleService.updateSettings(this.settings); - - // Actualizar integraciones - this.explorerIntegration.updateSettings(this.settings); - this.fileContextMenuIntegration.updateSettings(this.settings); - this.editorIntegration.updateSettings(this.settings); - this.metadataIntegration?.updateSettings(this.settings); - this.commandIntegration?.updateSettings(this.settings); - this.toolbarIntegration.updateSettings(this.settings); - this.workspaceIntegration.updateSettings(this.settings); - this.statusPane?.updateSettings(this.settings); - // Actualizar componentes UI - this.statusBar.updateSettings(this.settings); - this.statusDropdown.updateSettings(this.settings) - } + private initializeServices() { + this.statusService = new StatusService(this.app, this.settings); + this.styleService = new StyleService(this.settings); + } -onunload() { - // Clean up event listeners - if (this.boundHandleStatusChanged) { - window.removeEventListener('note-status:status-changed', this.boundHandleStatusChanged); - } - - // Clean up integrations - this.explorerIntegration?.unload(); - this.toolbarIntegration?.unload(); - this.fileContextMenuIntegration?.unload(); - this.workspaceIntegration?.unload(); - this.metadataIntegration?.unload(); - this.editorIntegration?.unload(); - this.commandIntegration?.unload(); - - // Clean up services - this.styleService?.unload(); - - // Clean up UI components - this.statusBar?.unload(); - this.statusDropdown?.unload(); -} + private registerViews() { + // Register status pane view + this.registerView("status-pane", (leaf) => { + this.statusPane = new StatusPaneViewController(leaf, this); + return this.statusPane; + }); + + // Add ribbon icon + this.addRibbonIcon("tag", "Open status pane", () => { + this.openStatusPane(); + }); + + // Añadir pestaña de configuración + this.addSettingTab( + new NoteStatusSettingTab(this.app, this, this.statusService), + ); + } + + private initializeIntegrations() { + this.explorerIntegration = new ExplorerIntegration( + this.app, + this.settings, + this.statusService, + ); + this.toolbarIntegration = new ToolbarIntegration( + this.app, + this.settings, + this.statusService, + this.statusDropdown, + ); + const statusContextMenu = new StatusContextMenu( + this.app, + this.settings, + this.statusService, + this.statusDropdown, + this.explorerIntegration, + ); + this.fileContextMenuIntegration = new FileContextMenuIntegration( + this.app, + this.settings, + this.statusService, + this.explorerIntegration, + statusContextMenu, + ); + + this.editorIntegration = new EditorIntegration( + this.app, + this.settings, + this.statusService, + this.statusDropdown, + ); + this.commandIntegration = new CommandIntegration( + this.app, + this, + this.settings, + this.statusService, + this.statusDropdown, + ); + this.metadataIntegration = new MetadataIntegration( + this.app, + this.settings, + this.statusService, + this.explorerIntegration, + ); + + this.workspaceIntegration = new WorkspaceIntegration( + this.app, + this.settings, + this.statusService, + this.toolbarIntegration, + ); + + // Registrar eventos en cada integración + this.fileContextMenuIntegration.registerFileContextMenuEvents(); + this.editorIntegration.registerEditorMenus(); + this.commandIntegration.registerCommands(); + this.metadataIntegration.registerMetadataEvents(); + this.workspaceIntegration.registerWorkspaceEvents(); + } + + private setupCustomEvents() { + this.boundHandleStatusChanged = this.handleStatusChanged.bind(this); + window.addEventListener( + "note-status:status-changed", + this.boundHandleStatusChanged, + ); + } + + private initializeUI() { + this.statusDropdown = new StatusDropdown( + this.app, + this.settings, + this.statusService, + ); + + // Inicializar barra de estado + this.statusBar = new StatusBar( + this.addStatusBarItem(), + this.settings, + this.statusService, + ); + + // Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio) + if (this.settings.showStatusIconsInExplorer) { + setTimeout(() => { + this.explorerIntegration.updateAllFileExplorerIcons(); + }, 2000); + } + } + + private handleStatusChanged(event: CustomEvent) { + const { statuses, file } = event.detail; + + // Actualizar barra de estado + this.statusBar.update(statuses); + // Actualizar toolbar + const activeFile = this.app.workspace.getActiveFile(); + if (activeFile?.path === file) { + this.toolbarIntegration.updateStatusDisplay(statuses); + } + // Actualizar dropdown + this.statusDropdown.update(statuses); + // Actualizar status pane si está abierto + const statusPaneLeaf = + this.app.workspace.getLeavesOfType("status-pane")[0]; + if ( + statusPaneLeaf?.view && + statusPaneLeaf.view instanceof StatusPaneViewController + ) { + (statusPaneLeaf.view as StatusPaneViewController).update(); + } + // Actualizar explorador si es necesario + if (this.settings.showStatusIconsInExplorer && file) { + const fileObj = this.app.vault.getFileByPath(file); + if (fileObj) { + this.explorerIntegration.updateFileExplorerIcons(fileObj); + } + } + } + + private async openStatusPane() { + await StatusPaneViewController.open(this.app); + } + + async saveSettings() { + await this.saveData(this.settings); + + // Actualizar servicios + this.statusService.updateSettings(this.settings); + this.styleService.updateSettings(this.settings); + + // Actualizar integraciones + this.explorerIntegration.updateSettings(this.settings); + this.fileContextMenuIntegration.updateSettings(this.settings); + this.editorIntegration.updateSettings(this.settings); + this.metadataIntegration?.updateSettings(this.settings); + this.commandIntegration?.updateSettings(this.settings); + this.toolbarIntegration.updateSettings(this.settings); + this.workspaceIntegration.updateSettings(this.settings); + this.statusPane?.updateSettings(this.settings); + // Actualizar componentes UI + this.statusBar.updateSettings(this.settings); + this.statusDropdown.updateSettings(this.settings); + } + + onunload() { + // Clean up event listeners + if (this.boundHandleStatusChanged) { + window.removeEventListener( + "note-status:status-changed", + this.boundHandleStatusChanged, + ); + } + + // Clean up integrations + this.explorerIntegration?.unload(); + this.toolbarIntegration?.unload(); + this.fileContextMenuIntegration?.unload(); + this.workspaceIntegration?.unload(); + this.metadataIntegration?.unload(); + this.editorIntegration?.unload(); + this.commandIntegration?.unload(); + + // Clean up services + this.styleService?.unload(); + + // Clean up UI components + this.statusBar?.unload(); + this.statusDropdown?.unload(); + } } diff --git a/scripts/README.md b/scripts/README.md index 593ff7e..ee7d9e9 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,14 +1,19 @@ ## Installation and Usage 1. Install dependencies: + ```bash npm install commander ``` + 2. Install tsx for executing TypeScript directly: + ```bash npm install -g tsx ``` + 3. Run the script: + ```bash tsx generate-test-vault.ts --count 40000 --output ./test-vault ``` @@ -23,6 +28,7 @@ - `--no-status` or `-n`: Generate notes without status tags (default: false) ## Performance Considerations + The script creates a realistic vault structure with: - Multiple folder levels @@ -31,4 +37,4 @@ The script creates a realistic vault structure with: - Varied note content length and structure - Proper frontmatter format matching your plugin's expectations -This will let you test real-world performance issues with your plugin without needing to manually create thousands of notes. \ No newline at end of file +This will let you test real-world performance issues with your plugin without needing to manually create thousands of notes. diff --git a/scripts/generate-test-vault.ts b/scripts/generate-test-vault.ts index 1f98b44..3b37d4c 100644 --- a/scripts/generate-test-vault.ts +++ b/scripts/generate-test-vault.ts @@ -1,27 +1,31 @@ #!/usr/bin/env tsx /** * Obsidian Test Vault Generator - * + * * This script generates a large number of markdown files with status tags * to test the performance of the Note Status plugin with large vaults. - * + * * Usage: * npx tsx generate-test-vault.ts --count 40000 --output ./test-vault */ -import * as fs from 'fs'; -import * as path from 'path'; -import { program } from 'commander'; +import * as fs from "fs"; +import * as path from "path"; +import { program } from "commander"; // Define command line options program - .option('-c, --count ', 'Number of notes to generate', '40000') - .option('-o, --output ', 'Output directory path', './test-vault') - .option('-t, --tag-prefix ', 'Status tag prefix', 'obsidian-note-status') - .option('-d, --depth ', 'Maximum folder depth', '5') - .option('-m, --max-per-folder ', 'Maximum files per folder', '200') - .option('-n, --no-status', 'Generate notes without status tags') - .parse(process.argv); + .option("-c, --count ", "Number of notes to generate", "40000") + .option("-o, --output ", "Output directory path", "./test-vault") + .option( + "-t, --tag-prefix ", + "Status tag prefix", + "obsidian-note-status", + ) + .option("-d, --depth ", "Maximum folder depth", "5") + .option("-m, --max-per-folder ", "Maximum files per folder", "200") + .option("-n, --no-status", "Generate notes without status tags") + .parse(process.argv); const options = program.opts(); @@ -34,205 +38,249 @@ const maxPerFolder = parseInt(options.maxPerFolder, 10); const includeStatus = options.status !== false; // true by default, false if --no-status is used if (isNaN(noteCount) || noteCount <= 0) { - console.error('Error: Note count must be a positive number'); - process.exit(1); + console.error("Error: Note count must be a positive number"); + process.exit(1); } // Define possible statuses (matching your plugin's defaults) const statuses = [ - 'active', - 'onHold', - 'completed', - 'dropped', - 'unknown', - // Add some from the colorful template - 'idea', - 'draft', - 'inProgress', - 'editing', - 'pending' + "active", + "onHold", + "completed", + "dropped", + "unknown", + // Add some from the colorful template + "idea", + "draft", + "inProgress", + "editing", + "pending", ]; // Define topics for more realistic note titles const topics = [ - 'Project', 'Meeting', 'Research', 'Idea', 'Book', 'Journal', - 'Task', 'Recipe', 'Person', 'Place', 'Event', 'Course', - 'Paper', 'Review', 'Analysis', 'Summary', 'Plan', 'Design', - 'Budget', 'Report', 'Feature', 'Bug', 'Release', 'Backlog', - 'Sprint', 'Goal', 'OKR', 'KPI', 'Metric', 'Reflection' + "Project", + "Meeting", + "Research", + "Idea", + "Book", + "Journal", + "Task", + "Recipe", + "Person", + "Place", + "Event", + "Course", + "Paper", + "Review", + "Analysis", + "Summary", + "Plan", + "Design", + "Budget", + "Report", + "Feature", + "Bug", + "Release", + "Backlog", + "Sprint", + "Goal", + "OKR", + "KPI", + "Metric", + "Reflection", ]; // Generate a random status function getRandomStatus(): string { - const multipleStatuses = Math.random() > 0.8; // 20% chance of multiple statuses - - if (multipleStatuses) { - const count = Math.floor(Math.random() * 2) + 2; // 2-3 statuses - const selectedStatuses = new Set(); - - while (selectedStatuses.size < count) { - selectedStatuses.add(statuses[Math.floor(Math.random() * statuses.length)]); - } - - return JSON.stringify(Array.from(selectedStatuses)); - } else { - return JSON.stringify([statuses[Math.floor(Math.random() * statuses.length)]]); - } + const multipleStatuses = Math.random() > 0.8; // 20% chance of multiple statuses + + if (multipleStatuses) { + const count = Math.floor(Math.random() * 2) + 2; // 2-3 statuses + const selectedStatuses = new Set(); + + while (selectedStatuses.size < count) { + selectedStatuses.add( + statuses[Math.floor(Math.random() * statuses.length)], + ); + } + + return JSON.stringify(Array.from(selectedStatuses)); + } else { + return JSON.stringify([ + statuses[Math.floor(Math.random() * statuses.length)], + ]); + } } // Generate a random title function generateTitle(): string { - const topic = topics[Math.floor(Math.random() * topics.length)]; - const qualifier = Math.random() > 0.5 ? - ['New', 'Important', 'Urgent', 'Weekly', 'Monthly', 'Daily', 'Annual'][Math.floor(Math.random() * 7)] : ''; - const number = Math.random() > 0.3 ? Math.floor(Math.random() * 1000).toString() : ''; - - return [qualifier, topic, number].filter(Boolean).join(' ').trim(); + const topic = topics[Math.floor(Math.random() * topics.length)]; + const qualifier = + Math.random() > 0.5 + ? [ + "New", + "Important", + "Urgent", + "Weekly", + "Monthly", + "Daily", + "Annual", + ][Math.floor(Math.random() * 7)] + : ""; + const number = + Math.random() > 0.3 ? Math.floor(Math.random() * 1000).toString() : ""; + + return [qualifier, topic, number].filter(Boolean).join(" ").trim(); } // Generate note content with random length function generateContent(): string { - const paragraphCount = Math.floor(Math.random() * 5) + 1; - let content = ''; - - for (let i = 0; i < paragraphCount; i++) { - const sentenceCount = Math.floor(Math.random() * 5) + 1; - let paragraph = ''; - - for (let j = 0; j < sentenceCount; j++) { - const wordCount = Math.floor(Math.random() * 15) + 5; - const sentence = Array(wordCount).fill('lorem').join(' ') + '. '; - paragraph += sentence; - } - - content += paragraph + '\n\n'; - } - - return content; + const paragraphCount = Math.floor(Math.random() * 5) + 1; + let content = ""; + + for (let i = 0; i < paragraphCount; i++) { + const sentenceCount = Math.floor(Math.random() * 5) + 1; + let paragraph = ""; + + for (let j = 0; j < sentenceCount; j++) { + const wordCount = Math.floor(Math.random() * 15) + 5; + const sentence = Array(wordCount).fill("lorem").join(" ") + ". "; + paragraph += sentence; + } + + content += paragraph + "\n\n"; + } + + return content; } // Create a note with optional frontmatter containing status function createNote(title: string, folderPath: string): void { - const safeName = title.replace(/[^a-zA-Z0-9 ]/g, '').replace(/\s+/g, ' '); - const fileName = `${safeName}.md`; - const filePath = path.join(folderPath, fileName); - - let fileContent = ''; - - if (includeStatus) { - // Create frontmatter with random status - const status = getRandomStatus(); - fileContent = `---\n${tagPrefix}: ${status}\n---\n\n`; - } - - // Create content - fileContent += `# ${title}\n\n${generateContent()}`; - - // Write file - fs.writeFileSync(filePath, fileContent); + const safeName = title.replace(/[^a-zA-Z0-9 ]/g, "").replace(/\s+/g, " "); + const fileName = `${safeName}.md`; + const filePath = path.join(folderPath, fileName); + + let fileContent = ""; + + if (includeStatus) { + // Create frontmatter with random status + const status = getRandomStatus(); + fileContent = `---\n${tagPrefix}: ${status}\n---\n\n`; + } + + // Create content + fileContent += `# ${title}\n\n${generateContent()}`; + + // Write file + fs.writeFileSync(filePath, fileContent); } // Generate a folder structure and notes function generateNotes(count: number, baseDir: string): void { - // Create base directory if it doesn't exist - if (!fs.existsSync(baseDir)) { - fs.mkdirSync(baseDir, { recursive: true }); - } - - // Track created files - let created = 0; - let foldersCreated = 0; - - // Create a function to generate notes in a folder with recursion - function generateNotesInFolder(folderPath: string, depth: number): void { - if (created >= count) return; - - // Determine how many files to create in this folder - const filesInThisFolder = Math.min( - Math.floor(Math.random() * maxPerFolder) + 1, - count - created - ); - - // Create files in this folder - for (let i = 0; i < filesInThisFolder; i++) { - if (created >= count) break; - - const title = generateTitle(); - createNote(title, folderPath); - created++; - - // Display progress - if (created % 1000 === 0 || created === count) { - console.log(`Created ${created}/${count} notes...`); - } - } - - // Possibly create subfolders if not at max depth - if (depth < maxDepth && created < count) { - // Decide how many subfolders to create - const subfoldersCount = Math.floor(Math.random() * 5) + 1; - - for (let i = 0; i < subfoldersCount; i++) { - if (created >= count) break; - - const folderName = `Folder ${++foldersCreated}`; - const subfolderPath = path.join(folderPath, folderName); - - // Create subfolder - if (!fs.existsSync(subfolderPath)) { - fs.mkdirSync(subfolderPath); - } - - // Recursively generate notes in subfolder - generateNotesInFolder(subfolderPath, depth + 1); - } - } - } - - // Start generation - generateNotesInFolder(baseDir, 0); - - console.log(`\nSuccessfully created ${created} notes in ${foldersCreated} folders.`); - console.log(`Output directory: ${path.resolve(baseDir)}`); + // Create base directory if it doesn't exist + if (!fs.existsSync(baseDir)) { + fs.mkdirSync(baseDir, { recursive: true }); + } + + // Track created files + let created = 0; + let foldersCreated = 0; + + // Create a function to generate notes in a folder with recursion + function generateNotesInFolder(folderPath: string, depth: number): void { + if (created >= count) return; + + // Determine how many files to create in this folder + const filesInThisFolder = Math.min( + Math.floor(Math.random() * maxPerFolder) + 1, + count - created, + ); + + // Create files in this folder + for (let i = 0; i < filesInThisFolder; i++) { + if (created >= count) break; + + const title = generateTitle(); + createNote(title, folderPath); + created++; + + // Display progress + if (created % 1000 === 0 || created === count) { + console.log(`Created ${created}/${count} notes...`); + } + } + + // Possibly create subfolders if not at max depth + if (depth < maxDepth && created < count) { + // Decide how many subfolders to create + const subfoldersCount = Math.floor(Math.random() * 5) + 1; + + for (let i = 0; i < subfoldersCount; i++) { + if (created >= count) break; + + const folderName = `Folder ${++foldersCreated}`; + const subfolderPath = path.join(folderPath, folderName); + + // Create subfolder + if (!fs.existsSync(subfolderPath)) { + fs.mkdirSync(subfolderPath); + } + + // Recursively generate notes in subfolder + generateNotesInFolder(subfolderPath, depth + 1); + } + } + } + + // Start generation + generateNotesInFolder(baseDir, 0); + + console.log( + `\nSuccessfully created ${created} notes in ${foldersCreated} folders.`, + ); + console.log(`Output directory: ${path.resolve(baseDir)}`); } // Create .obsidian folder with minimal config to ensure it's recognized as a vault function createObsidianConfig(baseDir: string): void { - const obsidianDir = path.join(baseDir, '.obsidian'); - if (!fs.existsSync(obsidianDir)) { - fs.mkdirSync(obsidianDir, { recursive: true }); - } - - // Create a minimal config.json - const configPath = path.join(obsidianDir, 'config.json'); - const config = { - "baseFontSize": 16, - "pluginEnabledStatus": { - "file-explorer": true, - "global-search": true, - "switcher": true, - "graph": true, - "backlink": true, - "page-preview": true, - "templates": true - } - }; - - fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); - - // Create a minimal appearance.json - const appearancePath = path.join(obsidianDir, 'appearance.json'); - const appearance = { - "baseFontSize": 16, - "theme": "obsidian" - }; - - fs.writeFileSync(appearancePath, JSON.stringify(appearance, null, 2)); + const obsidianDir = path.join(baseDir, ".obsidian"); + if (!fs.existsSync(obsidianDir)) { + fs.mkdirSync(obsidianDir, { recursive: true }); + } + + // Create a minimal config.json + const configPath = path.join(obsidianDir, "config.json"); + const config = { + baseFontSize: 16, + pluginEnabledStatus: { + "file-explorer": true, + "global-search": true, + switcher: true, + graph: true, + backlink: true, + "page-preview": true, + templates: true, + }, + }; + + fs.writeFileSync(configPath, JSON.stringify(config, null, 2)); + + // Create a minimal appearance.json + const appearancePath = path.join(obsidianDir, "appearance.json"); + const appearance = { + baseFontSize: 16, + theme: "obsidian", + }; + + fs.writeFileSync(appearancePath, JSON.stringify(appearance, null, 2)); } // Main execution -console.log(`Generating ${noteCount} notes in ${outputDir}${includeStatus ? ` with tag prefix '${tagPrefix}'` : ' without status tags'}`); -console.time('Generation completed in'); +console.log( + `Generating ${noteCount} notes in ${outputDir}${includeStatus ? ` with tag prefix '${tagPrefix}'` : " without status tags"}`, +); +console.time("Generation completed in"); // Create test vault generateNotes(noteCount, outputDir); @@ -240,10 +288,10 @@ generateNotes(noteCount, outputDir); // Create Obsidian configuration createObsidianConfig(outputDir); -console.timeEnd('Generation completed in'); -console.log('\nInstructions:'); -console.log('1. Open Obsidian'); +console.timeEnd("Generation completed in"); +console.log("\nInstructions:"); +console.log("1. Open Obsidian"); console.log('2. Select "Open folder as vault"'); console.log(`3. Navigate to: ${path.resolve(outputDir)}`); -console.log('4. Install and enable your Note Status plugin'); -console.log('5. Test the plugin performance with this large vault'); \ No newline at end of file +console.log("4. Install and enable your Note Status plugin"); +console.log("5. Test the plugin performance with this large vault"); diff --git a/services/style-service.ts b/services/style-service.ts index a34634b..896abec 100644 --- a/services/style-service.ts +++ b/services/style-service.ts @@ -1,82 +1,84 @@ -import { NoteStatusSettings } from '../models/types'; -import { PREDEFINED_TEMPLATES } from '../constants/status-templates'; +import { NoteStatusSettings } from "../models/types"; +import { PREDEFINED_TEMPLATES } from "../constants/status-templates"; /** * Handles dynamic styling for the plugin */ export class StyleService { - private dynamicStyleEl?: HTMLStyleElement; - private settings: NoteStatusSettings; + private dynamicStyleEl?: HTMLStyleElement; + private settings: NoteStatusSettings; - constructor(settings: NoteStatusSettings) { - this.settings = settings; - this.initializeDynamicStyles(); - } + constructor(settings: NoteStatusSettings) { + this.settings = settings; + this.initializeDynamicStyles(); + } - /** - * Updates the settings reference - */ - public updateSettings(settings: NoteStatusSettings): void { - this.settings = settings; - this.updateDynamicStyles(); - } + /** + * Updates the settings reference + */ + public updateSettings(settings: NoteStatusSettings): void { + this.settings = settings; + this.updateDynamicStyles(); + } - /** - * Creates the style element if it doesn't exist - */ - private initializeDynamicStyles(): void { - if (!this.dynamicStyleEl) { - this.dynamicStyleEl = document.createElement('style'); - document.head.appendChild(this.dynamicStyleEl); - } - this.updateDynamicStyles(); - } + /** + * Creates the style element if it doesn't exist + */ + private initializeDynamicStyles(): void { + if (!this.dynamicStyleEl) { + this.dynamicStyleEl = document.createElement("style"); + document.head.appendChild(this.dynamicStyleEl); + } + this.updateDynamicStyles(); + } - /** - * Updates the dynamic styles based on current settings - */ - private updateDynamicStyles(): void { - if (!this.dynamicStyleEl) { - this.initializeDynamicStyles(); - return; - } + /** + * Updates the dynamic styles based on current settings + */ + private updateDynamicStyles(): void { + if (!this.dynamicStyleEl) { + this.initializeDynamicStyles(); + return; + } - const allColors = this.getAllStatusColors(); - const cssRules = this.generateColorCssRules(allColors); - this.dynamicStyleEl.textContent = cssRules; - } - - /** - * Get all status colors including those from enabled templates - */ - private getAllStatusColors(): Record { - const colors = { ...this.settings.statusColors }; + const allColors = this.getAllStatusColors(); + const cssRules = this.generateColorCssRules(allColors); + this.dynamicStyleEl.textContent = cssRules; + } - if (this.settings.useCustomStatusesOnly) return colors; - - // Add colors from template statuses - for (const templateId of this.settings.enabledTemplates) { - const template = PREDEFINED_TEMPLATES.find(t => t.id === templateId); - if (!template) continue; - - for (const status of template.statuses) { - if (status.color && !colors[status.name]) { - colors[status.name] = status.color; - } - } - } + /** + * Get all status colors including those from enabled templates + */ + private getAllStatusColors(): Record { + const colors = { ...this.settings.statusColors }; - return colors; - } + if (this.settings.useCustomStatusesOnly) return colors; - /** - * Generate CSS rules for status colors - */ - private generateColorCssRules(colors: Record): string { - let css = ''; - - for (const [status, color] of Object.entries(colors)) { - css += ` + // Add colors from template statuses + for (const templateId of this.settings.enabledTemplates) { + const template = PREDEFINED_TEMPLATES.find( + (t) => t.id === templateId, + ); + if (!template) continue; + + for (const status of template.statuses) { + if (status.color && !colors[status.name]) { + colors[status.name] = status.color; + } + } + } + + return colors; + } + + /** + * Generate CSS rules for status colors + */ + private generateColorCssRules(colors: Record): string { + let css = ""; + + for (const [status, color] of Object.entries(colors)) { + css += ` .status-${status} { color: ${color} !important; } @@ -85,18 +87,18 @@ export class StyleService { color: ${color} !important; } `; - } - - return css; - } + } - /** - * Cleans up styles when plugin is unloaded - */ - public unload(): void { - if (this.dynamicStyleEl) { - this.dynamicStyleEl.remove(); - this.dynamicStyleEl = undefined; - } - } -} \ No newline at end of file + return css; + } + + /** + * Cleans up styles when plugin is unloaded + */ + public unload(): void { + if (this.dynamicStyleEl) { + this.dynamicStyleEl.remove(); + this.dynamicStyleEl = undefined; + } + } +} diff --git a/styles/base.css b/styles/base.css index 1be0960..096a10b 100644 --- a/styles/base.css +++ b/styles/base.css @@ -2,69 +2,93 @@ * Base styles and variables for Note Status Plugin */ :root { - --status-transition-time: 0.22s; - --status-border-radius: var(--radius-s, 4px); - --status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.15)); - --status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, 0.2)); - --status-icon-size: 16px; - - /* Animation curves */ - --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); - --ease-out: cubic-bezier(0, 0, 0.2, 1); - --ease-in: cubic-bezier(0.4, 0, 1, 1); + --status-transition-time: 0.22s; + --status-border-radius: var(--radius-s, 4px); + --status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.15)); + --status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, 0.2)); + --status-icon-size: 16px; + + /* Animation curves */ + --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1); + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --ease-in: cubic-bezier(0.4, 0, 1, 1); } /* Animations */ @keyframes note-status-scale-in { - from { opacity: 0; transform: scale(0.9); } - to { opacity: 1; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } } @keyframes note-status-slide-in-fade { - from { opacity: 0; transform: translateY(-10px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes note-status-fade-in-slide-down { - from { opacity: 0; transform: translateY(-6px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(-6px); + } + to { + opacity: 1; + transform: translateY(0); + } } @keyframes note-status-pulse { - 0% { transform: scale(1); } - 50% { transform: scale(0.98); } - 100% { transform: scale(1); } + 0% { + transform: scale(1); + } + 50% { + transform: scale(0.98); + } + 100% { + transform: scale(1); + } } /* Dark mode adjustments */ .theme-dark .note-status-popover { - box-shadow: var(--shadow-l); + box-shadow: var(--shadow-l); } .theme-dark .note-status-option-selecting { - box-shadow: 0 0 0 1px var(--interactive-accent); + box-shadow: 0 0 0 1px var(--interactive-accent); } /* High contrast improvements */ @media (forced-colors: active) { - .note-status-chip, - .note-status-option.is-selected { - border: 1px solid currentColor; - } - - .note-status-action-button:focus-visible, - .note-status-chip:focus-visible, - .note-status-option:focus-visible { - outline: 2px solid currentColor; - outline-offset: 2px; - } + .note-status-chip, + .note-status-option.is-selected { + border: 1px solid currentColor; + } + + .note-status-action-button:focus-visible, + .note-status-chip:focus-visible, + .note-status-option:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; + } } /* Print styles */ @media print { - .note-status-unified-dropdown, - .note-status-bar, - .note-status-pane { - display: none !important; - } -} \ No newline at end of file + .note-status-unified-dropdown, + .note-status-bar, + .note-status-pane { + display: none !important; + } +} diff --git a/styles/components/dropdown.css b/styles/components/dropdown.css index e81413c..c465ac8 100644 --- a/styles/components/dropdown.css +++ b/styles/components/dropdown.css @@ -2,249 +2,262 @@ * Dropdown component styles */ .note-status-popover { - position: absolute; - background: var(--background-primary); - border-radius: var(--radius-m); - box-shadow: var(--status-box-shadow); - z-index: calc(var(--layer-popover) + 10); - max-height: 300px; - display: flex; - flex-direction: column; - border: 1px solid var(--background-modifier-border); - min-width: 200px; - overflow: hidden; - width: auto !important; - opacity: 0; - transform: scale(0.95); - transition: opacity var(--status-transition-time) var(--ease-out), - transform var(--status-transition-time) var(--ease-out); - transform-origin: top left; - pointer-events: all !important; + position: absolute; + background: var(--background-primary); + border-radius: var(--radius-m); + box-shadow: var(--status-box-shadow); + z-index: calc(var(--layer-popover) + 10); + max-height: 300px; + display: flex; + flex-direction: column; + border: 1px solid var(--background-modifier-border); + min-width: 200px; + overflow: hidden; + width: auto !important; + opacity: 0; + transform: scale(0.95); + transition: + opacity var(--status-transition-time) var(--ease-out), + transform var(--status-transition-time) var(--ease-out); + transform-origin: top left; + pointer-events: all !important; } .note-status-popover.note-status-popover-animate-in { - opacity: 1; - transform: scale(1); + opacity: 1; + transform: scale(1); } .note-status-popover.note-status-popover-animate-out { - opacity: 0; - transform: scale(0.95); - pointer-events: none; + opacity: 0; + transform: scale(0.95); + pointer-events: none; } /* Search container */ .note-status-popover-search { - padding: 8px; - position: sticky; - top: 0; - background: var(--background-primary); - z-index: 3; - border-bottom: 1px solid var(--background-modifier-border); + padding: 8px; + position: sticky; + top: 0; + background: var(--background-primary); + z-index: 3; + border-bottom: 1px solid var(--background-modifier-border); } /* Status options container */ .note-status-options-container { - overflow-y: auto; - overflow-x: hidden; - max-height: 250px; - padding: var(--size-4-1, 4px); - scrollbar-width: thin; - scrollbar-color: var(--background-modifier-border) transparent; + overflow-y: auto; + overflow-x: hidden; + max-height: 250px; + padding: var(--size-4-1, 4px); + scrollbar-width: thin; + scrollbar-color: var(--background-modifier-border) transparent; } .note-status-options-container::-webkit-scrollbar { - width: 6px; + width: 6px; } .note-status-options-container::-webkit-scrollbar-thumb { - background-color: var(--background-modifier-border); - border-radius: 3px; + background-color: var(--background-modifier-border); + border-radius: 3px; } .note-status-options-container::-webkit-scrollbar-track { - background-color: transparent; + background-color: transparent; } /* Empty status options */ .note-status-empty-options { - padding: 16px 12px; - text-align: center; - color: var(--text-muted); - font-style: italic; - font-size: var(--font-smaller); + padding: 16px 12px; + text-align: center; + color: var(--text-muted); + font-style: italic; + font-size: var(--font-smaller); } /* Status option items */ .note-status-option { - display: flex; - align-items: center; - padding: 8px 12px; - margin: 2px 4px; - border-radius: var(--radius-s); - background: var(--background-primary); - color: var(--text-normal); - cursor: pointer !important; - transition: background-color 0.15s ease; + display: flex; + align-items: center; + padding: 8px 12px; + margin: 2px 4px; + border-radius: var(--radius-s); + background: var(--background-primary); + color: var(--text-normal); + cursor: pointer !important; + transition: background-color 0.15s ease; } .note-status-option:hover { - background: var(--background-modifier-hover); + background: var(--background-modifier-hover); } .note-status-option.is-selected { - background: var(--background-secondary-alt); - font-weight: var(--font-medium); + background: var(--background-secondary-alt); + font-weight: var(--font-medium); } .note-status-option-selecting { - background-color: var(--interactive-accent) !important; - color: var(--text-on-accent) !important; - animation: note-status-pulse 0.3s var(--ease-out); + background-color: var(--interactive-accent) !important; + color: var(--text-on-accent) !important; + animation: note-status-pulse 0.3s var(--ease-out); } .note-status-option-icon { - margin-right: 8px; - font-size: 1.1em; + margin-right: 8px; + font-size: 1.1em; } .note-status-option-text { - flex: 1; - font-size: var(--font-smaller); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + flex: 1; + font-size: var(--font-smaller); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .note-status-option-check { - margin-left: auto; - color: var(--text-accent); + margin-left: auto; + color: var(--text-accent); } /* Unified dropdown */ .note-status-unified-dropdown { - width: 280px; - max-width: 90vw; - border-radius: var(--radius-m); - overflow: hidden; - box-shadow: var(--shadow-m); - border: 1px solid var(--background-modifier-border); - background: var(--background-primary); - z-index: 9999 !important; - pointer-events: all !important; + width: 280px; + max-width: 90vw; + border-radius: var(--radius-m); + overflow: hidden; + box-shadow: var(--shadow-m); + border: 1px solid var(--background-modifier-border); + background: var(--background-primary); + z-index: 9999 !important; + pointer-events: all !important; } /* Animation for unified dropdown */ .note-status-popover-animate-in { - animation: note-status-dropdown-fade-in 0.22s var(--ease-out) forwards; + animation: note-status-dropdown-fade-in 0.22s var(--ease-out) forwards; } @keyframes note-status-dropdown-fade-in { - from { opacity: 0; transform: scale(0.95); } - to { opacity: 1; transform: scale(1); } + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } } @keyframes note-status-dropdown-fade-out { - from { opacity: 1; transform: scale(1); } - to { opacity: 0; transform: scale(0.95); } + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.95); + } } /* Popover header */ .note-status-popover-header { - padding: 12px; - border-bottom: 1px solid var(--background-modifier-border); - background: var(--background-secondary-alt); - display: flex; - align-items: center; - position: sticky; - top: 0; - z-index: 2; + padding: 12px; + border-bottom: 1px solid var(--background-modifier-border); + background: var(--background-secondary-alt); + display: flex; + align-items: center; + position: sticky; + top: 0; + z-index: 2; } .note-status-popover-title { - display: flex; - align-items: center; - gap: 8px; - font-weight: var(--font-semibold); - color: var(--text-normal); + display: flex; + align-items: center; + gap: 8px; + font-weight: var(--font-semibold); + color: var(--text-normal); } .note-status-popover-icon { - display: flex; - align-items: center; - justify-content: center; - color: var(--text-accent); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-accent); } .note-status-popover-chips { - display: flex; - flex-wrap: wrap; - gap: 6px; - padding: 12px; - background: var(--background-primary-alt); - max-height: 120px; - overflow-y: auto; - border-bottom: 1px solid var(--background-modifier-border); + display: flex; + flex-wrap: wrap; + gap: 6px; + padding: 12px; + background: var(--background-primary-alt); + max-height: 120px; + overflow-y: auto; + border-bottom: 1px solid var(--background-modifier-border); } /* Status chips styling */ .note-status-chip { - display: inline-flex; - align-items: center; - padding: 4px 10px; - background: var(--background-secondary); - border-radius: 16px; - box-shadow: var(--shadow-xs); - font-size: var(--font-smaller); - transition: all 0.15s var(--ease-out); - border: 1px solid var(--background-modifier-border); - max-width: 180px; - animation: note-status-scale-in 0.2s var(--ease-out); + display: inline-flex; + align-items: center; + padding: 4px 10px; + background: var(--background-secondary); + border-radius: 16px; + box-shadow: var(--shadow-xs); + font-size: var(--font-smaller); + transition: all 0.15s var(--ease-out); + border: 1px solid var(--background-modifier-border); + max-width: 180px; + animation: note-status-scale-in 0.2s var(--ease-out); } .note-status-chip.clickable { - cursor: pointer; + cursor: pointer; } .note-status-chip.clickable:hover { - background: var(--background-modifier-hover); - transform: translateY(-1px); - box-shadow: var(--status-hover-shadow); + background: var(--background-modifier-hover); + transform: translateY(-1px); + box-shadow: var(--status-hover-shadow); } .note-status-chip-removing { - transform: scale(0.8); - opacity: 0; - pointer-events: none; - transition: all 0.15s ease-out; + transform: scale(0.8); + opacity: 0; + pointer-events: none; + transition: all 0.15s ease-out; } .note-status-chip-icon { - margin-right: 6px; + margin-right: 6px; } .note-status-chip-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .note-status-chip-remove { - margin-left: 6px; - width: 16px; - height: 16px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 50%; - background: var(--background-modifier-border); - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s var(--ease-out); + margin-left: 6px; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--background-modifier-border); + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s var(--ease-out); } .note-status-chip-remove:hover { - background: var(--text-accent); - color: var(--text-on-accent); - transform: scale(1.1); -} \ No newline at end of file + background: var(--text-accent); + color: var(--text-on-accent); + transform: scale(1.1); +} diff --git a/styles/components/explorer.css b/styles/components/explorer.css index 6b008ad..4d63bae 100644 --- a/styles/components/explorer.css +++ b/styles/components/explorer.css @@ -2,57 +2,57 @@ * Explorer integration styles */ .note-status-toolbar-badge-container { - display: flex; - align-items: center; - justify-content: center; - position: relative; + display: flex; + align-items: center; + justify-content: center; + position: relative; } .note-status-toolbar-icon { - color: var(--text-normal) !important; - display: flex !important; - align-items: center !important; - justify-content: center !important; + color: var(--text-normal) !important; + display: flex !important; + align-items: center !important; + justify-content: center !important; } .note-status-count-badge { - position: absolute; - top: -5px; - right: -8px; - background-color: var(--interactive-accent); - color: var(--text-on-accent); - border-radius: 10px; - font-size: 10px; - min-width: 14px; - height: 14px; - padding: 0 3px; - display: flex; - align-items: center; - justify-content: center; - font-weight: var(--font-bold); + position: absolute; + top: -5px; + right: -8px; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border-radius: 10px; + font-size: 10px; + min-width: 14px; + height: 14px; + padding: 0 3px; + display: flex; + align-items: center; + justify-content: center; + font-weight: var(--font-bold); } /* Explorer icons container */ .note-status-icon-container { - display: inline-flex; - margin-left: var(--size-4-1, 4px); - gap: 2px; - align-items: center; - flex-shrink: 0; + display: inline-flex; + margin-left: var(--size-4-1, 4px); + gap: 2px; + align-items: center; + flex-shrink: 0; } /* Individual status icons in explorer */ .note-status-icon { - font-size: var(--font-ui-smaller); - display: inline-flex; - align-items: center; - justify-content: center; - width: var(--status-icon-size); - height: var(--status-icon-size); - position: relative; - z-index: 500; /* Higher than most elements */ + font-size: var(--font-ui-smaller); + display: inline-flex; + align-items: center; + justify-content: center; + width: var(--status-icon-size); + height: var(--status-icon-size); + position: relative; + z-index: 500; /* Higher than most elements */ } .nav-file-title .note-status-icon::after { - bottom: 150%; /* Position above in file explorer */ -} \ No newline at end of file + bottom: 150%; /* Position above in file explorer */ +} diff --git a/styles/components/settings.css b/styles/components/settings.css index 705f93a..651456e 100644 --- a/styles/components/settings.css +++ b/styles/components/settings.css @@ -3,60 +3,60 @@ */ /* Template styling */ .status-color-dot { - display: inline-block; - width: 8px; - height: 8px; - border-radius: 50%; - background-color: var(--dot-color, #ffffff); - margin-right: 4px; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + background-color: var(--dot-color, #ffffff); + margin-right: 4px; } .template-buttons { - margin-top: 15px; - display: flex; - gap: 10px; + margin-top: 15px; + display: flex; + gap: 10px; } /* Template selection styling */ .template-item { - border: 1px solid var(--background-modifier-border); - border-radius: var(--status-border-radius); - margin-bottom: 10px; - padding: 10px; - background: var(--background-primary-alt); + border: 1px solid var(--background-modifier-border); + border-radius: var(--status-border-radius); + margin-bottom: 10px; + padding: 10px; + background: var(--background-primary-alt); } .template-header { - display: flex; - align-items: center; - margin-bottom: 5px; + display: flex; + align-items: center; + margin-bottom: 5px; } .template-checkbox { - margin-right: 10px; + margin-right: 10px; } .template-name { - font-weight: bold; + font-weight: bold; } .template-description { - font-size: 0.9em; - color: var(--text-muted); - margin-bottom: 8px; + font-size: 0.9em; + color: var(--text-muted); + margin-bottom: 8px; } .template-statuses { - display: flex; - flex-wrap: wrap; - gap: 5px; + display: flex; + flex-wrap: wrap; + gap: 5px; } .template-status-chip { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: 12px; - background: var(--background-secondary); - font-size: 0.85em; -} \ No newline at end of file + display: inline-flex; + align-items: center; + padding: 2px 8px; + border-radius: 12px; + background: var(--background-secondary); + font-size: 0.85em; +} diff --git a/styles/components/status-bar.css b/styles/components/status-bar.css index faa63f1..e08c5ab 100644 --- a/styles/components/status-bar.css +++ b/styles/components/status-bar.css @@ -2,63 +2,63 @@ * Status bar styles */ .note-status-bar { - display: flex; - align-items: center; - gap: 6px; - padding: 4px 8px; - border-radius: var(--radius-s); - transition: all 0.2s var(--ease-out); - cursor: pointer; - height: 22px; + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px; + border-radius: var(--radius-s); + transition: all 0.2s var(--ease-out); + cursor: pointer; + height: 22px; } .note-status-bar:hover { - background-color: var(--background-modifier-hover); + background-color: var(--background-modifier-hover); } .note-status-badges { - display: flex; - align-items: center; - gap: 4px; + display: flex; + align-items: center; + gap: 4px; } .note-status-badge { - display: flex; - align-items: center; - padding: 2px 6px; - border-radius: 10px; - background: var(--background-secondary-alt); - font-size: var(--font-ui-smaller); - max-width: 100px; - margin-right: 2px; + display: flex; + align-items: center; + padding: 2px 6px; + border-radius: 10px; + background: var(--background-secondary-alt); + font-size: var(--font-ui-smaller); + max-width: 100px; + margin-right: 2px; } .note-status-badge-icon { - margin-right: 2px; + margin-right: 2px; } .note-status-badge-text { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* Status bar positioning and visibility */ .status-bar .note-status-icon::after { - bottom: auto; - top: -30px; + bottom: auto; + top: -30px; } /* Auto-hide behavior */ .note-status-bar.hidden { - display: none; + display: none; } .note-status-bar.auto-hide { - opacity: 0.5; - transition: opacity 0.2s var(--ease-out); + opacity: 0.5; + transition: opacity 0.2s var(--ease-out); } .note-status-bar.visible { - opacity: 1; -} \ No newline at end of file + opacity: 1; +} diff --git a/styles/components/status-pane.css b/styles/components/status-pane.css index e4db2cc..5d19417 100644 --- a/styles/components/status-pane.css +++ b/styles/components/status-pane.css @@ -2,312 +2,312 @@ * Status Pane Styles */ .note-status-pane { - padding: var(--size-4-2, 8px); - background: var(--background-secondary); - color: var(--text-normal); - font-family: var(--font-interface); - overflow-y: auto; - height: 100%; - display: flex; - flex-direction: column; + padding: var(--size-4-2, 8px); + 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, 8px); - border-bottom: 1px solid var(--background-modifier-border); - margin-bottom: var(--size-4-2, 8px); - display: flex; - flex-direction: column; - gap: var(--size-4-2, 8px); + position: sticky; + top: 0; + z-index: 10; + background: var(--background-secondary); + padding-bottom: var(--size-4-2, 8px); + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: var(--size-4-2, 8px); + display: flex; + flex-direction: column; + gap: var(--size-4-2, 8px); } /* Search input styles */ .note-status-search { - width: 100%; - position: relative; + width: 100%; + position: relative; } .search-input-wrapper, .note-status-search-wrapper { - position: relative; - display: flex; - align-items: center; + position: relative; + display: flex; + align-items: center; } .search-input-icon, .note-status-search-icon { - position: absolute; - left: var(--size-4-1, 4px); - color: var(--text-muted); - display: flex; - align-items: center; - pointer-events: none; - padding: 4px; + position: absolute; + left: var(--size-4-1, 4px); + color: var(--text-muted); + display: flex; + align-items: center; + pointer-events: none; + padding: 4px; } .note-status-search-input, .note-status-popover-search-input { - width: 100%; - padding: var(--input-padding); - padding-left: calc(var(--size-4-1, 4px) * 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) var(--ease-out); + width: 100%; + padding: var(--input-padding); + padding-left: calc(var(--size-4-1, 4px) * 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) var(--ease-out); } .note-status-search-input:focus, .note-status-popover-search-input:focus { - border-color: var(--interactive-accent); - box-shadow: 0 0 0 2px var(--interactive-accent-hover); + border-color: var(--interactive-accent); + box-shadow: 0 0 0 2px var(--interactive-accent-hover); } .search-input-clear-button, .note-status-search-clear-button { - opacity: 0; - position: absolute; - right: var(--size-4-1, 4px); - cursor: pointer; - color: var(--text-muted); - display: flex; - align-items: center; - transition: opacity var(--status-transition-time) var(--ease-out); - padding: 4px; - border-radius: 50%; + opacity: 0; + position: absolute; + right: var(--size-4-1, 4px); + cursor: pointer; + color: var(--text-muted); + display: flex; + align-items: center; + transition: opacity var(--status-transition-time) var(--ease-out); + padding: 4px; + border-radius: 50%; } .search-input-clear-button.is-visible, .note-status-search-clear-button.is-visible { - opacity: 1; + opacity: 1; } .search-input-clear-button:hover, .note-status-search-clear-button:hover { - color: var(--text-normal); - background-color: var(--background-modifier-hover); + color: var(--text-normal); + background-color: var(--background-modifier-hover); } /* Actions toolbar */ .status-pane-actions-container { - display: flex; - gap: var(--size-4-2, 8px); - justify-content: flex-end; - margin-top: var(--size-4-1, 4px); + display: flex; + gap: var(--size-4-2, 8px); + justify-content: flex-end; + margin-top: var(--size-4-1, 4px); } .note-status-view-toggle, .note-status-actions-refresh { - padding: var(--size-4-1, 4px) var(--size-4-2, 8px); - 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) var(--ease-out); - display: flex; - align-items: center; - justify-content: center; + padding: var(--size-4-1, 4px) var(--size-4-2, 8px); + 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) var(--ease-out); + 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: var(--shadow-s); + background: var(--background-modifier-hover); + box-shadow: var(--shadow-s); } .note-status-view-toggle:active, .note-status-actions-refresh:active { - transform: translateY(1px); - box-shadow: none; + transform: translateY(1px); + box-shadow: none; } /* Status Groups Container */ .note-status-groups-container { - flex: 1; - overflow-y: auto; - padding-right: 2px; /* Prevents scrollbar from hugging the edge */ + flex: 1; + overflow-y: auto; + padding-right: 2px; /* Prevents scrollbar from hugging the edge */ } /* Status Group Styling */ .note-status-group { - margin-bottom: var(--size-4-3, 12px); - border-radius: var(--radius-s); - background: var(--background-primary-alt); - box-shadow: var(--shadow-xs); - overflow: hidden; - animation: note-status-fade-in-slide-down 0.3s var(--ease-out); + margin-bottom: var(--size-4-3, 12px); + border-radius: var(--radius-s); + background: var(--background-primary-alt); + box-shadow: var(--shadow-xs); + overflow: hidden; + animation: note-status-fade-in-slide-down 0.3s var(--ease-out); } .note-status-group .nav-folder-title { - cursor: pointer; - padding: var(--size-4-1, 4px) var(--size-4-2, 8px); - background: var(--background-secondary-alt); - transition: background var(--status-transition-time) var(--ease-out); + cursor: pointer; + padding: var(--size-4-1, 4px) var(--size-4-2, 8px); + background: var(--background-secondary-alt); + transition: background var(--status-transition-time) var(--ease-out); } .note-status-group .nav-folder-title:hover { - background: var(--background-modifier-hover); + background: var(--background-modifier-hover); } .note-status-group .nav-folder-title-content { - font-weight: var(--font-semibold); - display: flex; - align-items: center; - gap: var(--size-4-1, 4px); + font-weight: var(--font-semibold); + display: flex; + align-items: center; + gap: var(--size-4-1, 4px); } /* Collapse indicators */ .note-status-collapse-indicator { - margin-right: var(--size-4-2, 8px); - display: flex; - align-items: center; - color: var(--text-muted); - transition: transform var(--status-transition-time) var(--ease-in-out); - opacity: 1; + margin-right: var(--size-4-2, 8px); + display: flex; + align-items: center; + color: var(--text-muted); + transition: transform var(--status-transition-time) var(--ease-in-out); + opacity: 1; } .note-status-is-collapsed .note-status-collapse-indicator { - transform: rotate(-90deg); + transform: rotate(-90deg); } /* File items */ .note-status-group .nav-folder-children { - padding: var(--size-4-1, 4px); - background: var(--background-primary); - transition: height var(--status-transition-time) var(--ease-out); + padding: var(--size-4-1, 4px); + background: var(--background-primary); + transition: height var(--status-transition-time) var(--ease-out); } .note-status-group.nav-folder.note-status-is-collapsed .nav-folder-children { - display: none; + display: none; } .nav-file { - border-radius: var(--radius-s); - transition: background var(--status-transition-time) var(--ease-out); - margin-bottom: 2px; - position: relative; + border-radius: var(--radius-s); + transition: background var(--status-transition-time) var(--ease-out); + margin-bottom: 2px; + position: relative; } .nav-file:hover { - background: var(--background-modifier-hover); + background: var(--background-modifier-hover); } .nav-file-title { - display: flex; - align-items: center; - padding: var(--size-4-1, 4px) var(--size-4-2, 8px); - flex-wrap: nowrap; + display: flex; + align-items: center; + padding: var(--size-4-1, 4px) var(--size-4-2, 8px); + flex-wrap: nowrap; } .nav-file-icon { - color: var(--text-muted); - margin-right: var(--size-4-2, 8px); - display: flex; - align-items: center; - flex-shrink: 0; + color: var(--text-muted); + margin-right: var(--size-4-2, 8px); + display: flex; + align-items: center; + flex-shrink: 0; } .nav-file-title-content { - flex: 1; - min-width: 0; /* Allows text to shrink properly */ - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + flex: 1; + min-width: 0; /* Allows text to shrink properly */ + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* Compact view */ .note-status-compact-view .nav-file-title { - padding: 2px var(--size-4-2, 8px); - font-size: var(--font-smaller); + padding: 2px var(--size-4-2, 8px); + font-size: var(--font-smaller); } .note-status-compact-view .nav-folder-children { - padding: 0; + padding: 0; } .note-status-compact-view .nav-file { - margin-bottom: 0; - border-radius: 0; - border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: 0; + border-radius: 0; + border-bottom: 1px solid var(--background-modifier-border); } .note-status-compact-view .nav-file:last-child { - border-bottom: none; + border-bottom: none; } /* Empty message and show unassigned button */ .note-status-empty-message { - margin-bottom: 12px; - text-align: center; - color: var(--text-muted); - font-style: italic; + margin-bottom: 12px; + text-align: center; + color: var(--text-muted); + font-style: italic; } .note-status-button-container { - display: flex; - justify-content: center; - margin-top: 16px; + display: flex; + justify-content: center; + margin-top: 16px; } .note-status-show-unassigned-button { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - padding: 8px 16px; - border-radius: var(--radius-s, 4px); - border: none; - cursor: pointer; - font-size: var(--font-ui-small); - transition: all 0.2s var(--ease-out); - box-shadow: var(--shadow-s); + background-color: var(--interactive-accent); + color: var(--text-on-accent); + padding: 8px 16px; + border-radius: var(--radius-s, 4px); + border: none; + cursor: pointer; + font-size: var(--font-ui-small); + transition: all 0.2s var(--ease-out); + box-shadow: var(--shadow-s); } .note-status-show-unassigned-button:hover { - background-color: var(--interactive-accent-hover); - transform: translateY(-1px); - box-shadow: var(--shadow-m); + background-color: var(--interactive-accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-m); } .note-status-show-unassigned-button:active { - transform: translateY(0); - box-shadow: var(--shadow-xs); + transform: translateY(0); + box-shadow: var(--shadow-xs); } /* * Pagination styles */ .note-status-pagination { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px; - border-top: 1px solid var(--background-modifier-border); - margin-top: 8px; + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + border-top: 1px solid var(--background-modifier-border); + margin-top: 8px; } .note-status-pagination-button { - padding: 4px 8px; - border-radius: var(--radius-s); - background: var(--background-secondary); - border: var(--input-border-width) solid var(--background-modifier-border); - color: var(--text-normal); - cursor: pointer; - transition: all 0.15s var(--ease-out); - font-size: var(--font-smaller); + padding: 4px 8px; + border-radius: var(--radius-s); + background: var(--background-secondary); + border: var(--input-border-width) solid var(--background-modifier-border); + color: var(--text-normal); + cursor: pointer; + transition: all 0.15s var(--ease-out); + font-size: var(--font-smaller); } .note-status-pagination-button:hover { - background: var(--background-modifier-hover); - box-shadow: var(--shadow-s); + background: var(--background-modifier-hover); + box-shadow: var(--shadow-s); } .note-status-pagination-info { - font-size: var(--font-smaller); - color: var(--text-muted); -} \ No newline at end of file + font-size: var(--font-smaller); + color: var(--text-muted); +} diff --git a/styles/index.css b/styles/index.css index 48f5275..52bb9fc 100644 --- a/styles/index.css +++ b/styles/index.css @@ -5,10 +5,10 @@ * Author: Aleix Soler */ -@import 'base.css'; -@import 'utils.css'; -@import 'components/status-bar.css'; -@import 'components/status-pane.css'; -@import 'components/dropdown.css'; -@import 'components/explorer.css'; -@import 'components/settings.css'; \ No newline at end of file +@import "base.css"; +@import "utils.css"; +@import "components/status-bar.css"; +@import "components/status-pane.css"; +@import "components/dropdown.css"; +@import "components/explorer.css"; +@import "components/settings.css"; diff --git a/styles/utils.css b/styles/utils.css index 9d3496b..36d2e50 100644 --- a/styles/utils.css +++ b/styles/utils.css @@ -2,105 +2,107 @@ * Utility classes for Note Status Plugin */ .note-status-empty-indicator { - color: var(--text-muted); - font-style: italic; - padding: var(--size-4-1, 4px); - display: flex; - align-items: center; - justify-content: center; - color: var(--text-accent); - font-size: 1.2em; + color: var(--text-muted); + font-style: italic; + padding: var(--size-4-1, 4px); + display: flex; + align-items: center; + justify-content: center; + color: var(--text-accent); + font-size: 1.2em; } /* Action buttons in modal */ .note-status-action-button { - display: flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: var(--radius-s); - background: transparent; - color: var(--text-muted); - cursor: pointer; - transition: all 0.15s ease; - border: none; - padding: 0; + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: var(--radius-s); + background: transparent; + color: var(--text-muted); + cursor: pointer; + transition: all 0.15s ease; + border: none; + padding: 0; } .note-status-action-button:hover { - background-color: var(--background-modifier-hover); - color: var(--text-normal); + background-color: var(--background-modifier-hover); + color: var(--text-normal); } .note-status-action-button.note-status-button-active { - background-color: var(--interactive-accent); - color: var(--text-on-accent); - transform: scale(0.95); + background-color: var(--interactive-accent); + color: var(--text-on-accent); + transform: scale(0.95); } /* Loading indicator */ .note-status-loading { - display: flex; - justify-content: center; - align-items: center; - padding: 20px; - color: var(--text-muted); - font-style: italic; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + color: var(--text-muted); + font-style: italic; } .note-status-loading span { - position: relative; - padding-left: 24px; + position: relative; + padding-left: 24px; } .note-status-loading span:before { - content: ''; - position: absolute; - left: 0; - top: 50%; - width: 16px; - height: 16px; - margin-top: -8px; - border: 2px solid var(--background-modifier-border); - border-top-color: var(--text-accent); - border-radius: 50%; - animation: note-status-loading-spinner 0.8s linear infinite; + content: ""; + position: absolute; + left: 0; + top: 50%; + width: 16px; + height: 16px; + margin-top: -8px; + border: 2px solid var(--background-modifier-border); + border-top-color: var(--text-accent); + border-radius: 50%; + animation: note-status-loading-spinner 0.8s linear infinite; } @keyframes note-status-loading-spinner { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } /* Positioning classes for dropdowns */ .note-status-dummy-target { - position: fixed; - z-index: 1000; - width: 0; - height: 0; - left: var(--pos-x-px, 0); - top: var(--pos-y-px, 0); - pointer-events: none; + position: fixed; + z-index: 1000; + width: 0; + height: 0; + left: var(--pos-x-px, 0); + top: var(--pos-y-px, 0); + pointer-events: none; } .note-status-popover-fixed { - position: fixed; - z-index: 999; - --pos-x: 0; - --pos-y: 0; - --max-height: 300px; - left: var(--pos-x-px, 0); - top: var(--pos-y-px, 0); - max-height: var(--max-height-px, 300px); + position: fixed; + z-index: 999; + --pos-x: 0; + --pos-y: 0; + --max-height: 300px; + left: var(--pos-x-px, 0); + top: var(--pos-y-px, 0); + max-height: var(--max-height-px, 300px); } /* Position adjustment classes */ .note-status-popover-right { - left: auto !important; - right: var(--right-offset-px, 10px); + left: auto !important; + right: var(--right-offset-px, 10px); } .note-status-popover-bottom { - top: auto !important; - bottom: var(--bottom-offset-px, 10px); -} \ No newline at end of file + top: auto !important; + bottom: var(--bottom-offset-px, 10px); +} diff --git a/tsconfig.json b/tsconfig.json index c44b729..c3939c0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,24 +1,17 @@ { - "compilerOptions": { - "baseUrl": ".", - "inlineSourceMap": true, - "inlineSources": true, - "module": "ESNext", - "target": "ES6", - "allowJs": true, - "noImplicitAny": true, - "moduleResolution": "node", - "importHelpers": true, - "isolatedModules": true, - "strictNullChecks": true, - "lib": [ - "DOM", - "ES5", - "ES6", - "ES7" - ] - }, - "include": [ - "**/*.ts" - ] + "compilerOptions": { + "baseUrl": ".", + "inlineSourceMap": true, + "inlineSources": true, + "module": "ESNext", + "target": "ES6", + "allowJs": true, + "noImplicitAny": true, + "moduleResolution": "node", + "importHelpers": true, + "isolatedModules": true, + "strictNullChecks": true, + "lib": ["DOM", "ES5", "ES6", "ES7"] + }, + "include": ["**/*.ts"] } diff --git a/views/status-pane-view/index.ts b/views/status-pane-view/index.ts index 93b29c9..a93c1c7 100644 --- a/views/status-pane-view/index.ts +++ b/views/status-pane-view/index.ts @@ -1,2 +1,2 @@ -export { StatusPaneView } from './status-pane-view'; -export { StatusPaneViewController } from './status-pane-view-controller'; +export { StatusPaneView } from "./status-pane-view"; +export { StatusPaneViewController } from "./status-pane-view-controller";