diff --git a/crates/mantra-ui/Cargo.toml b/crates/mantra-ui/Cargo.toml
index fa80baa..017aec3 100644
--- a/crates/mantra-ui/Cargo.toml
+++ b/crates/mantra-ui/Cargo.toml
@@ -26,6 +26,12 @@ web-sys = { version = "0.3", optional = true, features = [
"DomTokenList",
"Event",
"EventTarget",
+ "Selection",
+ "Range",
+ "Node",
+ "DomRect",
+ "Navigator",
+ "Clipboard",
"console",
] }
# Server-only — pulled in under ssr feature for notes I/O + Claude HTTP.
diff --git a/crates/mantra-ui/src/pages/artifact.rs b/crates/mantra-ui/src/pages/artifact.rs
index 15fdce4..b6a2692 100644
--- a/crates/mantra-ui/src/pages/artifact.rs
+++ b/crates/mantra-ui/src/pages/artifact.rs
@@ -1,9 +1,4 @@
//! `/artifact/:slug` — closing document of a cycle.
-//!
-//! Currently renders: manifest.md (cycle 1) and applied-principles.md
-//! (cycle 2). Same book-grade typography as the source page, same
-//! paragraph-id → margin drawer wiring so readers can annotate the
-//! artifacts too.
use leptos::prelude::*;
use leptos_router::hooks::{use_params, use_query_map};
@@ -11,7 +6,7 @@ use leptos_router::params::Params;
use crate::api::{fetch_artifact, ArtifactPageData};
use crate::corpus::Lang;
-use crate::pages::margin::{ActivePara, MarginDrawer};
+use crate::pages::inspector::{Anchor, Inspector, PopoverPos, SelectionPopover, Tab};
use crate::pages::shared::LangToggle;
#[derive(Params, PartialEq, Clone, Debug)]
@@ -31,12 +26,20 @@ pub fn ArtifactPage() -> impl IntoView {
Lang::from_query(query.read().get("lang").as_deref())
});
- let (active, set_active) = signal::
}>
- {move || {
- data.get().map(|res| {
- let lang_val = lang.get();
- let home_href = format!("/?lang={}", lang_val.as_str());
- let label_back = match lang_val {
- Lang::Ru => "все источники",
- Lang::En => "all sources",
- };
+
+
"…" }>
+ {move || {
+ data.get().map(|res| {
+ let lang_val = lang.get();
+ let home_href = format!("/?lang={}", lang_val.as_str());
+ let label_back = match lang_val {
+ Lang::Ru => "все источники",
+ Lang::En => "all sources",
+ };
- match res {
- Ok(Some(ArtifactPageData { artifact })) => view! {
-
-
+ match res {
+ Ok(Some(ArtifactPageData { artifact })) => view! {
+
+
-
+ on:click=move |ev| {
+ if popover_pos.get_untracked().is_some() {
+ return;
+ }
+ if let Some(a) = paragraph_from_event(&ev) {
+ set_anchor.set(Some(a));
+ }
+ }
+ >
-
-
- }.into_any(),
- Ok(None) => view! {
-
- "artifact not found"
- "← back"
-
- }.into_any(),
- Err(e) => view! {
-
- {format!("{e}")}
-
- }.into_any(),
- }
- })
- }}
-
+
+
+ }.into_any(),
+ Ok(None) => view! {
+
+ "artifact not found"
+ "← back"
+
+ }.into_any(),
+ Err(e) => view! {
+
+ {format!("{e}")}
+
+ }.into_any(),
+ }
+ })
+ }}
+
-
+
+
+
}
}
#[cfg(feature = "hydrate")]
-fn paragraph_from_event(ev: &leptos::ev::MouseEvent) -> Option {
+fn paragraph_from_event(ev: &leptos::ev::MouseEvent) -> Option {
use wasm_bindgen::JsCast;
let target = ev.target()?;
let mut el = target.dyn_into::().ok()?;
@@ -131,14 +152,77 @@ fn paragraph_from_event(ev: &leptos::ev::MouseEvent) -> Option {
let id = el.get_attribute("data-para-id")?;
let text = el.text_content().unwrap_or_default();
let excerpt = text.trim().to_string();
- return Some(ActivePara { id, excerpt });
+ return Some(Anchor {
+ para_id: id,
+ excerpt,
+ is_selection: false,
+ });
}
el = el.parent_element()?;
}
}
#[cfg(not(feature = "hydrate"))]
-fn paragraph_from_event(_ev: &leptos::ev::MouseEvent) -> Option {
+fn paragraph_from_event(_ev: &leptos::ev::MouseEvent) -> Option {
+ None
+}
+
+#[cfg(feature = "hydrate")]
+fn handle_mouseup(
+ _ev: &leptos::ev::MouseEvent,
+ set_pos: &WriteSignal