refactor: auto-format

This commit is contained in:
Aleix Soler 2025-05-25 13:24:28 +02:00
parent 028aed1f2b
commit 6b3b479b77
34 changed files with 2323 additions and 2018 deletions

View file

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

View file

@ -1,4 +1,4 @@
import { StatusBarController } from './status-bar-controller';
import { StatusBarController } from "./status-bar-controller";
export { StatusBarController as StatusBar};
export default StatusBarController;
export { StatusBarController as StatusBar };
export default StatusBarController;

View file

@ -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();
}
}
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();
}
}

View file

@ -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();
}
}
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();
}
}

View file

@ -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);
}
const { clickOutsideHandler, escapeKeyHandler } = options;
document.removeEventListener("click", clickOutsideHandler);
document.removeEventListener("keydown", escapeKeyHandler);
}

View file

@ -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;
}
}
}

View file

@ -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;
}
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;
}

View file

@ -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();
}
}
// 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();
}
}

View file

@ -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;
export default DropdownManager;

View file

@ -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<void>;
export type StatusRemoveHandler = (
status: string,
targetFile?: TFile | TFile[],
) => Promise<void>;
/**
* Status selection handler function type
*/
export type StatusSelectHandler = (status: string, targetFile: TFile | TFile[]) => Promise<void>;
export type StatusSelectHandler = (
status: string,
targetFile: TFile | TFile[],
) => Promise<void>;
/**
* Common dependencies for dropdown components
*/
export interface DropdownDependencies {
app: App;
settings: NoteStatusSettings;
statusService: StatusService;
app: App;
settings: NoteStatusSettings;
statusService: StatusService;
}

View file

@ -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;
}
}
}
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;
}
}
}

View file

@ -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<string, string> = {
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
};

View file

@ -20,5 +20,5 @@ export const ICONS = {
refresh: `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38"/></svg>`,
collapseDown: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>`,
collapseRight: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>`,
file: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`
file: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path><polyline points="14 2 14 8 20 8"></polyline></svg>`,
};

View file

@ -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'];
export const DEFAULT_ENABLED_TEMPLATES = ["colorful"];

View file

@ -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
});
}
}
}
/**
* 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,
});
}
}
}

View file

@ -1,2 +1,2 @@
export { EditorIntegration } from './editor-integration';
export { ToolbarIntegration } from './toolbar-integration';
export { EditorIntegration } from "./editor-integration";
export { ToolbarIntegration } from "./toolbar-integration";

View file

@ -1 +1 @@
export { ExplorerIntegration } from './explorer-integration';
export { ExplorerIntegration } from "./explorer-integration";

View file

@ -1 +1 @@
export { MetadataIntegration } from './metadata-integration';
export { MetadataIntegration } from "./metadata-integration";

View file

@ -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);
}
}

View file

@ -1 +1 @@
export { WorkspaceIntegration } from './workspace-integration';
export { WorkspaceIntegration } from "./workspace-integration";

448
main.ts
View file

@ -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();
}
}

View file

@ -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.
This will let you test real-world performance issues with your plugin without needing to manually create thousands of notes.

View file

@ -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>', 'Number of notes to generate', '40000')
.option('-o, --output <path>', 'Output directory path', './test-vault')
.option('-t, --tag-prefix <string>', 'Status tag prefix', 'obsidian-note-status')
.option('-d, --depth <number>', 'Maximum folder depth', '5')
.option('-m, --max-per-folder <number>', 'Maximum files per folder', '200')
.option('-n, --no-status', 'Generate notes without status tags')
.parse(process.argv);
.option("-c, --count <number>", "Number of notes to generate", "40000")
.option("-o, --output <path>", "Output directory path", "./test-vault")
.option(
"-t, --tag-prefix <string>",
"Status tag prefix",
"obsidian-note-status",
)
.option("-d, --depth <number>", "Maximum folder depth", "5")
.option("-m, --max-per-folder <number>", "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<string>();
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<string>();
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');
console.log("4. Install and enable your Note Status plugin");
console.log("5. Test the plugin performance with this large vault");

View file

@ -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<string, string> {
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<string, string> {
const colors = { ...this.settings.statusColors };
return colors;
}
if (this.settings.useCustomStatusesOnly) return colors;
/**
* Generate CSS rules for status colors
*/
private generateColorCssRules(colors: Record<string, string>): 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, string>): 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;
}
}
}
return css;
}
/**
* Cleans up styles when plugin is unloaded
*/
public unload(): void {
if (this.dynamicStyleEl) {
this.dynamicStyleEl.remove();
this.dynamicStyleEl = undefined;
}
}
}

View file

@ -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;
}
}
.note-status-unified-dropdown,
.note-status-bar,
.note-status-pane {
display: none !important;
}
}

View file

@ -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);
}
background: var(--text-accent);
color: var(--text-on-accent);
transform: scale(1.1);
}

View file

@ -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 */
}
bottom: 150%; /* Position above in file explorer */
}

View file

@ -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;
}
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 12px;
background: var(--background-secondary);
font-size: 0.85em;
}

View file

@ -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;
}
opacity: 1;
}

View file

@ -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);
}
font-size: var(--font-smaller);
color: var(--text-muted);
}

View file

@ -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';
@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";

View file

@ -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);
}
top: auto !important;
bottom: var(--bottom-offset-px, 10px);
}

View file

@ -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"]
}

View file

@ -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";