mirror of
https://github.com/devonthesofa/obsidian-note-status.git
synced 2026-07-22 05:45:04 +00:00
refactor: auto-format
This commit is contained in:
parent
028aed1f2b
commit
6b3b479b77
34 changed files with 2323 additions and 2018 deletions
56
README.md
56
README.md
|
|
@ -7,6 +7,7 @@ Enhance your Obsidian workflow with a powerful status tracking system for your n
|
|||

|
||||
|
||||
## 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:
|
||||
|
||||

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

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

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

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

|
||||
|
|
@ -103,6 +116,7 @@ When enabled in settings, you can assign multiple statuses to a single note:
|
|||

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

|
||||
|
||||
#### 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 
|
||||
|
||||
## Configuration
|
||||
|
||||
### Status Management
|
||||
|
||||
Access plugin settings via Settings → Note Status
|
||||
|
||||
#### Status Templates
|
||||
|
|
@ -150,7 +168,9 @@ Create your own statuses by:
|
|||

|
||||
|
||||
### 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { EditorIntegration } from './editor-integration';
|
||||
export { ToolbarIntegration } from './toolbar-integration';
|
||||
export { EditorIntegration } from "./editor-integration";
|
||||
export { ToolbarIntegration } from "./toolbar-integration";
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { ExplorerIntegration } from './explorer-integration';
|
||||
export { ExplorerIntegration } from "./explorer-integration";
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { MetadataIntegration } from './metadata-integration';
|
||||
export { MetadataIntegration } from "./metadata-integration";
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { WorkspaceIntegration } from './workspace-integration';
|
||||
export { WorkspaceIntegration } from "./workspace-integration";
|
||||
|
|
|
|||
448
main.ts
448
main.ts
|
|
@ -1,219 +1,267 @@
|
|||
import { Plugin, Notice } from 'obsidian';
|
||||
import { DEFAULT_SETTINGS } from './constants/defaults';
|
||||
import { NoteStatusSettings } from './models/types';
|
||||
import { StatusService } from 'services/status-service';
|
||||
import { StyleService } from 'services/style-service';
|
||||
import { Plugin, Notice } from "obsidian";
|
||||
import { DEFAULT_SETTINGS } from "./constants/defaults";
|
||||
import { NoteStatusSettings } from "./models/types";
|
||||
import { StatusService } from "services/status-service";
|
||||
import { StyleService } from "services/style-service";
|
||||
|
||||
// Importar integraciones
|
||||
import { ExplorerIntegration } from './integrations/explorer';
|
||||
import { EditorIntegration, ToolbarIntegration } from './integrations/editor';
|
||||
import { MetadataIntegration } from './integrations/metadata-cache';
|
||||
import { WorkspaceIntegration } from './integrations/workspace';
|
||||
import { FileContextMenuIntegration } from 'integrations/context-menu/file-context-menu-integration';
|
||||
import { NoteStatusSettingTab } from 'integrations/settings/settings-tab';
|
||||
import { CommandIntegration } from 'integrations/commands/command-integration';
|
||||
import { ExplorerIntegration } from "./integrations/explorer";
|
||||
import { EditorIntegration, ToolbarIntegration } from "./integrations/editor";
|
||||
import { MetadataIntegration } from "./integrations/metadata-cache";
|
||||
import { WorkspaceIntegration } from "./integrations/workspace";
|
||||
import { FileContextMenuIntegration } from "integrations/context-menu/file-context-menu-integration";
|
||||
import { NoteStatusSettingTab } from "integrations/settings/settings-tab";
|
||||
import { CommandIntegration } from "integrations/commands/command-integration";
|
||||
|
||||
// Importar vistas
|
||||
import { StatusPaneViewController } from './views/status-pane-view';
|
||||
import { StatusPaneViewController } from "./views/status-pane-view";
|
||||
|
||||
// Importar componentes UI
|
||||
import { StatusBar } from 'components/status-bar';
|
||||
import { StatusDropdown } from 'components/status-dropdown';
|
||||
import { StatusContextMenu } from 'integrations/context-menu/status-context-menu';
|
||||
import { StatusBar } from "components/status-bar";
|
||||
import { StatusDropdown } from "components/status-dropdown";
|
||||
import { StatusContextMenu } from "integrations/context-menu/status-context-menu";
|
||||
|
||||
export default class NoteStatus extends Plugin {
|
||||
settings: NoteStatusSettings;
|
||||
|
||||
// Servicios
|
||||
statusService: StatusService;
|
||||
styleService: StyleService;
|
||||
|
||||
// Componentes UI
|
||||
statusBar: StatusBar;
|
||||
statusDropdown: StatusDropdown
|
||||
|
||||
// Integraciones
|
||||
explorerIntegration: ExplorerIntegration;
|
||||
fileContextMenuIntegration: FileContextMenuIntegration;
|
||||
editorIntegration: EditorIntegration;
|
||||
toolbarIntegration: ToolbarIntegration;
|
||||
metadataIntegration: MetadataIntegration;
|
||||
workspaceIntegration: WorkspaceIntegration;
|
||||
commandIntegration: CommandIntegration;
|
||||
settings: NoteStatusSettings;
|
||||
|
||||
statusPane: StatusPaneViewController;
|
||||
// Servicios
|
||||
statusService: StatusService;
|
||||
styleService: StyleService;
|
||||
|
||||
private boundHandleStatusChanged: (event: CustomEvent) => void;
|
||||
// Componentes UI
|
||||
statusBar: StatusBar;
|
||||
statusDropdown: StatusDropdown;
|
||||
|
||||
async onload() {
|
||||
try {
|
||||
await this.loadSettings();
|
||||
this.initializeServices();
|
||||
this.registerViews();
|
||||
this.initializeUI();
|
||||
this.initializeIntegrations();
|
||||
this.setupCustomEvents();
|
||||
} catch (error) {
|
||||
console.error('Error loading Note Status plugin:', error);
|
||||
new Notice('Error loading Note Status plugin. Check console for details.');
|
||||
}
|
||||
}
|
||||
// Integraciones
|
||||
explorerIntegration: ExplorerIntegration;
|
||||
fileContextMenuIntegration: FileContextMenuIntegration;
|
||||
editorIntegration: EditorIntegration;
|
||||
toolbarIntegration: ToolbarIntegration;
|
||||
metadataIntegration: MetadataIntegration;
|
||||
workspaceIntegration: WorkspaceIntegration;
|
||||
commandIntegration: CommandIntegration;
|
||||
|
||||
private async loadSettings() {
|
||||
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||
}
|
||||
statusPane: StatusPaneViewController;
|
||||
|
||||
private initializeServices() {
|
||||
this.statusService = new StatusService(this.app, this.settings);
|
||||
this.styleService = new StyleService(this.settings);
|
||||
}
|
||||
private boundHandleStatusChanged: (event: CustomEvent) => void;
|
||||
|
||||
private registerViews() {
|
||||
// Register status pane view
|
||||
this.registerView('status-pane', (leaf) => {
|
||||
this.statusPane = new StatusPaneViewController(leaf, this);
|
||||
return this.statusPane;
|
||||
});
|
||||
|
||||
// Add ribbon icon
|
||||
this.addRibbonIcon('tag', 'Open status pane', () => {
|
||||
this.openStatusPane();
|
||||
});
|
||||
|
||||
// Añadir pestaña de configuración
|
||||
this.addSettingTab(new NoteStatusSettingTab(this.app, this, this.statusService));
|
||||
}
|
||||
|
||||
private initializeIntegrations() {
|
||||
this.explorerIntegration = new ExplorerIntegration(this.app, this.settings, this.statusService);
|
||||
this.toolbarIntegration = new ToolbarIntegration(this.app, this.settings, this.statusService, this.statusDropdown);
|
||||
const statusContextMenu = new StatusContextMenu(this.app, this.settings, this.statusService, this.statusDropdown, this.explorerIntegration);
|
||||
this.fileContextMenuIntegration = new FileContextMenuIntegration(this.app, this.settings, this.statusService, this.explorerIntegration, statusContextMenu);
|
||||
|
||||
this.editorIntegration = new EditorIntegration(this.app, this.settings, this.statusService, this.statusDropdown);
|
||||
this.commandIntegration = new CommandIntegration(
|
||||
this.app,
|
||||
this,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.statusDropdown
|
||||
);
|
||||
this.metadataIntegration = new MetadataIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.explorerIntegration
|
||||
);
|
||||
|
||||
this.workspaceIntegration = new WorkspaceIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.toolbarIntegration
|
||||
);
|
||||
|
||||
// Registrar eventos en cada integración
|
||||
this.fileContextMenuIntegration.registerFileContextMenuEvents();
|
||||
this.editorIntegration.registerEditorMenus();
|
||||
this.commandIntegration.registerCommands();
|
||||
this.metadataIntegration.registerMetadataEvents();
|
||||
this.workspaceIntegration.registerWorkspaceEvents();
|
||||
}
|
||||
|
||||
private setupCustomEvents() {
|
||||
this.boundHandleStatusChanged = this.handleStatusChanged.bind(this);
|
||||
window.addEventListener('note-status:status-changed', this.boundHandleStatusChanged);
|
||||
}
|
||||
|
||||
private initializeUI() {
|
||||
this.statusDropdown = new StatusDropdown(this.app, this.settings, this.statusService);
|
||||
|
||||
// Inicializar barra de estado
|
||||
this.statusBar = new StatusBar(
|
||||
this.addStatusBarItem(),
|
||||
this.settings,
|
||||
this.statusService
|
||||
);
|
||||
|
||||
// Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio)
|
||||
if (this.settings.showStatusIconsInExplorer) {
|
||||
setTimeout(() => {
|
||||
this.explorerIntegration.updateAllFileExplorerIcons();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
private handleStatusChanged(event: CustomEvent) {
|
||||
const { statuses, file } = event.detail;
|
||||
|
||||
// Actualizar barra de estado
|
||||
this.statusBar.update(statuses);
|
||||
// Actualizar toolbar
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile?.path === file) {
|
||||
this.toolbarIntegration.updateStatusDisplay(statuses);
|
||||
}
|
||||
// Actualizar dropdown
|
||||
this.statusDropdown.update(statuses);
|
||||
// Actualizar status pane si está abierto
|
||||
const statusPaneLeaf = this.app.workspace.getLeavesOfType('status-pane')[0];
|
||||
if (statusPaneLeaf?.view && statusPaneLeaf.view instanceof StatusPaneViewController) {
|
||||
(statusPaneLeaf.view as StatusPaneViewController).update();
|
||||
async onload() {
|
||||
try {
|
||||
await this.loadSettings();
|
||||
this.initializeServices();
|
||||
this.registerViews();
|
||||
this.initializeUI();
|
||||
this.initializeIntegrations();
|
||||
this.setupCustomEvents();
|
||||
} catch (error) {
|
||||
console.error("Error loading Note Status plugin:", error);
|
||||
new Notice(
|
||||
"Error loading Note Status plugin. Check console for details.",
|
||||
);
|
||||
}
|
||||
}
|
||||
// Actualizar explorador si es necesario
|
||||
if (this.settings.showStatusIconsInExplorer && file) {
|
||||
const fileObj = this.app.vault.getFileByPath(file);
|
||||
if (fileObj) {
|
||||
this.explorerIntegration.updateFileExplorerIcons(fileObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async openStatusPane() {
|
||||
await StatusPaneViewController.open(this.app);
|
||||
}
|
||||
private async loadSettings() {
|
||||
this.settings = Object.assign(
|
||||
{},
|
||||
DEFAULT_SETTINGS,
|
||||
await this.loadData(),
|
||||
);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Actualizar servicios
|
||||
this.statusService.updateSettings(this.settings);
|
||||
this.styleService.updateSettings(this.settings);
|
||||
|
||||
// Actualizar integraciones
|
||||
this.explorerIntegration.updateSettings(this.settings);
|
||||
this.fileContextMenuIntegration.updateSettings(this.settings);
|
||||
this.editorIntegration.updateSettings(this.settings);
|
||||
this.metadataIntegration?.updateSettings(this.settings);
|
||||
this.commandIntegration?.updateSettings(this.settings);
|
||||
this.toolbarIntegration.updateSettings(this.settings);
|
||||
this.workspaceIntegration.updateSettings(this.settings);
|
||||
this.statusPane?.updateSettings(this.settings);
|
||||
// Actualizar componentes UI
|
||||
this.statusBar.updateSettings(this.settings);
|
||||
this.statusDropdown.updateSettings(this.settings)
|
||||
}
|
||||
private initializeServices() {
|
||||
this.statusService = new StatusService(this.app, this.settings);
|
||||
this.styleService = new StyleService(this.settings);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Clean up event listeners
|
||||
if (this.boundHandleStatusChanged) {
|
||||
window.removeEventListener('note-status:status-changed', this.boundHandleStatusChanged);
|
||||
}
|
||||
|
||||
// Clean up integrations
|
||||
this.explorerIntegration?.unload();
|
||||
this.toolbarIntegration?.unload();
|
||||
this.fileContextMenuIntegration?.unload();
|
||||
this.workspaceIntegration?.unload();
|
||||
this.metadataIntegration?.unload();
|
||||
this.editorIntegration?.unload();
|
||||
this.commandIntegration?.unload();
|
||||
|
||||
// Clean up services
|
||||
this.styleService?.unload();
|
||||
|
||||
// Clean up UI components
|
||||
this.statusBar?.unload();
|
||||
this.statusDropdown?.unload();
|
||||
}
|
||||
private registerViews() {
|
||||
// Register status pane view
|
||||
this.registerView("status-pane", (leaf) => {
|
||||
this.statusPane = new StatusPaneViewController(leaf, this);
|
||||
return this.statusPane;
|
||||
});
|
||||
|
||||
// Add ribbon icon
|
||||
this.addRibbonIcon("tag", "Open status pane", () => {
|
||||
this.openStatusPane();
|
||||
});
|
||||
|
||||
// Añadir pestaña de configuración
|
||||
this.addSettingTab(
|
||||
new NoteStatusSettingTab(this.app, this, this.statusService),
|
||||
);
|
||||
}
|
||||
|
||||
private initializeIntegrations() {
|
||||
this.explorerIntegration = new ExplorerIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
);
|
||||
this.toolbarIntegration = new ToolbarIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.statusDropdown,
|
||||
);
|
||||
const statusContextMenu = new StatusContextMenu(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.statusDropdown,
|
||||
this.explorerIntegration,
|
||||
);
|
||||
this.fileContextMenuIntegration = new FileContextMenuIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.explorerIntegration,
|
||||
statusContextMenu,
|
||||
);
|
||||
|
||||
this.editorIntegration = new EditorIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.statusDropdown,
|
||||
);
|
||||
this.commandIntegration = new CommandIntegration(
|
||||
this.app,
|
||||
this,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.statusDropdown,
|
||||
);
|
||||
this.metadataIntegration = new MetadataIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.explorerIntegration,
|
||||
);
|
||||
|
||||
this.workspaceIntegration = new WorkspaceIntegration(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
this.toolbarIntegration,
|
||||
);
|
||||
|
||||
// Registrar eventos en cada integración
|
||||
this.fileContextMenuIntegration.registerFileContextMenuEvents();
|
||||
this.editorIntegration.registerEditorMenus();
|
||||
this.commandIntegration.registerCommands();
|
||||
this.metadataIntegration.registerMetadataEvents();
|
||||
this.workspaceIntegration.registerWorkspaceEvents();
|
||||
}
|
||||
|
||||
private setupCustomEvents() {
|
||||
this.boundHandleStatusChanged = this.handleStatusChanged.bind(this);
|
||||
window.addEventListener(
|
||||
"note-status:status-changed",
|
||||
this.boundHandleStatusChanged,
|
||||
);
|
||||
}
|
||||
|
||||
private initializeUI() {
|
||||
this.statusDropdown = new StatusDropdown(
|
||||
this.app,
|
||||
this.settings,
|
||||
this.statusService,
|
||||
);
|
||||
|
||||
// Inicializar barra de estado
|
||||
this.statusBar = new StatusBar(
|
||||
this.addStatusBarItem(),
|
||||
this.settings,
|
||||
this.statusService,
|
||||
);
|
||||
|
||||
// Inicializar iconos del explorador (con retraso para evitar ralentizar el inicio)
|
||||
if (this.settings.showStatusIconsInExplorer) {
|
||||
setTimeout(() => {
|
||||
this.explorerIntegration.updateAllFileExplorerIcons();
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
private handleStatusChanged(event: CustomEvent) {
|
||||
const { statuses, file } = event.detail;
|
||||
|
||||
// Actualizar barra de estado
|
||||
this.statusBar.update(statuses);
|
||||
// Actualizar toolbar
|
||||
const activeFile = this.app.workspace.getActiveFile();
|
||||
if (activeFile?.path === file) {
|
||||
this.toolbarIntegration.updateStatusDisplay(statuses);
|
||||
}
|
||||
// Actualizar dropdown
|
||||
this.statusDropdown.update(statuses);
|
||||
// Actualizar status pane si está abierto
|
||||
const statusPaneLeaf =
|
||||
this.app.workspace.getLeavesOfType("status-pane")[0];
|
||||
if (
|
||||
statusPaneLeaf?.view &&
|
||||
statusPaneLeaf.view instanceof StatusPaneViewController
|
||||
) {
|
||||
(statusPaneLeaf.view as StatusPaneViewController).update();
|
||||
}
|
||||
// Actualizar explorador si es necesario
|
||||
if (this.settings.showStatusIconsInExplorer && file) {
|
||||
const fileObj = this.app.vault.getFileByPath(file);
|
||||
if (fileObj) {
|
||||
this.explorerIntegration.updateFileExplorerIcons(fileObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async openStatusPane() {
|
||||
await StatusPaneViewController.open(this.app);
|
||||
}
|
||||
|
||||
async saveSettings() {
|
||||
await this.saveData(this.settings);
|
||||
|
||||
// Actualizar servicios
|
||||
this.statusService.updateSettings(this.settings);
|
||||
this.styleService.updateSettings(this.settings);
|
||||
|
||||
// Actualizar integraciones
|
||||
this.explorerIntegration.updateSettings(this.settings);
|
||||
this.fileContextMenuIntegration.updateSettings(this.settings);
|
||||
this.editorIntegration.updateSettings(this.settings);
|
||||
this.metadataIntegration?.updateSettings(this.settings);
|
||||
this.commandIntegration?.updateSettings(this.settings);
|
||||
this.toolbarIntegration.updateSettings(this.settings);
|
||||
this.workspaceIntegration.updateSettings(this.settings);
|
||||
this.statusPane?.updateSettings(this.settings);
|
||||
// Actualizar componentes UI
|
||||
this.statusBar.updateSettings(this.settings);
|
||||
this.statusDropdown.updateSettings(this.settings);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
// Clean up event listeners
|
||||
if (this.boundHandleStatusChanged) {
|
||||
window.removeEventListener(
|
||||
"note-status:status-changed",
|
||||
this.boundHandleStatusChanged,
|
||||
);
|
||||
}
|
||||
|
||||
// Clean up integrations
|
||||
this.explorerIntegration?.unload();
|
||||
this.toolbarIntegration?.unload();
|
||||
this.fileContextMenuIntegration?.unload();
|
||||
this.workspaceIntegration?.unload();
|
||||
this.metadataIntegration?.unload();
|
||||
this.editorIntegration?.unload();
|
||||
this.commandIntegration?.unload();
|
||||
|
||||
// Clean up services
|
||||
this.styleService?.unload();
|
||||
|
||||
// Clean up UI components
|
||||
this.statusBar?.unload();
|
||||
this.statusDropdown?.unload();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
100
styles/base.css
100
styles/base.css
|
|
@ -2,69 +2,93 @@
|
|||
* Base styles and variables for Note Status Plugin
|
||||
*/
|
||||
:root {
|
||||
--status-transition-time: 0.22s;
|
||||
--status-border-radius: var(--radius-s, 4px);
|
||||
--status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.15));
|
||||
--status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, 0.2));
|
||||
--status-icon-size: 16px;
|
||||
|
||||
/* Animation curves */
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
--status-transition-time: 0.22s;
|
||||
--status-border-radius: var(--radius-s, 4px);
|
||||
--status-box-shadow: var(--shadow-s, 0 2px 8px rgba(0, 0, 0, 0.15));
|
||||
--status-hover-shadow: var(--shadow-m, 0 4px 12px rgba(0, 0, 0, 0.2));
|
||||
--status-icon-size: 16px;
|
||||
|
||||
/* Animation curves */
|
||||
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--ease-out: cubic-bezier(0, 0, 0.2, 1);
|
||||
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes note-status-scale-in {
|
||||
from { opacity: 0; transform: scale(0.9); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes note-status-slide-in-fade {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes note-status-fade-in-slide-down {
|
||||
from { opacity: 0; transform: translateY(-6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes note-status-pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(0.98); }
|
||||
100% { transform: scale(1); }
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
.theme-dark .note-status-popover {
|
||||
box-shadow: var(--shadow-l);
|
||||
box-shadow: var(--shadow-l);
|
||||
}
|
||||
|
||||
.theme-dark .note-status-option-selecting {
|
||||
box-shadow: 0 0 0 1px var(--interactive-accent);
|
||||
box-shadow: 0 0 0 1px var(--interactive-accent);
|
||||
}
|
||||
|
||||
/* High contrast improvements */
|
||||
@media (forced-colors: active) {
|
||||
.note-status-chip,
|
||||
.note-status-option.is-selected {
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
|
||||
.note-status-action-button:focus-visible,
|
||||
.note-status-chip:focus-visible,
|
||||
.note-status-option:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.note-status-chip,
|
||||
.note-status-option.is-selected {
|
||||
border: 1px solid currentColor;
|
||||
}
|
||||
|
||||
.note-status-action-button:focus-visible,
|
||||
.note-status-chip:focus-visible,
|
||||
.note-status-option:focus-visible {
|
||||
outline: 2px solid currentColor;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
.note-status-unified-dropdown,
|
||||
.note-status-bar,
|
||||
.note-status-pane {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
.note-status-unified-dropdown,
|
||||
.note-status-bar,
|
||||
.note-status-pane {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
132
styles/utils.css
132
styles/utils.css
|
|
@ -2,105 +2,107 @@
|
|||
* Utility classes for Note Status Plugin
|
||||
*/
|
||||
.note-status-empty-indicator {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: var(--size-4-1, 4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-accent);
|
||||
font-size: 1.2em;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
padding: var(--size-4-1, 4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-accent);
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* Action buttons in modal */
|
||||
.note-status-action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-s);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: var(--radius-s);
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.note-status-action-button:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
background-color: var(--background-modifier-hover);
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.note-status-action-button.note-status-button-active {
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
transform: scale(0.95);
|
||||
background-color: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.note-status-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.note-status-loading span {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.note-status-loading span:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
border-top-color: var(--text-accent);
|
||||
border-radius: 50%;
|
||||
animation: note-status-loading-spinner 0.8s linear infinite;
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: -8px;
|
||||
border: 2px solid var(--background-modifier-border);
|
||||
border-top-color: var(--text-accent);
|
||||
border-radius: 50%;
|
||||
animation: note-status-loading-spinner 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes note-status-loading-spinner {
|
||||
to { transform: rotate(360deg); }
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Positioning classes for dropdowns */
|
||||
.note-status-dummy-target {
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: var(--pos-x-px, 0);
|
||||
top: var(--pos-y-px, 0);
|
||||
pointer-events: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
width: 0;
|
||||
height: 0;
|
||||
left: var(--pos-x-px, 0);
|
||||
top: var(--pos-y-px, 0);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.note-status-popover-fixed {
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
--pos-x: 0;
|
||||
--pos-y: 0;
|
||||
--max-height: 300px;
|
||||
left: var(--pos-x-px, 0);
|
||||
top: var(--pos-y-px, 0);
|
||||
max-height: var(--max-height-px, 300px);
|
||||
position: fixed;
|
||||
z-index: 999;
|
||||
--pos-x: 0;
|
||||
--pos-y: 0;
|
||||
--max-height: 300px;
|
||||
left: var(--pos-x-px, 0);
|
||||
top: var(--pos-y-px, 0);
|
||||
max-height: var(--max-height-px, 300px);
|
||||
}
|
||||
|
||||
/* Position adjustment classes */
|
||||
.note-status-popover-right {
|
||||
left: auto !important;
|
||||
right: var(--right-offset-px, 10px);
|
||||
left: auto !important;
|
||||
right: var(--right-offset-px, 10px);
|
||||
}
|
||||
|
||||
.note-status-popover-bottom {
|
||||
top: auto !important;
|
||||
bottom: var(--bottom-offset-px, 10px);
|
||||
}
|
||||
top: auto !important;
|
||||
bottom: var(--bottom-offset-px, 10px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue