Compare commits
2 Commits
86a9d54e70
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 10d95951a3 | |||
| 50d3177e9e |
@@ -62,6 +62,7 @@ Config stored in `~/.config/claudia/config.json`
|
|||||||
- **Left/Right Arrow** - Navigate between response alternatives
|
- **Left/Right Arrow** - Navigate between response alternatives
|
||||||
- **Escape** - Close panels/modals, cancel editing
|
- **Escape** - Close panels/modals, cancel editing
|
||||||
- **Ctrl+K** - Focus message input
|
- **Ctrl+K** - Focus message input
|
||||||
|
- **Ctrl+P** - Open command palette (quick access to all actions)
|
||||||
- **Ctrl+/** - Toggle Roleplay Tools panel
|
- **Ctrl+/** - Toggle Roleplay Tools panel
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|||||||
167
ROADMAP.md
167
ROADMAP.md
@@ -19,11 +19,12 @@
|
|||||||
- Enhanced Message Controls (delete, pin, hide, continue, regenerate any message)
|
- Enhanced Message Controls (delete, pin, hide, continue, regenerate any message)
|
||||||
- Token Counter (real-time display with per-section breakdown)
|
- Token Counter (real-time display with per-section breakdown)
|
||||||
- Message Examples (character card examples injected into context)
|
- Message Examples (character card examples injected into context)
|
||||||
|
- Chat Branching/Checkpoints (create, switch, delete, rename branches from any message)
|
||||||
|
|
||||||
### 🎯 Current Focus: Advanced Chat Management
|
### 🎯 Current Focus: Quality of Life & Polish
|
||||||
**Next Up:** Implementing Chat Branching/Checkpoints to enable non-linear conversation exploration with the ability to save conversation states, create branches from any point, and switch between different conversation paths.
|
**Next Up:** Implementing high-impact QoL features to reduce friction and improve user experience - starting with Toast Notifications, Command Palette, Auto-save, Drag & Drop, and Chat Search.
|
||||||
|
|
||||||
**Recent Completion:** Message Examples - character card message examples are now parsed, processed with template variable replacement, and injected into context at configurable positions to teach the AI the character's voice and writing style.
|
**Recent Completion:** Chat Branching/Checkpoints - Full conversation branching system allowing users to create and explore alternate conversation paths from any message point. Each branch maintains its own complete message history with a branch manager modal for easy navigation.
|
||||||
|
|
||||||
## Phase 1: Core Roleplay Infrastructure (High Priority)
|
## Phase 1: Core Roleplay Infrastructure (High Priority)
|
||||||
**Goal: Enable basic roleplay-focused prompt engineering**
|
**Goal: Enable basic roleplay-focused prompt engineering**
|
||||||
@@ -87,13 +88,14 @@
|
|||||||
## Phase 3: Advanced Chat Management (Medium Priority)
|
## Phase 3: Advanced Chat Management (Medium Priority)
|
||||||
**Goal: Non-linear conversation control**
|
**Goal: Non-linear conversation control**
|
||||||
|
|
||||||
### 1. Chat Branching/Checkpoints
|
### 1. Chat Branching/Checkpoints ✅
|
||||||
- [ ] Save conversation state at any message
|
- [x] Save conversation state at any message
|
||||||
- [ ] Create branches from any point
|
- [x] Create branches from any point
|
||||||
- [ ] Switch between branches
|
- [x] Switch between branches
|
||||||
- [ ] Visual branch indicator in UI
|
- [x] Visual branch indicator in UI
|
||||||
- [ ] Branch naming and organization
|
- [x] Branch naming and organization
|
||||||
- [ ] Delete/merge branches
|
- [x] Delete branches
|
||||||
|
- [ ] Merge branches (deferred - nice to have)
|
||||||
|
|
||||||
**Why Important:** Roleplay often involves exploring "what if" scenarios. Branching lets you explore different conversation paths without losing previous progress.
|
**Why Important:** Roleplay often involves exploring "what if" scenarios. Branching lets you explore different conversation paths without losing previous progress.
|
||||||
|
|
||||||
@@ -252,6 +254,151 @@
|
|||||||
|
|
||||||
**Why Important:** Better UI means less friction and more immersion in roleplay.
|
**Why Important:** Better UI means less friction and more immersion in roleplay.
|
||||||
|
|
||||||
|
## Phase 8: Quality of Life & Polish (High Priority)
|
||||||
|
**Goal: Reduce friction, improve feedback, and enhance overall user experience**
|
||||||
|
|
||||||
|
### 1. Toast Notification System
|
||||||
|
- [ ] Create toast component (bottom-right positioning)
|
||||||
|
- [ ] Success/error/info/warning variants
|
||||||
|
- [ ] Auto-dismiss with configurable timeout
|
||||||
|
- [ ] Queue multiple toasts
|
||||||
|
- [ ] Hook into all major actions (save, delete, import, export, etc.)
|
||||||
|
|
||||||
|
**Why Important:** Users currently have no immediate feedback when actions succeed or fail. Toasts provide instant visual confirmation without blocking workflow.
|
||||||
|
|
||||||
|
### 2. Command Palette
|
||||||
|
- [ ] Ctrl+P to open command palette modal
|
||||||
|
- [ ] Fuzzy search for all actions
|
||||||
|
- [ ] Keyboard navigation (arrow keys, enter, escape)
|
||||||
|
- [ ] Recent/frequent actions at top
|
||||||
|
- [ ] Show keyboard shortcuts in results
|
||||||
|
- [ ] Categories (Chat, Character, Settings, etc.)
|
||||||
|
|
||||||
|
**Why Important:** Power users want keyboard-first workflow. Command palette dramatically speeds up common actions without memorizing shortcuts.
|
||||||
|
|
||||||
|
### 3. Auto-save & Recovery
|
||||||
|
- [ ] Auto-save unsent message in input field
|
||||||
|
- [ ] Restore unsent message after app restart
|
||||||
|
- [ ] Draft system for in-progress edits
|
||||||
|
- [ ] Session recovery (restore scroll position, open panels)
|
||||||
|
- [ ] Crash recovery with last known state
|
||||||
|
|
||||||
|
**Why Important:** Losing work due to crashes or accidental closes is extremely frustrating. Auto-save provides a safety net for all user work.
|
||||||
|
|
||||||
|
### 4. Drag & Drop Support
|
||||||
|
- [ ] Drag character card PNGs to import
|
||||||
|
- [ ] Drag lorebook JSON files to import
|
||||||
|
- [ ] Drag chat history JSON to import
|
||||||
|
- [ ] Drag images to set as character avatar
|
||||||
|
- [ ] Drop zone overlay with visual feedback
|
||||||
|
- [ ] Support for multiple file drops
|
||||||
|
|
||||||
|
**Why Important:** Drag & drop feels natural and is much faster than navigate-click-select workflow. Modern desktop apps are expected to support this.
|
||||||
|
|
||||||
|
### 5. Search in Chat History
|
||||||
|
- [ ] Ctrl+F to open search bar
|
||||||
|
- [ ] Highlight all matches in messages
|
||||||
|
- [ ] Navigate between results (prev/next buttons)
|
||||||
|
- [ ] Case-insensitive search
|
||||||
|
- [ ] Search counter (e.g., "3 of 42 matches")
|
||||||
|
- [ ] Clear search and restore view
|
||||||
|
|
||||||
|
**Why Important:** Long roleplay sessions can span hundreds of messages. Finding specific content without search is tedious and time-consuming.
|
||||||
|
|
||||||
|
### 6. Context Menus (Right-Click)
|
||||||
|
- [ ] Right-click messages for actions (edit, delete, regenerate, branch, copy)
|
||||||
|
- [ ] Right-click character dropdown for quick actions
|
||||||
|
- [ ] Right-click World Info entries for edit/delete
|
||||||
|
- [ ] Right-click in message input for paste/clear/templates
|
||||||
|
- [ ] Context-aware menu items
|
||||||
|
|
||||||
|
**Why Important:** Right-click is muscle memory for desktop users. Faster than hovering to reveal action buttons.
|
||||||
|
|
||||||
|
### 7. Better Feedback & Confirmations
|
||||||
|
- [ ] Confirmation dialogs for destructive actions (delete character, clear chat)
|
||||||
|
- [ ] Loading spinners for API calls
|
||||||
|
- [ ] Progress bars for file imports
|
||||||
|
- [ ] "Saving..." / "Saved" indicators
|
||||||
|
- [ ] Success messages for completed actions
|
||||||
|
|
||||||
|
**Why Important:** Users should never wonder if an action succeeded or is still processing. Clear feedback prevents confusion and repeated clicks.
|
||||||
|
|
||||||
|
### 8. Undo/Redo System
|
||||||
|
- [ ] Undo message edit (Ctrl+Z)
|
||||||
|
- [ ] Undo message delete
|
||||||
|
- [ ] Undo character field changes
|
||||||
|
- [ ] Undo World Info changes
|
||||||
|
- [ ] Action history panel (optional)
|
||||||
|
- [ ] Redo support (Ctrl+Shift+Z)
|
||||||
|
|
||||||
|
**Why Important:** Mistakes happen. An undo system provides a safety net and encourages experimentation without fear of losing work.
|
||||||
|
|
||||||
|
### 9. Settings Search
|
||||||
|
- [ ] Search bar at top of settings panel
|
||||||
|
- [ ] Fuzzy search across all setting names and descriptions
|
||||||
|
- [ ] Highlight matching settings
|
||||||
|
- [ ] Collapse/expand sections based on matches
|
||||||
|
- [ ] "Recently changed" section
|
||||||
|
|
||||||
|
**Why Important:** With 22+ features, finding specific settings is tedious. Search makes configuration much faster.
|
||||||
|
|
||||||
|
### 10. Character Management Enhancements
|
||||||
|
- [ ] Recent characters quick-switch dropdown
|
||||||
|
- [ ] Character search/filter by name or tags
|
||||||
|
- [ ] Character folders/categories
|
||||||
|
- [ ] Duplicate character (as template)
|
||||||
|
- [ ] Favorite/star characters
|
||||||
|
- [ ] Sort options (name, date created, last used)
|
||||||
|
|
||||||
|
**Why Important:** Managing 10+ characters becomes messy. Better organization tools scale with user's character collection.
|
||||||
|
|
||||||
|
### 11. Enhanced Keyboard Support
|
||||||
|
- [ ] Full keyboard navigation in all modals (Tab, Arrow keys, Enter)
|
||||||
|
- [ ] Escape to close any open panel/modal
|
||||||
|
- [ ] Vim-style navigation mode (optional, j/k for scroll)
|
||||||
|
- [ ] Keyboard shortcut hints on hover
|
||||||
|
- [ ] Focus indicators for keyboard navigation
|
||||||
|
|
||||||
|
**Why Important:** Keyboard navigation should work everywhere. Current implementation is inconsistent across different UI sections.
|
||||||
|
|
||||||
|
### 12. Export/Share Enhancements
|
||||||
|
- [ ] Export conversation as formatted HTML
|
||||||
|
- [ ] Export conversation as formatted PDF
|
||||||
|
- [ ] Export as markdown with proper formatting
|
||||||
|
- [ ] Copy conversation to clipboard (formatted)
|
||||||
|
- [ ] Export individual messages
|
||||||
|
|
||||||
|
**Why Important:** Users want to share and archive conversations in readable formats, not just JSON.
|
||||||
|
|
||||||
|
### 13. Accessibility Improvements
|
||||||
|
- [ ] ARIA labels for all interactive elements
|
||||||
|
- [ ] Screen reader support
|
||||||
|
- [ ] High contrast mode option
|
||||||
|
- [ ] Larger click targets option (accessibility mode)
|
||||||
|
- [ ] Reduced motion mode (respect prefers-reduced-motion)
|
||||||
|
- [ ] Focus indicators for keyboard navigation
|
||||||
|
|
||||||
|
**Why Important:** Accessibility makes the app usable for everyone, including users with disabilities. It's also often legally required.
|
||||||
|
|
||||||
|
### 14. Better Visual Feedback
|
||||||
|
- [ ] Smooth transitions for panel open/close
|
||||||
|
- [ ] Hover states for all interactive elements
|
||||||
|
- [ ] Active state indicators (focused panel)
|
||||||
|
- [ ] Better empty states with helpful text
|
||||||
|
- [ ] Skeleton loaders for content loading
|
||||||
|
- [ ] Micro-animations for actions (delete, save, etc.)
|
||||||
|
|
||||||
|
**Why Important:** Visual polish makes the app feel responsive and professional. Small animations provide context for state changes.
|
||||||
|
|
||||||
|
### 15. Smart Defaults & Templates
|
||||||
|
- [ ] Scenario templates (fantasy RPG, sci-fi, modern, etc.)
|
||||||
|
- [ ] Pre-filled World Info templates
|
||||||
|
- [ ] Character card templates
|
||||||
|
- [ ] Quick-start wizard for new users
|
||||||
|
- [ ] Import from popular character repositories
|
||||||
|
|
||||||
|
**Why Important:** Reduces friction for new users and speeds up common tasks. Templates provide starting points for customization.
|
||||||
|
|
||||||
## Implementation Priority Ranking
|
## Implementation Priority Ranking
|
||||||
|
|
||||||
### Must-Have for Basic Roleplay:
|
### Must-Have for Basic Roleplay:
|
||||||
|
|||||||
@@ -507,6 +507,47 @@ struct ChatHistory {
|
|||||||
messages: Vec<Message>,
|
messages: Vec<Message>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New branching structures
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct Branch {
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
created_at: i64, // Unix timestamp in milliseconds
|
||||||
|
#[serde(default)]
|
||||||
|
parent_branch_id: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
diverge_at_index: usize, // Message index in parent where this branch diverged
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct BranchedChatHistory {
|
||||||
|
#[serde(default = "default_branches")]
|
||||||
|
branches: Vec<Branch>,
|
||||||
|
#[serde(default = "default_active_branch")]
|
||||||
|
active_branch_id: String,
|
||||||
|
#[serde(default)]
|
||||||
|
branch_messages: std::collections::HashMap<String, Vec<Message>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_branches() -> Vec<Branch> {
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
vec![Branch {
|
||||||
|
id: "main".to_string(),
|
||||||
|
name: "Main".to_string(),
|
||||||
|
created_at: timestamp,
|
||||||
|
parent_branch_id: None,
|
||||||
|
diverge_at_index: 0,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_active_branch() -> String {
|
||||||
|
"main".to_string()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct ChatRequest {
|
struct ChatRequest {
|
||||||
model: String,
|
model: String,
|
||||||
@@ -951,21 +992,61 @@ fn save_config(config: &ApiConfig) -> Result<(), String> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_history(character_id: &str) -> ChatHistory {
|
// Load branched history (with backward compatibility)
|
||||||
|
fn load_branched_history(character_id: &str) -> BranchedChatHistory {
|
||||||
let path = get_character_history_path(character_id);
|
let path = get_character_history_path(character_id);
|
||||||
if let Ok(contents) = fs::read_to_string(path) {
|
if let Ok(contents) = fs::read_to_string(&path) {
|
||||||
let mut history: ChatHistory = serde_json::from_str(&contents).unwrap_or(ChatHistory { messages: vec![] });
|
// Try to load as new branched format first
|
||||||
|
if let Ok(mut branched) = serde_json::from_str::<BranchedChatHistory>(&contents) {
|
||||||
// Migrate old messages to new format
|
// Migrate old messages to new format
|
||||||
for msg in &mut history.messages {
|
for messages in branched.branch_messages.values_mut() {
|
||||||
|
for msg in messages {
|
||||||
msg.migrate();
|
msg.migrate();
|
||||||
}
|
}
|
||||||
history
|
}
|
||||||
} else {
|
return branched;
|
||||||
ChatHistory { messages: vec![] }
|
}
|
||||||
|
|
||||||
|
// Fall back to old linear format and migrate
|
||||||
|
if let Ok(mut old_history) = serde_json::from_str::<ChatHistory>(&contents) {
|
||||||
|
for msg in &mut old_history.messages {
|
||||||
|
msg.migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to branched format
|
||||||
|
let mut branch_messages = HashMap::new();
|
||||||
|
branch_messages.insert("main".to_string(), old_history.messages);
|
||||||
|
|
||||||
|
return BranchedChatHistory {
|
||||||
|
branches: default_branches(),
|
||||||
|
active_branch_id: "main".to_string(),
|
||||||
|
branch_messages,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn save_history(character_id: &str, history: &ChatHistory) -> Result<(), String> {
|
// Return empty history with main branch
|
||||||
|
let mut branch_messages = HashMap::new();
|
||||||
|
branch_messages.insert("main".to_string(), vec![]);
|
||||||
|
|
||||||
|
BranchedChatHistory {
|
||||||
|
branches: default_branches(),
|
||||||
|
active_branch_id: "main".to_string(),
|
||||||
|
branch_messages,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy function - returns active branch messages
|
||||||
|
fn load_history(character_id: &str) -> ChatHistory {
|
||||||
|
let branched = load_branched_history(character_id);
|
||||||
|
let messages = branched.branch_messages
|
||||||
|
.get(&branched.active_branch_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
ChatHistory { messages }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_branched_history(character_id: &str, history: &BranchedChatHistory) -> Result<(), String> {
|
||||||
let path = get_character_history_path(character_id);
|
let path = get_character_history_path(character_id);
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
|
||||||
@@ -975,6 +1056,13 @@ fn save_history(character_id: &str, history: &ChatHistory) -> Result<(), String>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Legacy function - saves to active branch
|
||||||
|
fn save_history(character_id: &str, history: &ChatHistory) -> Result<(), String> {
|
||||||
|
let mut branched = load_branched_history(character_id);
|
||||||
|
branched.branch_messages.insert(branched.active_branch_id.clone(), history.messages.clone());
|
||||||
|
save_branched_history(character_id, &branched)
|
||||||
|
}
|
||||||
|
|
||||||
fn load_character(character_id: &str) -> Option<Character> {
|
fn load_character(character_id: &str) -> Option<Character> {
|
||||||
let path = get_character_path(character_id);
|
let path = get_character_path(character_id);
|
||||||
if let Ok(contents) = fs::read_to_string(path) {
|
if let Ok(contents) = fs::read_to_string(path) {
|
||||||
@@ -3299,6 +3387,153 @@ async fn export_character_card(app_handle: tauri::AppHandle, character_id: Strin
|
|||||||
Ok(output_path.to_string_lossy().to_string())
|
Ok(output_path.to_string_lossy().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Branch Management Commands
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn create_branch(message_index: usize, branch_name: String) -> Result<Branch, String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let mut branched = load_branched_history(&character.id);
|
||||||
|
|
||||||
|
// Generate new branch ID
|
||||||
|
let branch_id = Uuid::new_v4().to_string();
|
||||||
|
|
||||||
|
// Get current branch messages
|
||||||
|
let current_messages = branched.branch_messages
|
||||||
|
.get(&branched.active_branch_id)
|
||||||
|
.ok_or_else(|| "Active branch not found".to_string())?;
|
||||||
|
|
||||||
|
// Validate message index
|
||||||
|
if message_index > current_messages.len() {
|
||||||
|
return Err(format!("Invalid message index: {}", message_index));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new branch
|
||||||
|
let timestamp = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_millis() as i64;
|
||||||
|
|
||||||
|
let new_branch = Branch {
|
||||||
|
id: branch_id.clone(),
|
||||||
|
name: branch_name,
|
||||||
|
created_at: timestamp,
|
||||||
|
parent_branch_id: Some(branched.active_branch_id.clone()),
|
||||||
|
diverge_at_index: message_index,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy messages up to divergence point
|
||||||
|
let branch_messages: Vec<Message> = current_messages[..message_index].to_vec();
|
||||||
|
|
||||||
|
// Add branch and its messages
|
||||||
|
branched.branches.push(new_branch.clone());
|
||||||
|
branched.branch_messages.insert(branch_id.clone(), branch_messages);
|
||||||
|
|
||||||
|
// Save and return
|
||||||
|
save_branched_history(&character.id, &branched)?;
|
||||||
|
Ok(new_branch)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn switch_branch(branch_id: String) -> Result<Vec<Message>, String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let mut branched = load_branched_history(&character.id);
|
||||||
|
|
||||||
|
// Verify branch exists
|
||||||
|
if !branched.branch_messages.contains_key(&branch_id) {
|
||||||
|
return Err(format!("Branch '{}' not found", branch_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Switch active branch
|
||||||
|
branched.active_branch_id = branch_id.clone();
|
||||||
|
save_branched_history(&character.id, &branched)?;
|
||||||
|
|
||||||
|
// Return messages for the new active branch
|
||||||
|
let messages = branched.branch_messages
|
||||||
|
.get(&branch_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn delete_branch(branch_id: String) -> Result<(), String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let mut branched = load_branched_history(&character.id);
|
||||||
|
|
||||||
|
// Cannot delete main branch
|
||||||
|
if branch_id == "main" {
|
||||||
|
return Err("Cannot delete main branch".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot delete active branch
|
||||||
|
if branch_id == branched.active_branch_id {
|
||||||
|
return Err("Cannot delete active branch. Switch to another branch first.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove branch
|
||||||
|
branched.branches.retain(|b| b.id != branch_id);
|
||||||
|
branched.branch_messages.remove(&branch_id);
|
||||||
|
|
||||||
|
// Also remove any child branches that depended on this one
|
||||||
|
let mut branches_to_remove = vec![];
|
||||||
|
for branch in &branched.branches {
|
||||||
|
if branch.parent_branch_id.as_ref() == Some(&branch_id) {
|
||||||
|
branches_to_remove.push(branch.id.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for child_id in branches_to_remove {
|
||||||
|
branched.branches.retain(|b| b.id != child_id);
|
||||||
|
branched.branch_messages.remove(&child_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
save_branched_history(&character.id, &branched)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn list_branches() -> Result<Vec<Branch>, String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let branched = load_branched_history(&character.id);
|
||||||
|
Ok(branched.branches)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn rename_branch(branch_id: String, new_name: String) -> Result<(), String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let mut branched = load_branched_history(&character.id);
|
||||||
|
|
||||||
|
// Find and rename the branch
|
||||||
|
let branch = branched.branches.iter_mut()
|
||||||
|
.find(|b| b.id == branch_id)
|
||||||
|
.ok_or_else(|| format!("Branch '{}' not found", branch_id))?;
|
||||||
|
|
||||||
|
branch.name = new_name;
|
||||||
|
save_branched_history(&character.id, &branched)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_active_branch_id() -> Result<String, String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let branched = load_branched_history(&character.id);
|
||||||
|
Ok(branched.active_branch_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_branch_info(branch_id: String) -> Result<Branch, String> {
|
||||||
|
let character = get_active_character();
|
||||||
|
let branched = load_branched_history(&character.id);
|
||||||
|
|
||||||
|
branched.branches.iter()
|
||||||
|
.find(|b| b.id == branch_id)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| format!("Branch '{}' not found", branch_id))
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -3362,7 +3597,14 @@ pub fn run() {
|
|||||||
update_world_info_entry,
|
update_world_info_entry,
|
||||||
delete_world_info_entry,
|
delete_world_info_entry,
|
||||||
export_world_info,
|
export_world_info,
|
||||||
import_world_info
|
import_world_info,
|
||||||
|
create_branch,
|
||||||
|
switch_branch,
|
||||||
|
delete_branch,
|
||||||
|
list_branches,
|
||||||
|
rename_branch,
|
||||||
|
get_active_branch_id,
|
||||||
|
get_branch_info
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -733,6 +733,33 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Toast Container -->
|
||||||
|
<div id="toast-container" class="toast-container"></div>
|
||||||
|
|
||||||
|
<!-- Command Palette -->
|
||||||
|
<div id="command-palette-modal" class="command-palette-modal" style="display: none;">
|
||||||
|
<div class="command-palette-overlay"></div>
|
||||||
|
<div class="command-palette-content">
|
||||||
|
<div class="command-palette-search">
|
||||||
|
<svg class="command-palette-search-icon" width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||||
|
<circle cx="8" cy="8" r="6" stroke="currentColor" stroke-width="2"/>
|
||||||
|
<path d="M12.5 12.5L17 17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="command-palette-input"
|
||||||
|
class="command-palette-input"
|
||||||
|
placeholder="Type a command or search..."
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<kbd class="command-palette-hint">Esc to close</kbd>
|
||||||
|
</div>
|
||||||
|
<div id="command-palette-results" class="command-palette-results">
|
||||||
|
<!-- Command results will be dynamically populated here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Avatar zoom modal -->
|
<!-- Avatar zoom modal -->
|
||||||
<div id="avatar-modal" class="avatar-modal" style="display: none;">
|
<div id="avatar-modal" class="avatar-modal" style="display: none;">
|
||||||
<div class="avatar-modal-overlay"></div>
|
<div class="avatar-modal-overlay"></div>
|
||||||
|
|||||||
847
src/main.js
847
src/main.js
File diff suppressed because it is too large
Load Diff
614
src/styles.css
614
src/styles.css
@@ -211,6 +211,151 @@ body {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Branch Badge */
|
||||||
|
.branch-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #22c55e;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-badge:hover {
|
||||||
|
background: rgba(34, 197, 94, 0.15);
|
||||||
|
border-color: rgba(34, 197, 94, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-badge svg {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Branch Manager Modal */
|
||||||
|
.branch-manager-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10002;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-manager-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-manager-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 80vh;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-manager-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-manager-header h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 60vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-item {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-item:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-item.active {
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-name {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-meta {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.branch-active-label {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: var(--accent);
|
||||||
|
color: white;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.icon-btn {
|
.icon-btn {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
@@ -371,19 +516,24 @@ body {
|
|||||||
/* Message action buttons */
|
/* Message action buttons */
|
||||||
.message-actions {
|
.message-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: -4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease;
|
transition: opacity 0.2s ease;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 8px;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.user .message-actions {
|
.message.user .message-actions {
|
||||||
right: 8px;
|
right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.assistant .message-actions {
|
.message.assistant .message-actions {
|
||||||
right: 8px;
|
right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message:hover .message-actions {
|
.message:hover .message-actions {
|
||||||
@@ -391,26 +541,28 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-action-btn {
|
.message-action-btn {
|
||||||
width: 24px;
|
width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
border: none;
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.85);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(8px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
color: var(--text-secondary);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-action-btn:hover {
|
.message-action-btn:hover {
|
||||||
background: rgba(0, 0, 0, 0.7);
|
background: rgba(0, 0, 0, 0.95);
|
||||||
color: var(--text-primary);
|
border-color: rgba(255, 255, 255, 0.3);
|
||||||
transform: scale(1.1);
|
color: white;
|
||||||
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-action-btn:active {
|
.message-action-btn:active {
|
||||||
@@ -418,8 +570,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-action-btn svg {
|
.message-action-btn svg {
|
||||||
width: 14px;
|
width: 16px;
|
||||||
height: 14px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced message control buttons */
|
/* Enhanced message control buttons */
|
||||||
@@ -2029,3 +2181,435 @@ body.view-comfortable .message-content pre {
|
|||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toast Notification System */
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10003;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
pointer-events: auto;
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
-webkit-backdrop-filter: blur(12px);
|
||||||
|
animation: slideInRight 0.3s ease;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.removing {
|
||||||
|
animation: slideOutRight 0.3s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(400px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast Variants */
|
||||||
|
.toast.success {
|
||||||
|
border-color: rgba(34, 197, 94, 0.5);
|
||||||
|
background: rgba(34, 197, 94, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success .toast-icon {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success .toast-title {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
border-color: rgba(239, 68, 68, 0.5);
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error .toast-icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error .toast-title {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning {
|
||||||
|
border-color: rgba(249, 115, 22, 0.5);
|
||||||
|
background: rgba(249, 115, 22, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning .toast-icon {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.warning .toast-title {
|
||||||
|
color: #f97316;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
border-color: rgba(99, 102, 241, 0.5);
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info .toast-icon {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info .toast-title {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Progress bar for auto-dismiss */
|
||||||
|
.toast-progress {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 0 0 12px 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-progress-bar {
|
||||||
|
height: 100%;
|
||||||
|
background: currentColor;
|
||||||
|
animation: progressShrink var(--duration) linear forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progressShrink {
|
||||||
|
from {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive toast positioning */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.toast-container {
|
||||||
|
left: 12px;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 12px;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Command Palette */
|
||||||
|
.command-palette-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 10004;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 15vh;
|
||||||
|
animation: fadeIn 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 12px;
|
||||||
|
width: 90%;
|
||||||
|
max-width: 600px;
|
||||||
|
max-height: 60vh;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
animation: slideInDown 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideInDown {
|
||||||
|
from {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-search-icon {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-input {
|
||||||
|
flex: 1;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-input::placeholder {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-hint {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-results {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 50vh;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-results::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-results::-webkit-scrollbar-track {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-results::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-color: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item.selected {
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--accent);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-description {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-shortcut {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-shortcut kbd {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-empty {
|
||||||
|
padding: 40px 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-empty-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
margin: 0 auto 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-empty-text {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-section {
|
||||||
|
padding: 8px 12px 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-section:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive command palette */
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.command-palette-modal {
|
||||||
|
padding-top: 10vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-palette-content {
|
||||||
|
width: 95%;
|
||||||
|
max-height: 70vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.command-item-shortcut {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user