this repo has no description
1
fork

Configure Feed

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

feat: routing skeleton, zoom keybinding, cloze blank highlight

Add Home/Editor/Settings routes under a persistent navbar shell.
Inject Ctrl+/-/0 zoom via sessionStorage-backed JS. Highlight
#blank[] content with yellow box in authoring preamble.

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

+156 -98
+1 -1
crates/tala-typst/src/lib.rs
··· 76 76 sides.at(1) 77 77 } 78 78 } 79 - #let blank(..args) = args.pos().at(0, default: []) 79 + #let blank(..args) = box(fill: rgb(255, 220, 50), inset: (x: 3pt, y: 2pt), radius: 2pt, baseline: 20%)[#args.pos().at(0, default: [])] 80 80 #let cloze(id: "", ..args) = args.pos().at(0, default: []) 81 81 #let img_cloze(id: "", src: "") = image("images/" + src + ".png") 82 82 #let img(name) = image("images/" + name + ".png")
+65 -67
crates/tala/assets/main.css
··· 1 - /* App-wide styling */ 1 + /* ── Reset ───────────────────────────────────────────────────────────────── */ 2 + *, *::before, *::after { box-sizing: border-box; } 3 + 2 4 body { 3 5 background-color: #0f1116; 4 6 color: #ffffff; ··· 6 8 margin: 0; 7 9 height: 100vh; 8 10 overflow: hidden; 11 + display: flex; 12 + flex-direction: column; 13 + } 14 + 15 + /* ── Navbar ──────────────────────────────────────────────────────────────── */ 16 + #navbar { 17 + display: flex; 18 + flex-direction: row; 19 + align-items: center; 20 + gap: 4px; 21 + padding: 0 12px; 22 + height: 36px; 23 + flex-shrink: 0; 24 + background: #0a0c11; 25 + border-bottom: 1px solid #1e2130; 26 + } 27 + 28 + #navbar a { 29 + color: #8892aa; 30 + text-decoration: none; 31 + font-size: 13px; 32 + padding: 4px 10px; 33 + border-radius: 4px; 34 + transition: color 0.15s, background 0.15s; 35 + } 36 + 37 + #navbar a:hover { 38 + color: #ffffff; 39 + background: #1e2130; 9 40 } 10 41 42 + /* ── Page container (fills space below navbar) ───────────────────────────── */ 43 + #home, 44 + #settings { 45 + padding: 32px 40px; 46 + flex: 1; 47 + overflow-y: auto; 48 + } 49 + 50 + #home h2, #settings h2 { 51 + margin-top: 0; 52 + font-size: 20px; 53 + color: #c8d0e8; 54 + } 55 + 56 + #home p, #settings p { 57 + color: #8892aa; 58 + font-size: 14px; 59 + } 60 + 61 + #home a { 62 + color: #7aa2f7; 63 + text-decoration: none; 64 + font-size: 14px; 65 + } 66 + 67 + #home a:hover { text-decoration: underline; } 68 + 69 + /* ── Editor ──────────────────────────────────────────────────────────────── */ 11 70 #editor { 12 71 display: flex; 13 72 flex-direction: row; 14 - height: 100vh; 73 + flex: 1; 74 + min-height: 0; 15 75 } 16 76 17 77 #source-pane { ··· 24 84 #divider { 25 85 width: 5px; 26 86 flex-shrink: 0; 27 - background: #2a2d36; 87 + background: #1e2130; 28 88 cursor: col-resize; 29 89 transition: background 0.15s; 30 90 } 31 91 32 - #divider:hover { 33 - background: #4a5080; 34 - } 92 + #divider:hover { background: #4a5080; } 35 93 36 94 #card-source { 37 95 flex: 1; ··· 53 111 justify-content: center; 54 112 background: #13151c; 55 113 padding: 24px; 114 + overflow: auto; 56 115 } 57 116 58 117 .card-preview { ··· 74 133 word-break: break-word; 75 134 max-width: 100%; 76 135 } 77 - 78 - #hero { 79 - margin: 0; 80 - display: flex; 81 - flex-direction: column; 82 - justify-content: center; 83 - align-items: center; 84 - } 85 - 86 - #links { 87 - width: 400px; 88 - text-align: left; 89 - font-size: x-large; 90 - color: white; 91 - display: flex; 92 - flex-direction: column; 93 - } 94 - 95 - #links a { 96 - color: white; 97 - text-decoration: none; 98 - margin-top: 20px; 99 - margin: 10px 0px; 100 - border: white 1px solid; 101 - border-radius: 5px; 102 - padding: 10px; 103 - } 104 - 105 - #links a:hover { 106 - background-color: #1f1f1f; 107 - cursor: pointer; 108 - } 109 - 110 - /* Navbar */ 111 - #navbar { 112 - display: flex; 113 - flex-direction: row; 114 - } 115 - 116 - #navbar a { 117 - color: #ffffff; 118 - margin-right: 20px; 119 - text-decoration: none; 120 - transition: color 0.2s ease; 121 - } 122 - 123 - #navbar a:hover { 124 - cursor: pointer; 125 - color: #91a4d2; 126 - } 127 - 128 - /* Blog page */ 129 - #blog { 130 - margin-top: 50px; 131 - } 132 - 133 - #blog a { 134 - color: #ffffff; 135 - margin-top: 50px; 136 - } 137 -
+90 -30
crates/tala/src/main.rs
··· 8 8 const FAVICON: Asset = asset!("/assets/favicon.ico"); 9 9 const MAIN_CSS: Asset = asset!("/assets/main.css"); 10 10 11 - fn main() { 12 - dioxus::launch(App); 13 - } 14 - 15 - #[component] 16 - fn App() -> Element { 17 - rsx! { 18 - document::Title { "tala" } 19 - document::Link { rel: "icon", href: FAVICON } 20 - document::Link { rel: "stylesheet", href: MAIN_CSS } 21 - CardEditor {} 22 - } 23 - } 11 + // Ctrl+/- zoom and Ctrl+0 reset, persisted in sessionStorage. 12 + const ZOOM_JS: &str = r#" 13 + (function() { 14 + var STEP = 0.1, MIN = 0.5, MAX = 3.0; 15 + var zoom = parseFloat(sessionStorage.getItem('zoom') || '1'); 16 + document.documentElement.style.zoom = zoom; 17 + document.addEventListener('keydown', function(e) { 18 + if (!e.ctrlKey) return; 19 + if (e.key === '=' || e.key === '+') { 20 + e.preventDefault(); 21 + zoom = Math.min(MAX, parseFloat((zoom + STEP).toFixed(1))); 22 + } else if (e.key === '-') { 23 + e.preventDefault(); 24 + zoom = Math.max(MIN, parseFloat((zoom - STEP).toFixed(1))); 25 + } else if (e.key === '0') { 26 + e.preventDefault(); 27 + zoom = 1; 28 + } else { return; } 29 + document.documentElement.style.zoom = zoom; 30 + sessionStorage.setItem('zoom', zoom); 31 + }); 32 + })(); 33 + "#; 24 34 25 35 const DIVIDER_DRAG_JS: &str = r#" 26 36 (function() { ··· 44 54 })(); 45 55 "#; 46 56 57 + #[derive(Debug, Clone, Routable, PartialEq)] 58 + #[rustfmt::skip] 59 + enum Route { 60 + #[layout(Shell)] 61 + #[route("/")] 62 + Home {}, 63 + #[route("/editor")] 64 + Editor {}, 65 + #[route("/settings")] 66 + Settings {}, 67 + } 68 + 69 + fn main() { 70 + dioxus::launch(App); 71 + } 72 + 47 73 #[component] 48 - fn CardEditor() -> Element { 74 + fn App() -> Element { 75 + rsx! { 76 + document::Title { "tala" } 77 + document::Link { rel: "icon", href: FAVICON } 78 + document::Link { rel: "stylesheet", href: MAIN_CSS } 79 + document::Script { {ZOOM_JS} } 80 + Router::<Route> {} 81 + } 82 + } 83 + 84 + /// Persistent shell: navbar + page content. 85 + #[component] 86 + fn Shell() -> Element { 87 + rsx! { 88 + nav { id: "navbar", 89 + Link { to: Route::Home {}, "Home" } 90 + Link { to: Route::Editor {}, "Editor" } 91 + Link { to: Route::Settings {}, "Settings" } 92 + } 93 + Outlet::<Route> {} 94 + } 95 + } 96 + 97 + #[component] 98 + fn Home() -> Element { 99 + rsx! { 100 + div { id: "home", 101 + h2 { "tala" } 102 + p { "Select a deck to begin." } 103 + Link { to: Route::Editor {}, "Open editor →" } 104 + } 105 + } 106 + } 107 + 108 + #[component] 109 + fn Settings() -> Element { 110 + rsx! { 111 + div { id: "settings", 112 + h2 { "Settings" } 113 + p { "Nothing here yet." } 114 + } 115 + } 116 + } 117 + 118 + #[component] 119 + fn Editor() -> Element { 49 120 let mut source = use_signal(|| { 50 - r#"#card(id: "example")[What is $E = m c^2$?][Einstein's mass--energy relation.]"# 121 + r#"#cloze(id: "example")[The #blank[Gaussian integral] is $integral_(-infinity)^(infinity) e^(-x^2) dif x = sqrt(pi)$]"# 51 122 .to_string() 52 123 }); 53 124 54 - // Re-runs whenever `source` changes. Reads signal before first `.await` so 55 - // reactivity is established. The sleep acts as a debounce. 56 125 let preview = use_resource(move || async move { 57 126 let text = source.read().clone(); 58 127 tokio::time::sleep(Duration::from_millis(300)).await; ··· 73 142 } 74 143 div { 75 144 id: "divider", 76 - onmousedown: move |_| { 77 - document::eval(DIVIDER_DRAG_JS); 78 - }, 145 + onmousedown: move |_| { document::eval(DIVIDER_DRAG_JS); }, 79 146 } 80 147 div { id: "preview-pane", 81 148 match &*preview.read() { 82 - None => rsx! { 83 - span { class: "status", "Rendering…" } 84 - }, 149 + None => rsx! { span { class: "status", "Rendering…" } }, 85 150 Some(Ok(b64)) => { 86 151 let src = format!("data:image/png;base64,{b64}"); 87 - rsx! { 88 - img { class: "card-preview", src: "{src}" } 89 - } 152 + rsx! { img { class: "card-preview", src: "{src}" } } 90 153 } 91 - Some(Err(msg)) => rsx! { 92 - pre { class: "render-error", "{msg}" } 93 - }, 154 + Some(Err(msg)) => rsx! { pre { class: "render-error", "{msg}" } }, 94 155 } 95 156 } 96 157 } ··· 102 163 let result = tala_typst::render(&deck_dir, source, tala_typst::Preamble::Authoring) 103 164 .map_err(|e| e.to_string())?; 104 165 105 - // Premultiplied RGBA → straight alpha. 106 166 let straight: Vec<u8> = result 107 167 .rgba 108 168 .chunks_exact(4)