···66use dioxus::prelude::*;
7788use super::document::{EditorDocument, Selection};
99-use super::offset_map::{find_nearest_valid_position, is_valid_cursor_position, SnapDirection};
99+use super::offset_map::{SnapDirection, find_nearest_valid_position, is_valid_cursor_position};
1010use super::paragraph::ParagraphRender;
11111212/// Sync internal cursor and selection state from browser DOM selection.
···7272 let focus_offset = selection.focus_offset() as usize;
73737474 // Convert both DOM positions to rope offsets using cached paragraphs
7575- let anchor_rope = dom_position_to_rope_offset(
7575+ let anchor_rope = dom_position_to_text_offset(
7676 &dom_document,
7777 &editor_element,
7878 &anchor_node,
···8080 paragraphs,
8181 direction_hint,
8282 );
8383- let focus_rope = dom_position_to_rope_offset(
8383+ let focus_rope = dom_position_to_text_offset(
8484 &dom_document,
8585 &editor_element,
8686 &focus_node,
···114114/// The `direction_hint` is used when snapping from invisible content to determine
115115/// which direction to prefer.
116116#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
117117-fn dom_position_to_rope_offset(
117117+pub fn dom_position_to_text_offset(
118118 dom_document: &web_sys::Document,
119119 editor_element: &web_sys::Element,
120120 node: &web_sys::Node,
···215215 // No mapping found - try to find any valid position in paragraphs
216216 // This handles clicks on non-text elements like images
217217 for para in paragraphs {
218218- if let Some(snapped) = find_nearest_valid_position(¶.offset_map, para.char_range.start, direction_hint) {
218218+ if let Some(snapped) =
219219+ find_nearest_valid_position(¶.offset_map, para.char_range.start, direction_hint)
220220+ {
219221 return Some(snapped.char_offset());
220222 }
221223 }
+2
crates/weaver-app/src/components/editor/mod.rs
···44//! characters are hidden contextually based on cursor position, while still
55//! editing plain markdown text under the hood.
6677+mod actions;
88+mod beforeinput;
79mod component;
810mod cursor;
911mod document;
+31-1
crates/weaver-app/src/components/editor/writer.rs
···13221322 self.record_mapping(range.clone(), char_start..char_end);
13231323 self.last_char_offset = char_end;
13241324 }
13251325- SoftBreak => self.write_newline()?,
13251325+ SoftBreak => {
13261326+ // Emit <br> for visual line break, plus a space for cursor positioning.
13271327+ // This space maps to the \n so the cursor can land here when navigating.
13281328+ let char_start = self.last_char_offset;
13291329+13301330+ // Emit <br>
13311331+ self.write("<br />")?;
13321332+ self.current_node_child_count += 1;
13331333+13341334+ // Emit space for cursor positioning - this gives the browser somewhere
13351335+ // to place the cursor when navigating to this line
13361336+ self.write(" ")?;
13371337+ self.current_node_child_count += 1;
13381338+13391339+ // Map the space to the newline position - cursor landing here means
13401340+ // we're at the end of the line (after the \n)
13411341+ if let Some(ref node_id) = self.current_node_id {
13421342+ let mapping = OffsetMapping {
13431343+ byte_range: range.clone(),
13441344+ char_range: char_start..char_start + 1,
13451345+ node_id: node_id.clone(),
13461346+ char_offset_in_node: self.current_node_char_offset,
13471347+ child_index: None,
13481348+ utf16_len: 1, // the space we emitted
13491349+ };
13501350+ self.offset_maps.push(mapping);
13511351+ self.current_node_char_offset += 1;
13521352+ }
13531353+13541354+ self.last_char_offset = char_start + 1; // +1 for the \n
13551355+ }
13261356 HardBreak => {
13271357 // Emit the two spaces as visible (dimmed) text, then <br>
13281358 let gap = &self.source[range.clone()];