this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: images delete button; fix editor empty-state & ghost char bug

- Images: add Delete button per tile with confirm dialog; warns if N
cards reference the image via extract_img_names
- Editor: replace static "no cards" message with a live textarea so
new cards can be typed directly into the empty state
- Editor: fix ghost character reappearing after tab switch — on_input
was early-returning on empty value, leaving stale content in source
that onmounted would restore on next mount

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+70 -6
+1
crates/tala/assets/main.css
··· 210 210 211 211 .btn:hover { color: #fff; background: #252a3d; } 212 212 .btn.active { color: #fff; background: #3a4070; border-color: #5060a0; } 213 + .btn.btn-danger:hover { color: #e06c75; background: #2a1e22; border-color: #6a2a30; } 213 214 214 215 .card-preview { 215 216 display: block;
+33 -4
crates/tala/src/editor.rs
··· 627 627 // ── Source & save ───────────────────────────────────────────────────────── 628 628 let mut source = use_signal(|| std::fs::read_to_string(cards_path()).unwrap_or_default()); 629 629 let mut save_status = use_signal(|| SaveStatus::Clean); 630 + let mut new_card_draft = use_signal(|| String::new()); 630 631 631 632 // Auto-save: debounce 1s after last edit. 632 633 let _saver = use_resource(move || async move { ··· 690 691 let pvs = previews.read().clone(); // subscribe 691 692 let text = source.peek().clone(); // no subscription 692 693 let segs = make_segments(&text); 694 + if segs.is_empty() { 695 + blank_rects_sig.set(vec![]); 696 + glyph_map_sig.set(vec![]); 697 + math_spans_sig.set(vec![]); 698 + image_boxes_sig.set(vec![]); 699 + return; 700 + } 693 701 let ai = idx.min(segs.len().saturating_sub(1)); 694 702 let card_idx = segs[..=ai] 695 703 .iter() ··· 794 802 // spliced in and active_idx advances to the last one. 795 803 let on_input = move |e: FormEvent| { 796 804 let new_frag = e.value(); 797 - if new_frag.trim().is_empty() { 798 - return; 799 - } 800 805 let cur = source.read().clone(); 801 806 let segs = make_segments(&cur); 802 807 let ai = (*active_idx.read()).min(segs.len().saturating_sub(1)); ··· 829 834 let text_snap = source.read().clone(); 830 835 let segs_snap = make_segments(&text_snap); 831 836 let n_segs = segs_snap.len(); 837 + if n_segs == 0 && !new_card_draft.read().is_empty() { 838 + new_card_draft.set(String::new()); 839 + } 832 840 let active_i = (*active_idx.read()).min(n_segs.saturating_sub(1)); 833 841 let active_is_cloze = match segs_snap.get(active_i) { 834 842 Some(Seg { ··· 940 948 // ── Card list ───────────────────────────────────────────────────── 941 949 div { class: "card-list", 942 950 if n_segs == 0 { 943 - p { class: "status", "No cards yet — start with #cloze[...] or #card[...][...]" } 951 + div { class: "card-row active", 952 + textarea { 953 + class: "card-source", 954 + spellcheck: "false", 955 + placeholder: "#cloze[...] or #card[...][...]", 956 + onmounted: move |_| { 957 + spawn(async move { 958 + document::eval( 959 + "setTimeout(()=>document.querySelector('.card-list .card-row.active textarea')?.focus(),0)" 960 + ).await.ok(); 961 + }); 962 + }, 963 + oninput: move |e| { 964 + let val = e.value(); 965 + new_card_draft.set(val.clone()); 966 + if !val.trim().is_empty() { 967 + source.set(val); 968 + save_status.set(SaveStatus::Dirty); 969 + } 970 + }, 971 + } 972 + } 944 973 } 945 974 { 946 975 segs_snap.iter().enumerate().map(|(i, seg)| {
+36 -2
crates/tala/src/images.rs
··· 5 5 use dioxus::prelude::*; 6 6 use tala_srs::{CardSchedule, Sidecar}; 7 7 8 - use crate::util::{card_dir, cards_path}; 8 + use crate::util::{card_dir, cards_path, extract_img_names}; 9 9 10 10 11 11 fn list_images(dir: &Path) -> Vec<PathBuf> { ··· 49 49 on_renamed: move |_| { 50 50 files.set(list_images(&card_dir().join("images"))); 51 51 }, 52 + on_deleted: move |_| { 53 + files.set(list_images(&card_dir().join("images"))); 54 + }, 52 55 on_view: move |uri: String| lightbox.set(Some(uri)), 53 56 } 54 57 } ··· 71 74 } 72 75 73 76 #[component] 74 - fn ImageTile(path: PathBuf, on_renamed: EventHandler<()>, on_view: EventHandler<String>) -> Element { 77 + fn ImageTile(path: PathBuf, on_renamed: EventHandler<()>, on_deleted: EventHandler<()>, on_view: EventHandler<String>) -> Element { 75 78 let stem = path 76 79 .file_stem() 77 80 .map(|s| s.to_string_lossy().into_owned()) ··· 191 194 } 192 195 }, 193 196 "Rename" 197 + } 198 + button { 199 + class: "btn btn-danger", 200 + onclick: { 201 + let path = path.clone(); 202 + let stem = stem.clone(); 203 + move |_| { 204 + let path = path.clone(); 205 + let stem = stem.clone(); 206 + spawn(async move { 207 + let ref_count = std::fs::read_to_string(cards_path()) 208 + .map(|src| extract_img_names(&src).into_iter().filter(|n| n == &stem).count()) 209 + .unwrap_or(0); 210 + let msg = if ref_count > 0 { 211 + format!( 212 + "Delete {}?\\nWarning: {} card{} reference this image.", 213 + stem, ref_count, if ref_count == 1 { "" } else { "s" } 214 + ) 215 + } else { 216 + format!("Delete {}?", stem) 217 + }; 218 + let js = format!("dioxus.send(window.confirm({:?}))", msg); 219 + let confirmed = document::eval(&js).recv::<bool>().await.unwrap_or(false); 220 + if confirmed { 221 + let _ = std::fs::remove_file(&path); 222 + on_deleted.call(()); 223 + } 224 + }); 225 + } 226 + }, 227 + "Delete" 194 228 } 195 229 } 196 230 }