# longcipher-leptos-components A production-ready UI component library for Leptos web applications. This library provides tree-shakeable, type-safe components with SSR/CSR compatibility, built-in accessibility following WCAG 2.1 guidelines, and theming via CSS custom properties. The core offering is a full-featured text editor component with optional syntax highlighting, find & replace, code folding, and document statistics. The library is designed with modularity in mind using Cargo feature flags to minimize bundle sizes. Components follow Leptos conventions with `#[prop(into)]` for flexible input types, optional props with sensible defaults, and controlled state management through signals. All components return `impl IntoView` and support custom styling via a `class` prop. ## Installation Add to your `Cargo.toml` with desired features: ```toml # Minimal editor [dependencies] longcipher-leptos-components = { version = "0.1", features = ["editor"] } # Full-featured editor with all capabilities [dependencies] longcipher-leptos-components = { version = "0.1", features = ["editor-full"] } # With SSR support [dependencies] longcipher-leptos-components = { version = "0.1", features = ["editor", "ssr"] } ``` ## Editor Component The main Editor component provides a full-featured text editing experience with line numbers, undo/redo, syntax highlighting, and various optional features. ```rust use leptos::prelude::*; use longcipher_leptos_components::editor::Editor; #[component] fn MyEditor() -> impl IntoView { let (content, set_content) = signal(String::from("# Hello World\n\nStart editing...")); let (cursor_pos, set_cursor_pos) = signal((1usize, 1usize)); let (selection, set_selection) = signal(Option::::None); view! { // Display cursor position
"Line: " {move || cursor_pos.get().0} ", Column: " {move || cursor_pos.get().1} {move || selection.get().map(|s| format!(" | {} chars selected", s.len()))}
} } ``` ## EditorState API The `EditorState` struct manages editor content, cursors, and history programmatically. Use this for advanced manipulation outside the component. ```rust use longcipher_leptos_components::editor::{EditorState, EditorConfig, CursorPosition}; // Create editor state with default config let mut state = EditorState::new("Initial content"); // Create with custom configuration let config = EditorConfig { tab_size: 2, insert_spaces: true, word_wrap: true, show_line_numbers: true, highlight_current_line: true, match_brackets: true, auto_indent: true, auto_close_brackets: true, font_size: 16.0, line_height: 1.6, read_only: false, ..Default::default() }; let mut state = EditorState::with_config("fn main() {}", config); // Content operations state.set_content("New content"); // Adds to history state.replace_content("New content"); // No history entry let content = state.content(); // Get current content let lines = state.line_count(); // Count lines let line = state.get_line(0); // Get specific line (0-indexed) // Cursor operations let pos = state.cursor_position(); // Get current position state.set_cursor(CursorPosition::new(5, 10)); // Set to line 5, column 10 state.set_cursor_with_selection( CursorPosition::new(5, 15), // head CursorPosition::new(5, 10) // anchor ); // Text manipulation state.insert("Hello"); // Insert at cursor state.delete_backward(); // Backspace state.delete_forward(); // Delete // Undo/Redo if state.can_undo() { state.undo(); } if state.can_redo() { state.redo(); } // Track modifications state.mark_saved(); // Clear modified flag let is_modified = state.is_modified; // Check if modified since last save // Position conversion let offset = state.position_to_offset(CursorPosition::new(2, 5)); let position = state.offset_to_position(100); ``` ## History Management The `History` struct provides undo/redo functionality with configurable coalescing of rapid edits. ```rust use longcipher_leptos_components::editor::{History, HistoryConfig, HistoryEntry}; use longcipher_leptos_components::editor::{CursorSet, Cursor, CursorPosition}; // Create with default config (1000 entries, 500ms coalesce window) let mut history = History::new(); // Custom configuration let config = HistoryConfig { max_entries: 500, // Maximum undo stack size coalesce_window_ms: 300, // Group edits within 300ms }; let mut history = History::with_config(config); // Record state changes let cursors = CursorSet::new(Cursor::new(CursorPosition::zero())); history.push("state1".to_string(), cursors.clone()); // For explicit checkpoints that won't be coalesced history.push_checkpoint("important state".to_string(), cursors.clone()); // Perform undo let current_content = "current text"; let current_cursors = CursorSet::new(Cursor::zero()); if let Some(entry) = history.undo(current_content, ¤t_cursors) { // entry.content contains the previous content // entry.cursors contains the previous cursor state println!("Restored: {}", entry.content); } // Perform redo if let Some(entry) = history.redo(current_content, ¤t_cursors) { println!("Redone: {}", entry.content); } // Query state let can_undo = history.can_undo(); let can_redo = history.can_redo(); let undo_count = history.undo_count(); let redo_count = history.redo_count(); // Clear all history history.clear(); ``` ## Cursor and Selection Manage cursor positions and text selections with multi-cursor support. ```rust use longcipher_leptos_components::editor::{ CursorPosition, Cursor, CursorSet, Selection, SelectionMode }; // Create cursor positions (0-indexed line and column) let pos = CursorPosition::new(10, 25); // Line 10, column 25 let zero = CursorPosition::zero(); // Line 0, column 0 // Position comparisons let pos1 = CursorPosition::new(5, 10); let pos2 = CursorPosition::new(5, 20); let is_before = pos1.is_before(&pos2); // true let min_pos = pos1.min(&pos2); // pos1 let max_pos = pos1.max(&pos2); // pos2 // Create cursors let cursor = Cursor::new(CursorPosition::new(5, 10)); let cursor_zero = Cursor::zero(); // Create cursor with selection (head and anchor) let cursor_with_sel = Cursor::with_selection( CursorPosition::new(5, 20), // head (where cursor is) CursorPosition::new(5, 10) // anchor (selection start) ); // Selection operations let has_sel = cursor_with_sel.has_selection(); // true let start = cursor_with_sel.selection_start(); // (5, 10) let end = cursor_with_sel.selection_end(); // (5, 20) // Modify cursor let mut cursor = Cursor::new(CursorPosition::new(0, 0)); cursor.move_to(CursorPosition::new(10, 5), false); // Move without selection cursor.move_to(CursorPosition::new(10, 15), true); // Extend selection cursor.collapse(); // Remove selection cursor.set_preferred_column(20); // For vertical movement // Multi-cursor support with CursorSet let mut cursors = CursorSet::new(Cursor::zero()); cursors.add(Cursor::new(CursorPosition::new(5, 0))); // Add second cursor cursors.add(Cursor::new(CursorPosition::new(10, 0))); // Add third cursor let primary = cursors.primary(); // First cursor let all = cursors.all(); // All cursors let is_multi = cursors.is_multi(); // true (multiple cursors) cursors.collapse_to_primary(); // Keep only primary cursor // Selection struct for range operations let selection = Selection::new( CursorPosition::new(5, 10), CursorPosition::new(8, 15) ); let is_empty = selection.is_empty(); let contains = selection.contains(CursorPosition::new(6, 0)); let (start, end) = selection.normalized(); // Ensures start < end // Check overlap and merge selections let sel1 = Selection::new(CursorPosition::new(0, 0), CursorPosition::new(0, 10)); let sel2 = Selection::new(CursorPosition::new(0, 5), CursorPosition::new(0, 15)); let overlaps = sel1.overlaps(&sel2); // true if let Some(merged) = sel1.merge(&sel2) { // merged spans from (0,0) to (0,15) } ``` ## Find and Replace Search and replace functionality with support for case sensitivity, whole word matching, and regex patterns. ```rust use longcipher_leptos_components::editor::{FindState, FindOptions, FindResult}; let mut find = FindState::new(); // Configure search options find.options = FindOptions { case_sensitive: false, // Case-insensitive search whole_word: true, // Match whole words only use_regex: false, // Use literal string matching wrap_around: true, // Wrap to start when reaching end }; // Set search query and execute search find.query = "function".to_string(); let document_text = "function foo() {}\nfunction bar() {}\nfunctions list"; find.search(document_text); // Get results let count = find.match_count(); // Number of matches let has_matches = find.has_matches(); // true/false println!("Found {} matches", count); // Navigate through matches if let Some(result) = find.current_match() { println!("Current match: {} to {}", result.start, result.end); } find.next(); // Go to next match find.prev(); // Go to previous match // Replace functionality find.replacement = "fn".to_string(); // Replace current match if let Some(new_text) = find.replace_current(document_text) { println!("After replacement: {}", new_text); } // Replace all matches let replaced_all = find.replace_all(document_text); println!("All replaced: {}", replaced_all); // UI state management find.show(); // Show find panel find.show_replace(); // Show find & replace panel find.hide(); // Hide panel find.clear(); // Clear search state // Using regex patterns (requires 'find-replace' feature) find.options.use_regex = true; find.query = r"\bfn\s+\w+".to_string(); // Match function declarations find.search("fn foo() {}\nfn bar_baz() {}"); ``` ## Document Statistics Calculate text metrics including word count, character count, reading time, and markdown element counts. ```rust use longcipher_leptos_components::editor::{TextStats, DocumentStats}; let text = r#"# My Document This is a paragraph with some **bold** text and a [link](https://example.com). ## Section One - Item 1 - Item 2 - Item 3 ```rust fn main() { println!("Hello!"); } ``` Another paragraph here with more content. "#; // Basic text statistics let text_stats = TextStats::from_text(text); println!("Words: {}", text_stats.words); println!("Characters: {}", text_stats.characters); println!("Characters (no spaces): {}", text_stats.characters_no_spaces); println!("Lines: {}", text_stats.lines); println!("Paragraphs: {}", text_stats.paragraphs); println!("Summary: {}", text_stats.format_compact()); // Output: "42 words | 287 chars | 18 lines" // Comprehensive document statistics (includes markdown parsing) let doc_stats = DocumentStats::from_text(text); // Text metrics println!("Words: {}", doc_stats.text.words); println!("Lines: {}", doc_stats.text.lines); // Reading time (based on 250 WPM average) println!("Reading time: {}", doc_stats.format_reading_time()); // Output: "1 min read" // Markdown elements println!("Total headings: {}", doc_stats.heading_count); println!("H1 count: {}", doc_stats.headings_by_level[0]); println!("H2 count: {}", doc_stats.headings_by_level[1]); println!("Links: {}", doc_stats.link_count); println!("Images: {}", doc_stats.image_count); println!("Code blocks: {}", doc_stats.code_block_count); println!("List items: {}", doc_stats.list_item_count); println!("Blockquotes: {}", doc_stats.blockquote_count); println!("Tables: {}", doc_stats.table_count); ``` ## Syntax Highlighting Apply syntax highlighting to code using the `syntax-highlighting` feature. ```rust use longcipher_leptos_components::editor::{ Highlighter, Language, SyntaxConfig, HighlightedLine, HighlightedSpan }; // Detect language from file extension let lang = Language::from_extension("rs"); // Language::Rust let lang = Language::from_extension("js"); // Language::JavaScript let lang = Language::from_extension("py"); // Language::Python let lang = Language::from_extension("md"); // Language::Markdown let lang = Language::from_extension("xyz"); // Language::PlainText // Get syntax name for display let name = Language::Rust.syntax_name(); // "Rust" // Configure syntax highlighting let config = SyntaxConfig { language: Language::Rust, is_dark: true, // Use dark theme enabled: true, // Enable highlighting }; // Create highlighter and highlight code let highlighter = Highlighter::new(); let line = "let x: i32 = 42;"; let highlighted: HighlightedLine = highlighter.highlight_line(line, Language::Rust, true); // Process highlighted spans for rendering for span in &highlighted.spans { println!("Text: {}", span.text); println!("Color: {}", span.color); // e.g., "rgb(255, 128, 0)" println!("Weight: {}", span.font_weight); // "normal" or "bold" println!("Style: {}", span.font_style); // "normal" or "italic" println!("CSS: {}", span.style()); // Full CSS style string } // Create plain (unstyled) span manually let plain = HighlightedSpan::plain("some text"); // Supported languages let languages = [ Language::Rust, Language::JavaScript, Language::TypeScript, Language::Python, Language::Html, Language::Css, Language::Json, Language::Yaml, Language::Toml, Language::Markdown, Language::Sql, Language::Shell, Language::Go, Language::C, Language::Cpp, Language::Java, Language::PlainText, ]; ``` ## Code Folding Collapse and expand code regions for better navigation in large documents. ```rust use longcipher_leptos_components::editor::{ FoldState, FoldRegion, FoldKind, detect_markdown_folds, detect_heading_level }; // Automatically detect fold regions in markdown let markdown = r#"# Title Introduction paragraph. ## Section One Content for section one. ### Subsection More detailed content. ```rust fn example() { println!("code block"); } ``` ## Section Two Content for section two. "#; let mut folds = detect_markdown_folds(markdown); // Query fold regions println!("Total regions: {}", folds.region_count()); // Iterate over all fold regions for region in folds.iter() { println!( "Region {}: lines {}-{}, kind: {:?}, folded: {}", region.id, region.start_line, region.end_line, region.kind, region.is_folded ); } // Check if a line has a fold indicator if let Some(region) = folds.region_at_line(0) { println!("Line 0 starts a fold region"); } // Toggle fold at a specific line folds.toggle_at_line(0); // Returns true if fold was toggled // Check if a line is hidden due to folding let is_hidden = folds.is_line_hidden(5); // Get all fold indicators for rendering let indicators: Vec<(usize, bool)> = folds.fold_indicators(); for (line, is_folded) in indicators { println!("Line {}: {}", line, if is_folded { "folded" } else { "expanded" }); } // Bulk operations folds.fold_all(); // Collapse all regions folds.unfold_all(); // Expand all regions folds.fold_kind(FoldKind::CodeBlock); // Fold only code blocks folds.unfold_kind(FoldKind::Heading(2)); // Unfold only H2 sections // Manual fold region management let mut state = FoldState::new(); let id1 = state.add_region(10, 20, FoldKind::Heading(1)); let id2 = state.add_region_with_preview( 25, 35, FoldKind::CodeBlock, "fn main() { ... }" ); // Access regions by ID if let Some(region) = state.get_region_mut(id1) { region.toggle(); // Toggle folded state } // Detect heading level from a line let level = detect_heading_level("## Section Title"); // Some(2) let level = detect_heading_level("Not a heading"); // None // FoldKind variants let kinds = [ FoldKind::Heading(1), // H1-H6 headings FoldKind::CodeBlock, // Fenced code blocks FoldKind::List, // Ordered/unordered lists FoldKind::Blockquote, // Blockquote sections FoldKind::Indentation, // Indentation-based (JSON, YAML) FoldKind::Custom, // Custom fold markers ]; ``` ## Minimap Component Display a VS Code-style minimap for quick document navigation. ```rust use leptos::prelude::*; use longcipher_leptos_components::editor::{Minimap, MINIMAP_STYLES}; #[component] fn EditorWithMinimap() -> impl IntoView { let (content, set_content) = signal(String::from("Long document content...")); let (scroll_line, set_scroll_line) = signal(0usize); view! {
// Main editor (simplified for example)