fixed lexicon resolution, styling

Orual 9538a97b d8f38532

+153 -62
-8
Cargo.lock
··· 4079 4079 [[package]] 4080 4080 name = "jacquard" 4081 4081 version = "0.9.0" 4082 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4083 4082 dependencies = [ 4084 4083 "bytes", 4085 4084 "getrandom 0.2.16", ··· 4109 4108 [[package]] 4110 4109 name = "jacquard-api" 4111 4110 version = "0.9.0" 4112 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4113 4111 dependencies = [ 4114 4112 "bon", 4115 4113 "bytes", ··· 4127 4125 [[package]] 4128 4126 name = "jacquard-axum" 4129 4127 version = "0.9.0" 4130 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4131 4128 dependencies = [ 4132 4129 "axum", 4133 4130 "bytes", ··· 4149 4146 [[package]] 4150 4147 name = "jacquard-common" 4151 4148 version = "0.9.0" 4152 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4153 4149 dependencies = [ 4154 4150 "base64 0.22.1", 4155 4151 "bon", ··· 4192 4188 [[package]] 4193 4189 name = "jacquard-derive" 4194 4190 version = "0.9.0" 4195 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4196 4191 dependencies = [ 4197 4192 "heck 0.5.0", 4198 4193 "jacquard-lexicon", ··· 4204 4199 [[package]] 4205 4200 name = "jacquard-identity" 4206 4201 version = "0.9.1" 4207 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4208 4202 dependencies = [ 4209 4203 "bon", 4210 4204 "bytes", ··· 4232 4226 [[package]] 4233 4227 name = "jacquard-lexicon" 4234 4228 version = "0.9.1" 4235 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4236 4229 dependencies = [ 4237 4230 "cid", 4238 4231 "dashmap", ··· 4258 4251 [[package]] 4259 4252 name = "jacquard-oauth" 4260 4253 version = "0.9.0" 4261 - source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65" 4262 4254 dependencies = [ 4263 4255 "base64 0.22.1", 4264 4256 "bytes",
+12 -12
Cargo.toml
··· 40 40 markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" } 41 41 markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" } 42 42 43 - jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } 44 - jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 45 - jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 46 - jacquard-axum = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 47 - jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 48 - jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false } 43 + # jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } 44 + # jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 45 + # jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 46 + # jacquard-axum = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 47 + # jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" } 48 + # jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false } 49 49 50 - # jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } 51 - # jacquard-api = { path = "../jacquard/crates/jacquard-api" } 52 - # jacquard-common = { path = "../jacquard/crates/jacquard-common" } 53 - # jacquard-axum = {path = "../jacquard/crates/jacquard-axum" } 54 - # jacquard-derive = { path = "../jacquard/crates/jacquard-derive" } 55 - # jacquard-lexicon = { path = "../jacquard/crates/jacquard-lexicon", default-features = false } 50 + jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] } 51 + jacquard-api = { path = "../jacquard/crates/jacquard-api" } 52 + jacquard-common = { path = "../jacquard/crates/jacquard-common" } 53 + jacquard-axum = {path = "../jacquard/crates/jacquard-axum" } 54 + jacquard-derive = { path = "../jacquard/crates/jacquard-derive" } 55 + jacquard-lexicon = { path = "../jacquard/crates/jacquard-lexicon", default-features = false } 56 56 57 57 [profile] 58 58
+1 -2
crates/weaver-app/Cargo.toml
··· 20 20 dashmap = "6.1.0" 21 21 22 22 dioxus = { version = "0.7.1", features = ["router"] } 23 + #dioxus-router = { version = "0.7.1", features = ["wasm-split"] } 23 24 weaver-common = { path = "../weaver-common" } 24 25 jacquard = { workspace = true, features = ["streaming"] } 25 26 jacquard-lexicon = { workspace = true } ··· 48 49 serde_html_form = "0.2.8" 49 50 webbrowser = "1.0.6" 50 51 tracing.workspace = true 51 - 52 - 53 52 54 53 55 54 [target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
+30 -7
crates/weaver-app/assets/styling/record-view.css
··· 1 1 .record-view-container { 2 - font-family: var(--font-mono); 3 2 max-width: 1200px; 4 3 margin: 2rem auto; 5 4 padding: 0 1rem; ··· 8 7 .record-header { 9 8 margin-bottom: 2rem; 10 9 padding-bottom: 0.5rem; 10 + 11 + font-family: var(--font-mono); 11 12 border-bottom: 1px solid var(--color-border); 12 13 } 13 14 ··· 62 63 } 63 64 64 65 .metadata-label { 66 + font-family: var(--font-mono); 65 67 color: var(--color-subtle); 66 68 font-size: 0.85rem; 67 69 text-transform: uppercase; ··· 612 614 border-bottom-color: var(--color-error, #ff6b6b); 613 615 } 614 616 615 - .record-field input[type="checkbox"] { 616 - width: 1.1rem; 617 - height: 1.1rem; 618 - margin-top: 0.3rem; 619 - margin-bottom: 0.3rem; 617 + .boolean-toggle { 618 + font-family: var(--font-mono); 619 + font-size: 0.9rem; 620 + color: var(--color-text); 621 + background: var(--color-surface); 622 + border: 1px solid var(--color-border); 623 + padding: 0.25rem 0.25rem; 624 + margin-right: 0.5rem; 625 + margin-bottom: 0.2rem; 620 626 cursor: pointer; 621 - accent-color: var(--color-primary); 627 + transition: 628 + background-color 0.2s, 629 + border-color 0.2s; 630 + } 631 + 632 + .boolean-toggle-false { 633 + color: var(--color-error); 634 + border: 1px solid var(--color-error); 635 + } 636 + 637 + .boolean-toggle-true { 638 + border: 1px solid var(--color-success); 639 + } 640 + 641 + .boolean-toggle:hover { 642 + border: 1px solid var(--color-primary); 643 + background-color: var(--color-primary); 644 + color: var(--color-surface); 622 645 } 623 646 624 647 .field-error {
+1 -1
crates/weaver-app/src/components/login.rs
··· 37 37 use crate::Route; 38 38 use gloo_storage::Storage; 39 39 let full_route = use_route::<Route>(); 40 - gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)); 40 + gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok(); 41 41 } 42 42 43 43 use_effect(move || {
+3 -3
crates/weaver-app/src/data.rs
··· 8 8 use dioxus::prelude::*; 9 9 #[cfg(feature = "fullstack-server")] 10 10 #[allow(unused_imports)] 11 - use dioxus::{fullstack::extract::Extension, CapturedError}; 11 + use dioxus::{CapturedError, fullstack::extract::Extension}; 12 12 use jacquard::types::{did::Did, string::Handle}; 13 13 #[allow(unused_imports)] 14 14 use jacquard::{ ··· 18 18 }; 19 19 #[allow(unused_imports)] 20 20 use std::sync::Arc; 21 - use weaver_api::sh_weaver::notebook::{entry::Entry, BookEntryView}; 21 + use weaver_api::sh_weaver::notebook::{BookEntryView, entry::Entry}; 22 22 // ============================================================================ 23 23 // Wrapper Hooks (feature-gated) 24 24 // ============================================================================ ··· 216 216 async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> String { 217 217 use n0_future::stream::StreamExt; 218 218 use weaver_renderer::{ 219 - atproto::{ClientContext, ClientWriter}, 220 219 ContextIterator, NotebookProcessor, 220 + atproto::{ClientContext, ClientWriter}, 221 221 }; 222 222 223 223 let ctx = ClientContext::<()>::new(content.clone(), did);
+1 -1
crates/weaver-app/src/main.rs
··· 65 65 #[layout(RecordIndex)] 66 66 #[route("/:..uri")] 67 67 RecordView { uri: Vec<String> }, 68 - #[end_layout] 68 + #[end_layout] 69 69 #[end_nest] 70 70 #[route("/callback?:state&:iss&:code")] 71 71 Callback { state: SmolStr, iss: SmolStr, code: SmolStr },
-1
crates/weaver-app/src/service_worker.rs
··· 3 3 4 4 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 5 5 use wasm_bindgen_futures::JsFuture; 6 - 7 6 #[cfg(all(target_family = "wasm", target_os = "unknown"))] 8 7 use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window}; 9 8
+105 -27
crates/weaver-app/src/views/record.rs
··· 6 6 use humansize::format_size; 7 7 use jacquard::api::com_atproto::repo::get_record::GetRecordOutput; 8 8 use jacquard::client::AgentError; 9 + use jacquard::common::to_data; 9 10 use jacquard::prelude::*; 10 11 use jacquard::smol_str::ToSmolStr; 11 12 use jacquard::{ ··· 14 15 identity::lexicon_resolver::LexiconSchemaResolver, 15 16 types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid}, 16 17 }; 18 + use jacquard_lexicon::lexicon::LexiconDoc; 17 19 use mime_sniffer::MimeTypeSniffer; 18 20 use weaver_api::com_atproto::repo::{ 19 21 create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord, ··· 24 26 enum ViewMode { 25 27 Pretty, 26 28 Json, 29 + Schema, 27 30 } 28 31 29 32 #[component] 30 33 pub fn RecordIndex() -> Element { 31 34 let navigator = use_navigator(); 32 35 let mut uri_input = use_signal(|| String::new()); 33 - 34 36 let handle_uri_submit = move || { 35 37 let input_uri = uri_input.read().clone(); 36 38 if !input_uri.is_empty() { ··· 85 87 async move { client.fetch_record_slingshot(&*uri.read()).await } 86 88 }); 87 89 90 + // Fetch schema for the record 91 + let schema_resource = use_resource(move || { 92 + let fetcher = fetcher.clone(); 93 + async move { 94 + let record_read = record_resource.read(); 95 + let record = record_read.as_ref()?.as_ref().ok()?; 96 + 97 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 98 + let main_type = record.value.type_discriminator(); 99 + let mut main_schema = None; 100 + 101 + // Find and resolve all schemas (including main and nested) 102 + for type_val in record.value.query("...$type").values() { 103 + if let Some(type_str) = type_val.as_str() { 104 + // Skip non-NSID types (like "blob") 105 + if !type_str.contains('.') { 106 + continue; 107 + } 108 + 109 + if let Ok(nsid) = Nsid::new(type_str) { 110 + // Fetch and register schema 111 + if let Ok(schema) = fetcher.resolve_lexicon_schema(&nsid).await { 112 + validator 113 + .registry() 114 + .insert(nsid.to_smolstr(), schema.doc.clone()); 115 + 116 + // Keep the main record schema 117 + if Some(type_str) == main_type { 118 + main_schema = Some(schema.doc); 119 + } 120 + } 121 + } 122 + } 123 + } 124 + 125 + main_schema 126 + } 127 + }); 128 + 129 + let schema_signal = use_memo(move || schema_resource.read().clone().flatten()); 130 + 88 131 // Check ownership for edit access 89 132 let auth_state = use_context::<Signal<AuthState>>(); 90 133 let is_owner = use_memo(move || { ··· 119 162 view_mode: view_mode, 120 163 edit_mode: edit_mode, 121 164 record_resource: record_resource, 165 + schema: schema_signal, 122 166 } 123 167 } else { 124 168 div { ··· 133 177 onclick: move |_| view_mode.set(ViewMode::Json), 134 178 "JSON" 135 179 } 180 + button { 181 + class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, 182 + onclick: move |_| view_mode.set(ViewMode::Schema), 183 + "Schema" 184 + } 136 185 if is_owner() { 137 186 button { 138 187 class: "tab-button edit-button", ··· 158 207 lang: Some("json".to_string()), 159 208 } 160 209 } 210 + }, 211 + ViewMode::Schema => rsx! { 212 + SchemaView { schema: schema_signal } 161 213 }, 162 214 } 163 215 } ··· 180 232 } 181 233 } 182 234 } 235 + 236 + #[component] 237 + fn SchemaView(schema: ReadSignal<Option<LexiconDoc<'static>>>) -> Element { 238 + if let Some(schema_doc) = schema() { 239 + // Convert LexiconDoc to Data for display 240 + let schema_data = use_memo(move || to_data(&schema_doc).ok().map(|d| d.into_static())); 241 + 242 + if let Some(data) = schema_data() { 243 + rsx! { 244 + div { 245 + class: "pretty-record", 246 + DataView { data: data, path: String::new(), did: String::new() } 247 + } 248 + } 249 + } else { 250 + rsx! { 251 + div { class: "schema-error", "Failed to convert schema to displayable format" } 252 + } 253 + } 254 + } else { 255 + rsx! { 256 + div { class: "schema-loading", "Loading schema..." } 257 + } 258 + } 259 + } 183 260 fn get_hex_rep(byte_array: &mut [u8]) -> String { 184 261 let build_string_vec: Vec<String> = byte_array 185 262 .chunks(2) ··· 563 640 } 564 641 565 642 #[component] 566 - fn JsonEditor(data: Signal<Data<'static>>, nsid: ReadSignal<Option<String>>) -> Element { 643 + fn JsonEditor( 644 + data: Signal<Data<'static>>, 645 + nsid: ReadSignal<Option<String>>, 646 + schema: ReadSignal<Option<LexiconDoc<'static>>>, 647 + ) -> Element { 567 648 let mut json_text = 568 649 use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default()); 569 - let mut parse_error = use_signal(|| None::<String>); 570 650 571 651 let height = use_memo(move || { 572 652 let line_count = json_text().lines().count(); ··· 577 657 format!("{}px", lines * 22 + 32) 578 658 }); 579 659 580 - let fetcher = use_context::<CachedFetcher>(); 581 - 582 660 let validation = use_resource(move || { 583 661 let text = json_text(); 584 662 let nsid_val = nsid(); 585 - let fetcher = fetcher.clone(); 663 + let _ = schema(); // Track schema changes 586 664 587 665 async move { 588 666 // Only validate if we have an NSID ··· 596 674 } 597 675 }; 598 676 599 - // Resolve lexicon if needed 600 - let registry = jacquard_lexicon::schema::SchemaRegistry::from_inventory(); 601 - if registry.get(&nsid_str).is_none() { 602 - let nsid_str = nsid_str.split('#').next(); 603 - if let Some(Ok(nsid_parsed)) = nsid_str.map(|s| Nsid::new(s)) { 604 - if let Ok(schema) = fetcher.resolve_lexicon_schema(&nsid_parsed).await { 605 - registry.insert(nsid_parsed.to_smolstr(), schema.doc); 606 - } 607 - } 608 - } 609 - 610 - // Validate 611 - let validator = jacquard_lexicon::validation::SchemaValidator::from_registry(registry); 677 + // Use global validator (schema already registered) 678 + let validator = jacquard_lexicon::validation::SchemaValidator::global(); 612 679 let result = validator.validate_by_nsid(&nsid_str, &parsed); 613 680 614 681 Some((Some(result), None)) ··· 1133 1200 } 1134 1201 } 1135 1202 1136 - /// Boolean field (checkbox) 1203 + /// Boolean field (toggle button) 1137 1204 #[component] 1138 1205 fn EditableBooleanField( 1139 1206 root: Signal<Data<'static>>, ··· 1155 1222 PathLabel { path: path.clone() } 1156 1223 {remove_button} 1157 1224 } 1158 - input { 1159 - r#type: "checkbox", 1160 - checked: current_value(), 1161 - onchange: move |evt| { 1225 + button { 1226 + class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" }, 1227 + onclick: move |_| { 1162 1228 root.with_mut(|data| { 1163 1229 if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) { 1164 - *target = Data::Boolean(evt.checked()); 1230 + if let Some(bool_val) = target.as_boolean() { 1231 + *target = Data::Boolean(!bool_val); 1232 + } 1165 1233 } 1166 1234 }); 1167 - } 1235 + }, 1236 + "{current_value()}" 1168 1237 } 1169 1238 } 1170 1239 } ··· 2133 2202 view_mode: Signal<ViewMode>, 2134 2203 edit_mode: Signal<bool>, 2135 2204 record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>, 2205 + schema: ReadSignal<Option<LexiconDoc<'static>>>, 2136 2206 ) -> Element { 2137 2207 let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone())); 2138 2208 let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string())); ··· 2156 2226 class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" }, 2157 2227 onclick: move |_| view_mode.set(ViewMode::Json), 2158 2228 "JSON" 2229 + } 2230 + button { 2231 + class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" }, 2232 + onclick: move |_| view_mode.set(ViewMode::Schema), 2233 + "Schema" 2159 2234 } 2160 2235 ActionButtons { 2161 2236 on_update: move |_| { ··· 2323 2398 } 2324 2399 }, 2325 2400 ViewMode::Json => rsx! { 2326 - JsonEditor { data: edit_data, nsid } 2401 + JsonEditor { data: edit_data, nsid, schema } 2402 + }, 2403 + ViewMode::Schema => rsx! { 2404 + SchemaView { schema } 2327 2405 }, 2328 2406 } 2329 2407 }