first
This commit is contained in:
@@ -0,0 +1 @@
|
||||
__pycache__
|
||||
@@ -0,0 +1,94 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a ComfyUI extension that adds a server-side notes sidebar for managing markdown notes. The extension provides a complete notes management system with both backend API endpoints and a frontend JavaScript interface.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Backend (Python)
|
||||
- **Main Module**: `__init__.py` - Core extension entry point
|
||||
- **NotesManager Class**: Handles all note operations (CRUD) with file system storage
|
||||
- **API Endpoints**: RESTful endpoints using aiohttp web framework integrated with ComfyUI's PromptServer
|
||||
- **File Storage**: Notes stored as `.md` files in the `notes/` directory
|
||||
|
||||
### Frontend (JavaScript)
|
||||
- **Extension Module**: `js/extension.js` - ComfyUI extension integration
|
||||
- **NotesSidebar Class**: Main UI component managing tabs, content area, and user interactions
|
||||
- **Styling**: `js/notes-sidebar.css` - Complete UI styling for dark theme integration
|
||||
|
||||
### Key Components
|
||||
|
||||
1. **Server Integration**: Uses ComfyUI's PromptServer.instance.routes for API endpoints
|
||||
2. **File Management**: Notes stored as markdown files with automatic directory creation
|
||||
3. **UI Architecture**: Tab-based interface with content area and action buttons
|
||||
4. **Extension Registration**: Properly registers with ComfyUI's extension manager as a sidebar tab
|
||||
5. **State Management**: Three Maps track content state:
|
||||
- `noteContent`: Current content for each note (including unsaved)
|
||||
- `savedContent`: Last saved content from server
|
||||
- `unsavedChanges`: Boolean flag for each note's modification state
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The extension exposes these REST endpoints:
|
||||
- `GET /notes_sidebar/list` - List all notes
|
||||
- `GET /notes_sidebar/note/{note_name}` - Get specific note content
|
||||
- `POST /notes_sidebar/note/{note_name}` - Save/create note
|
||||
- `DELETE /notes_sidebar/note/{note_name}` - Delete note
|
||||
|
||||
## Development Notes
|
||||
|
||||
### Extension Structure
|
||||
- Must export `NODE_CLASS_MAPPINGS`, `NODE_DISPLAY_NAME_MAPPINGS`, and `WEB_DIRECTORY` for ComfyUI compatibility
|
||||
- JavaScript extension uses ComfyUI's `app.registerExtension()` pattern
|
||||
- Sidebar integration uses `app.extensionManager.registerSidebarTab()`
|
||||
|
||||
### File Operations
|
||||
- Notes are automatically saved as `.md` files in the `notes/` directory
|
||||
- File names are URL-encoded for safe handling
|
||||
- Directory creation is handled automatically on initialization
|
||||
|
||||
### UI State Management
|
||||
- Active note tracking with tab highlighting
|
||||
- Content persistence across sidebar open/close cycles
|
||||
- Real-time tab rendering on note operations
|
||||
- Button layout: Save/Rename on left, Refresh/Delete on right
|
||||
- **Unsaved Changes Tracking**: Visual indicators (*) for modified notes
|
||||
- **Content Preservation**: Tab switching preserves unsaved content
|
||||
- **Change Detection**: Automatic tracking of modifications vs saved state
|
||||
|
||||
### Unsaved Changes Behavior
|
||||
- Notes with modifications show asterisk (*) in tab name
|
||||
- Switching tabs preserves unsaved content in inactive notes
|
||||
- `onTextAreaChange()` method tracks real-time content changes
|
||||
- Save operation clears unsaved state and updates saved content reference
|
||||
- Delete/rename operations properly clean up tracking maps
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Installation & Setup
|
||||
- No build process required - extension loads directly in ComfyUI
|
||||
- Place extension folder in ComfyUI's `custom_nodes/` directory
|
||||
- Restart ComfyUI to load the extension
|
||||
|
||||
### Testing the Extension
|
||||
No automated tests defined. Manual testing workflow:
|
||||
1. Load extension in ComfyUI
|
||||
2. Verify sidebar tab appears with "Notes" label and book icon
|
||||
3. Test CRUD operations: create, edit, save, rename, delete notes
|
||||
4. Verify tab switching and content persistence
|
||||
|
||||
### Development Workflow
|
||||
1. Modify Python code in `__init__.py` for backend changes
|
||||
2. Modify JavaScript in `js/extension.js` for frontend changes
|
||||
3. Update CSS in `js/notes-sidebar.css` for styling changes
|
||||
4. Restart ComfyUI to reload the extension (required for Python changes)
|
||||
5. Refresh browser for JavaScript/CSS changes
|
||||
|
||||
### File Structure
|
||||
- `notes/` - Storage directory for markdown notes (auto-created)
|
||||
- `js/` - Frontend assets loaded by ComfyUI
|
||||
- `__init__.py` - Main extension code
|
||||
- `pyproject.toml` - Project metadata
|
||||
@@ -0,0 +1,3 @@
|
||||
# ComfyUI Notes Sidebar
|
||||
|
||||
A ComfyUI extension that adds a notes sidebar for managing markdown notes on the server-side.
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
|
||||
WEB_DIRECTORY = "./js"
|
||||
|
||||
# Required for ComfyUI to recognize this as a valid extension
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {}
|
||||
|
||||
class NotesManager:
|
||||
def __init__(self):
|
||||
self.notes_dir = os.path.join(os.path.dirname(__file__), "notes")
|
||||
os.makedirs(self.notes_dir, exist_ok=True)
|
||||
|
||||
def get_notes_list(self) -> list[str]:
|
||||
notes : list[str] = []
|
||||
for filename in os.listdir(self.notes_dir):
|
||||
if filename.endswith('.md'):
|
||||
notes.append(filename[:-3]) # Remove .md extension
|
||||
return sorted(notes)
|
||||
|
||||
def get_note_content(self, note_name: str):
|
||||
file_path = os.path.join(self.notes_dir, f"{note_name}.md")
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
return ""
|
||||
|
||||
def save_note(self, note_name: str, content: str):
|
||||
file_path = os.path.join(self.notes_dir, f"{note_name}.md")
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
def delete_note(self, note_name: str):
|
||||
file_path = os.path.join(self.notes_dir, f"{note_name}.md")
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
notes_manager = NotesManager()
|
||||
|
||||
@PromptServer.instance.routes.get("/notes_sidebar/list")
|
||||
async def get_notes_list(request):
|
||||
try:
|
||||
notes = notes_manager.get_notes_list()
|
||||
return web.json_response({"notes": notes})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.get("/notes_sidebar/note/{note_name}")
|
||||
async def get_note(request):
|
||||
try:
|
||||
note_name = request.match_info['note_name']
|
||||
content = notes_manager.get_note_content(note_name)
|
||||
return web.json_response({"content": content})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.post("/notes_sidebar/note/{note_name}")
|
||||
async def save_note(request):
|
||||
try:
|
||||
note_name = request.match_info['note_name']
|
||||
data = await request.json()
|
||||
content = data.get('content', '')
|
||||
notes_manager.save_note(note_name, content)
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
@PromptServer.instance.routes.delete("/notes_sidebar/note/{note_name}")
|
||||
async def delete_note(request):
|
||||
try:
|
||||
note_name = request.match_info['note_name']
|
||||
notes_manager.delete_note(note_name)
|
||||
return web.json_response({"success": True})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY']
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
import { app } from "/scripts/app.js";
|
||||
|
||||
// Load CSS
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/extensions/comfyui-notes-sidebar/notes-sidebar.css';
|
||||
document.head.appendChild(link);
|
||||
|
||||
class NotesSidebar {
|
||||
constructor() {
|
||||
this.notes = [];
|
||||
this.activeNote = null;
|
||||
this.tabsContainer = null;
|
||||
this.contentArea = null;
|
||||
this.textArea = null;
|
||||
this.isInitialized = false;
|
||||
this.sidebarElement = null;
|
||||
// Track unsaved changes and content for each note
|
||||
this.unsavedChanges = new Map(); // noteName -> boolean
|
||||
this.noteContent = new Map(); // noteName -> string (current content)
|
||||
this.savedContent = new Map(); // noteName -> string (last saved content)
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadNotes();
|
||||
}
|
||||
|
||||
async createSidebarContent(el) {
|
||||
// Ensure the parent element has proper height
|
||||
el.style.height = '100%';
|
||||
el.style.display = 'flex';
|
||||
el.style.flexDirection = 'column';
|
||||
|
||||
// If already initialized, just update the existing content
|
||||
if (this.isInitialized && this.sidebarElement) {
|
||||
el.innerHTML = '';
|
||||
el.appendChild(this.sidebarElement);
|
||||
// Refresh the active note content when reopening
|
||||
if (this.activeNote) {
|
||||
this.loadNoteContent(this.activeNote);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new sidebar content
|
||||
this.sidebarElement = document.createElement('div');
|
||||
this.sidebarElement.className = 'notes-sidebar';
|
||||
|
||||
this.createTabsContainer(this.sidebarElement);
|
||||
this.createContentArea(this.sidebarElement);
|
||||
|
||||
el.appendChild(this.sidebarElement);
|
||||
this.isInitialized = true;
|
||||
|
||||
// Load notes after creating UI
|
||||
await this.loadNotes();
|
||||
}
|
||||
|
||||
createTabsContainer(parent) {
|
||||
this.tabsContainer = document.createElement('div');
|
||||
this.tabsContainer.className = 'notes-tabs-container';
|
||||
|
||||
const newTabBtn = document.createElement('button');
|
||||
newTabBtn.textContent = '+';
|
||||
newTabBtn.title = 'New Note';
|
||||
newTabBtn.className = 'notes-new-tab-btn';
|
||||
newTabBtn.onclick = () => this.createNewNote();
|
||||
|
||||
this.tabsContainer.appendChild(newTabBtn);
|
||||
parent.appendChild(this.tabsContainer);
|
||||
}
|
||||
|
||||
createContentArea(parent) {
|
||||
this.contentArea = document.createElement('div');
|
||||
this.contentArea.className = 'notes-content-area';
|
||||
|
||||
this.textArea = document.createElement('textarea');
|
||||
this.textArea.className = 'notes-textarea';
|
||||
this.textArea.placeholder = 'Start typing your notes...';
|
||||
|
||||
// Add event listener to track changes
|
||||
this.textArea.addEventListener('input', () => this.onTextAreaChange());
|
||||
|
||||
const buttonContainer = document.createElement('div');
|
||||
buttonContainer.className = 'notes-button-container';
|
||||
|
||||
const leftButtonContainer = document.createElement('div');
|
||||
leftButtonContainer.className = 'notes-button-group';
|
||||
|
||||
const rightButtonContainer = document.createElement('div');
|
||||
rightButtonContainer.className = 'notes-button-group';
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.textContent = 'delete';
|
||||
deleteBtn.className = 'notes-btn delete';
|
||||
deleteBtn.onclick = () => this.deleteActiveNote();
|
||||
|
||||
const renameBtn = document.createElement('button');
|
||||
renameBtn.textContent = 'rename';
|
||||
renameBtn.className = 'notes-btn rename';
|
||||
renameBtn.onclick = () => this.renameActiveNote();
|
||||
|
||||
const saveBtn = document.createElement('button');
|
||||
saveBtn.textContent = 'save';
|
||||
saveBtn.className = 'notes-btn save';
|
||||
saveBtn.onclick = () => this.saveActiveNote();
|
||||
|
||||
const refreshBtn = document.createElement('button');
|
||||
refreshBtn.textContent = 'refresh';
|
||||
refreshBtn.className = 'notes-btn refresh';
|
||||
refreshBtn.onclick = () => this.refreshNotes();
|
||||
|
||||
leftButtonContainer.appendChild(saveBtn);
|
||||
leftButtonContainer.appendChild(renameBtn);
|
||||
rightButtonContainer.appendChild(refreshBtn);
|
||||
rightButtonContainer.appendChild(deleteBtn);
|
||||
|
||||
buttonContainer.appendChild(leftButtonContainer);
|
||||
buttonContainer.appendChild(rightButtonContainer);
|
||||
|
||||
this.contentArea.appendChild(this.textArea);
|
||||
this.contentArea.appendChild(buttonContainer);
|
||||
parent.appendChild(this.contentArea);
|
||||
}
|
||||
|
||||
async loadNotes() {
|
||||
try {
|
||||
const response = await fetch('/notes_sidebar/list');
|
||||
const data = await response.json();
|
||||
this.notes = data.notes || [];
|
||||
|
||||
if (this.notes.length > 0 && !this.activeNote) {
|
||||
this.activeNote = this.notes[0];
|
||||
}
|
||||
|
||||
this.renderTabs();
|
||||
|
||||
// Load content for active note after rendering tabs
|
||||
if (this.activeNote) {
|
||||
await this.loadNoteContent(this.activeNote);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading notes:', error);
|
||||
}
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
const existingTabs = this.tabsContainer.querySelectorAll('.note-tab');
|
||||
existingTabs.forEach(tab => tab.remove());
|
||||
|
||||
this.notes.forEach(noteName => {
|
||||
const tab = document.createElement('button');
|
||||
const isUnsaved = this.unsavedChanges.get(noteName) || false;
|
||||
tab.textContent = noteName + (isUnsaved ? ' *' : '');
|
||||
tab.className = `note-tab ${this.activeNote === noteName ? 'active' : ''}`;
|
||||
tab.onclick = () => this.selectNote(noteName);
|
||||
|
||||
this.tabsContainer.appendChild(tab);
|
||||
});
|
||||
}
|
||||
|
||||
async selectNote(noteName) {
|
||||
// Save current content before switching
|
||||
if (this.activeNote && this.textArea) {
|
||||
this.noteContent.set(this.activeNote, this.textArea.value);
|
||||
}
|
||||
|
||||
this.activeNote = noteName;
|
||||
await this.loadNoteContent(noteName);
|
||||
this.renderTabs();
|
||||
}
|
||||
|
||||
async loadNoteContent(noteName) {
|
||||
try {
|
||||
// Check if we have unsaved content for this note
|
||||
if (this.noteContent.has(noteName)) {
|
||||
// Use the unsaved content
|
||||
if (this.textArea) {
|
||||
this.textArea.value = this.noteContent.get(noteName);
|
||||
}
|
||||
} else {
|
||||
// Load from server
|
||||
const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`);
|
||||
const data = await response.json();
|
||||
const content = data.content || '';
|
||||
if (this.textArea) {
|
||||
this.textArea.value = content;
|
||||
}
|
||||
// Store both current and saved content
|
||||
this.noteContent.set(noteName, content);
|
||||
this.savedContent.set(noteName, content);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading note content:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async saveActiveNote() {
|
||||
if (!this.activeNote) return;
|
||||
|
||||
try {
|
||||
const content = this.textArea.value;
|
||||
const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(this.activeNote)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', },
|
||||
body: JSON.stringify({ content: content })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Note saved successfully');
|
||||
// Mark as saved
|
||||
this.savedContent.set(this.activeNote, content);
|
||||
this.noteContent.set(this.activeNote, content);
|
||||
this.unsavedChanges.set(this.activeNote, false);
|
||||
this.renderTabs(); // Update tab display
|
||||
} else {
|
||||
console.error('Error saving note');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving note:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteActiveNote() {
|
||||
if (!this.activeNote) return;
|
||||
|
||||
if (!confirm(`Are you sure you want to delete "${this.activeNote}"?`)) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(this.activeNote)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Clean up tracking for deleted note
|
||||
const deletedNote = this.activeNote;
|
||||
this.unsavedChanges.delete(deletedNote);
|
||||
this.noteContent.delete(deletedNote);
|
||||
this.savedContent.delete(deletedNote);
|
||||
|
||||
await this.refreshNotes();
|
||||
// Select the first available note or clear if no notes left
|
||||
this.activeNote = this.notes.length > 0 ? this.notes[0] : null;
|
||||
if (this.activeNote) {
|
||||
await this.loadNoteContent(this.activeNote);
|
||||
} else {
|
||||
this.textArea.value = '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting note:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async renameActiveNote() {
|
||||
if (!this.activeNote) return;
|
||||
|
||||
const newName = prompt('Enter new note name:', this.activeNote);
|
||||
if (!newName || newName === this.activeNote) return;
|
||||
|
||||
try {
|
||||
// Check if new name already exists
|
||||
if (this.notes.includes(newName)) {
|
||||
alert('A note with this name already exists!');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current content
|
||||
const currentContent = this.textArea.value;
|
||||
|
||||
// Create new note with the content
|
||||
const createResponse = await fetch(`/notes_sidebar/note/${encodeURIComponent(newName)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', },
|
||||
body: JSON.stringify({ content: currentContent })
|
||||
});
|
||||
|
||||
if (createResponse.ok) {
|
||||
// Delete old note
|
||||
await fetch(`/notes_sidebar/note/${encodeURIComponent(this.activeNote)}`, { method: 'DELETE' });
|
||||
|
||||
// Update tracking maps
|
||||
const oldName = this.activeNote;
|
||||
const unsavedState = this.unsavedChanges.get(oldName) || false;
|
||||
const content = this.noteContent.get(oldName) || currentContent;
|
||||
|
||||
// Remove old tracking
|
||||
this.unsavedChanges.delete(oldName);
|
||||
this.noteContent.delete(oldName);
|
||||
this.savedContent.delete(oldName);
|
||||
|
||||
// Add new tracking
|
||||
this.noteContent.set(newName, content);
|
||||
this.savedContent.set(newName, currentContent); // The content we just saved
|
||||
this.unsavedChanges.set(newName, false); // Just saved, so no unsaved changes
|
||||
|
||||
// Update active note and refresh
|
||||
this.activeNote = newName;
|
||||
await this.refreshNotes();
|
||||
this.renderTabs();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error renaming note:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async createNewNote() {
|
||||
const noteName = prompt('Enter note name:');
|
||||
if (!noteName) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/notes_sidebar/note/${encodeURIComponent(noteName)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', },
|
||||
body: JSON.stringify({ content: '' })
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await this.refreshNotes();
|
||||
this.activeNote = noteName;
|
||||
this.textArea.value = '';
|
||||
|
||||
// Initialize tracking for new note
|
||||
this.noteContent.set(noteName, '');
|
||||
this.savedContent.set(noteName, '');
|
||||
this.unsavedChanges.set(noteName, false);
|
||||
|
||||
this.renderTabs();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error creating note:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshNotes() {
|
||||
// Preserve current content before refreshing
|
||||
if (this.activeNote && this.textArea) {
|
||||
this.noteContent.set(this.activeNote, this.textArea.value);
|
||||
}
|
||||
|
||||
await this.loadNotes();
|
||||
}
|
||||
|
||||
onTextAreaChange() {
|
||||
if (!this.activeNote) return;
|
||||
|
||||
const currentContent = this.textArea.value;
|
||||
const savedContent = this.savedContent.get(this.activeNote) || '';
|
||||
|
||||
// Update current content
|
||||
this.noteContent.set(this.activeNote, currentContent);
|
||||
|
||||
// Check if content has changed from saved version
|
||||
const hasUnsavedChanges = currentContent !== savedContent;
|
||||
this.unsavedChanges.set(this.activeNote, hasUnsavedChanges);
|
||||
|
||||
// Update tab display
|
||||
this.renderTabs();
|
||||
}
|
||||
}
|
||||
|
||||
const notesSidebar = new NotesSidebar();
|
||||
|
||||
app.registerExtension({
|
||||
name: "comfyui.notes.sidebar",
|
||||
async setup() {
|
||||
await notesSidebar.init();
|
||||
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "notes",
|
||||
icon: "pi pi-book",
|
||||
title: "Notes",
|
||||
tooltip: "Notes",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
notesSidebar.createSidebarContent(el);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
/* Notes Sidebar Styles */
|
||||
|
||||
.notes-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-family: Arial, sans-serif;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.notes-tabs-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
padding: 10px;
|
||||
background: #1a1a1a;
|
||||
border-bottom: 1px solid #444;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.notes-new-tab-btn {
|
||||
background: #444;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.notes-new-tab-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.note-tab {
|
||||
background: #444;
|
||||
border: none;
|
||||
color: #fff;
|
||||
padding: 5px 10px;
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
font-size: 12px;
|
||||
max-width: 100px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.note-tab:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.note-tab.active {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.notes-content-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 15px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notes-textarea {
|
||||
flex: 1;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #444;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
resize: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.notes-textarea:focus {
|
||||
border-color: #4CAF50;
|
||||
}
|
||||
|
||||
.notes-button-container {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.notes-button-group {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.notes-btn {
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.notes-btn:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.notes-btn.delete {
|
||||
background: #ff4444;
|
||||
}
|
||||
|
||||
.notes-btn.save {
|
||||
background: #4CAF50;
|
||||
}
|
||||
|
||||
.notes-btn.refresh {
|
||||
background: #2196F3;
|
||||
}
|
||||
|
||||
.notes-btn.rename {
|
||||
background: #FF9800;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
[project]
|
||||
name = "comfyui-notes-sidebar"
|
||||
version = "1.0.0"
|
||||
description = "A ComfyUI extension that adds a notes sidebar for managing markdown notes"
|
||||
authors = [
|
||||
{name = "ComfyUI Notes Extension", email = "example@email.com"}
|
||||
]
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
requires-python = ">=3.7"
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/your-username/comfyui-notes-sidebar"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
Reference in New Issue
Block a user