this repo has no description
1
fork

Configure Feed

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

feat: tag filter chips in Editor and inline filter bar in Review session

- Editor: tag chips filter visible card segments (display:none preserves indices)
- Review: inline filter bar + Restart button available during active session
- Factored collect_all_tags_from_source for reactive derivation from live source signal

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

+122 -3
+122 -3
crates/tala/src/main.rs
··· 539 539 } 540 540 }); 541 541 542 + // ── Tag filter ──────────────────────────────────────────────────────────── 543 + let mut editor_selected_tags: Signal<Vec<String>> = use_signal(Vec::new); 544 + let mut editor_tag_mode = use_signal(|| TagMode::Or); 545 + let editor_all_tags = use_memo(move || collect_all_tags_from_source(&source.read())); 546 + 542 547 // ── Multi-card state ─────────────────────────────────────────────────────── 543 548 let mut active_idx = use_signal(|| 0usize); 544 549 // Indexed by segment. None = text segment (no render attempt). ··· 762 767 .unwrap_or_default() 763 768 }; 764 769 770 + let filter_tags_snap = editor_selected_tags.read().clone(); 771 + let filter_mode_snap = editor_tag_mode.read().clone(); 772 + let all_editor_tags_snap = editor_all_tags.read().clone(); 773 + 765 774 rsx! { 766 775 div { id: "editor", 767 776 // ── Toolbar ─────────────────────────────────────────────────────── ··· 789 798 if let Some(err) = &*insert_error.read() { 790 799 span { class: "insert-error", "{err}" } 791 800 } 801 + // ── Tag filter ─────────────────────────────────────────────── 802 + if !all_editor_tags_snap.is_empty() { 803 + div { class: "tag-chip-row", 804 + for tag in all_editor_tags_snap.clone() { 805 + { 806 + let tag2 = tag.clone(); 807 + let is_sel = filter_tags_snap.contains(&tag); 808 + rsx! { 809 + button { 810 + class: if is_sel { "tag-chip selected" } else { "tag-chip" }, 811 + onclick: move |_| { 812 + let mut sel = editor_selected_tags.write(); 813 + if sel.contains(&tag2) { 814 + sel.retain(|t| t != &tag2); 815 + } else { 816 + sel.push(tag2.clone()); 817 + } 818 + }, 819 + "{tag}" 820 + } 821 + } 822 + } 823 + } 824 + if filter_tags_snap.len() > 1 { 825 + button { 826 + class: if filter_mode_snap == TagMode::Or { "btn active" } else { "btn" }, 827 + onclick: move |_| editor_tag_mode.set(TagMode::Or), 828 + "OR" 829 + } 830 + button { 831 + class: if filter_mode_snap == TagMode::And { "btn active" } else { "btn" }, 832 + onclick: move |_| editor_tag_mode.set(TagMode::And), 833 + "AND" 834 + } 835 + } 836 + } 837 + } 792 838 } 793 839 // ── Card list ───────────────────────────────────────────────────── 794 840 div { class: "card-list", ··· 800 846 let is_active = i == active_i; 801 847 let row_class = if is_active { "card-row active" } else { "card-row" }; 802 848 let seg_key = if seg.kind == SegKind::Card { format!("c{i}") } else { format!("t{i}") }; 849 + let seg_visible = if filter_tags_snap.is_empty() || seg.kind != SegKind::Card { 850 + true 851 + } else { 852 + let seg_tags = tala_format::parse_cards(&text_snap[seg.start..seg.end]) 853 + .into_iter() 854 + .next() 855 + .map(|c| c.tags) 856 + .unwrap_or_default(); 857 + match filter_mode_snap { 858 + TagMode::And => filter_tags_snap.iter().all(|t| seg_tags.contains(t)), 859 + TagMode::Or => filter_tags_snap.iter().any(|t| seg_tags.contains(t)), 860 + } 861 + }; 803 862 804 863 // Render data — naturally None for text segments (previews stores None for them). 805 864 let card_result: Option<Result<PreviewData, String>> = pvs_snap.get(i).cloned().flatten(); ··· 850 909 div { 851 910 key: "{seg_key}", 852 911 class: row_class, 912 + style: if !seg_visible { "display:none" } else { "" }, 853 913 onclick: move |_| { 854 914 if !is_active { 855 915 active_idx.set(i); ··· 2350 2410 } 2351 2411 } 2352 2412 2353 - fn collect_all_tags() -> Vec<String> { 2354 - let source = std::fs::read_to_string(cards_path()).unwrap_or_default(); 2355 - let cards = tala_format::parse_cards(&source); 2413 + fn collect_all_tags_from_source(source: &str) -> Vec<String> { 2414 + let cards = tala_format::parse_cards(source); 2356 2415 let mut tags: Vec<String> = cards.into_iter().flat_map(|c| c.tags).collect(); 2357 2416 tags.sort(); 2358 2417 tags.dedup(); 2359 2418 tags 2419 + } 2420 + 2421 + fn collect_all_tags() -> Vec<String> { 2422 + let source = std::fs::read_to_string(cards_path()).unwrap_or_default(); 2423 + collect_all_tags_from_source(&source) 2360 2424 } 2361 2425 2362 2426 #[component] ··· 2525 2589 let q_src = q_img.read(); 2526 2590 let a_src = a_img.read(); 2527 2591 let has_answer = matches!(a_src.as_ref(), Some(Some(_))); 2592 + let session_tags_snap = selected_tags.read().clone(); 2593 + let session_tag_mode_snap = tag_mode.read().clone(); 2594 + let session_all_tags_snap = all_tags.read().clone(); 2528 2595 2529 2596 rsx! { 2530 2597 div { ··· 2565 2632 } 2566 2633 _ => {} 2567 2634 }, 2635 + // ── Inline filter bar ──────────────────────────────────────────── 2636 + if !session_all_tags_snap.is_empty() { 2637 + div { class: "tag-chip-row", 2638 + for tag in session_all_tags_snap.clone() { 2639 + { 2640 + let tag2 = tag.clone(); 2641 + let is_sel = session_tags_snap.contains(&tag); 2642 + rsx! { 2643 + button { 2644 + class: if is_sel { "tag-chip selected" } else { "tag-chip" }, 2645 + onclick: move |_| { 2646 + let mut sel = selected_tags.write(); 2647 + if sel.contains(&tag2) { 2648 + sel.retain(|t| t != &tag2); 2649 + } else { 2650 + sel.push(tag2.clone()); 2651 + } 2652 + }, 2653 + "{tag}" 2654 + } 2655 + } 2656 + } 2657 + } 2658 + if session_tags_snap.len() > 1 { 2659 + button { 2660 + class: if session_tag_mode_snap == TagMode::Or { "btn active" } else { "btn" }, 2661 + onclick: move |_| tag_mode.set(TagMode::Or), 2662 + "OR" 2663 + } 2664 + button { 2665 + class: if session_tag_mode_snap == TagMode::And { "btn active" } else { "btn" }, 2666 + onclick: move |_| tag_mode.set(TagMode::And), 2667 + "AND" 2668 + } 2669 + } 2670 + button { 2671 + class: "btn", 2672 + onclick: move |_| { 2673 + let tags = selected_tags.read().clone(); 2674 + let mode = tag_mode.read().clone(); 2675 + let (queue, sidecar) = build_review_queue(&tags, mode); 2676 + session.set(ReviewSession { queue, sidecar }); 2677 + idx.set(0); 2678 + revealed.set(false); 2679 + show_answer.set(false); 2680 + done.set(false); 2681 + graded_count.set(0); 2682 + }, 2683 + "Restart \u{2192}" 2684 + } 2685 + } 2686 + } 2568 2687 span { class: "review-progress", "{cur_idx + 1} / {total}" } 2569 2688 if let Some(q) = q_src.as_ref().filter(|s| !s.is_empty()) { 2570 2689 div {