collapsible objects & arrays, improved lexicon resolution

Orual 1f3e4380 da465d03

+259 -207
+41 -1
crates/weaver-app/assets/styling/record-view.css
··· 7 7 .record-header { 8 8 margin-bottom: 2rem; 9 9 padding-bottom: 0.5rem; 10 - 11 10 font-family: var(--font-mono); 12 11 border-bottom: 1px solid var(--color-border); 13 12 } ··· 931 930 color: var(--color-error, #ff5252); 932 931 border-bottom-color: var(--color-error, #ff6b6b); 933 932 } 933 + 934 + .accordion-content { 935 + grid-template-rows: 1fr; 936 + padding-left: 2.8rem; 937 + } 938 + 939 + .accordion-content .section-content { 940 + margin-left: -1px; 941 + } 942 + 943 + .accordion-content .record-field { 944 + margin-left: 0px; 945 + } 946 + 947 + .accordion-content .array-item { 948 + margin-left: 0px; 949 + } 950 + 951 + .accordion-content .array-item .section-content { 952 + border-collapse: collapse; 953 + } 954 + 955 + .accordion-content .array-item .record-section { 956 + margin-left: 1.5rem; 957 + } 958 + 959 + .accordion-content .array-item .record-section .record-field { 960 + margin-left: -1px; 961 + } 962 + 963 + .accordion-content .array-item .record-section .accordion-trigger .section-label { 964 + margin-left: -2px; 965 + } 966 + 967 + .accordion-trigger { 968 + background-color: var(--color-base) !important; 969 + } 970 + 971 + .accordion { 972 + margin-left: -2.8rem; 973 + }
+1 -3
crates/weaver-app/src/components/accordion/component.rs
··· 9 9 document::Link { rel: "stylesheet", href: asset!("./style.css") } 10 10 accordion::Accordion { 11 11 class: "accordion", 12 - width: "15rem", 13 12 id: props.id, 14 13 allow_multiple_open: props.allow_multiple_open, 15 14 disabled: props.disabled, ··· 44 43 class: "accordion-trigger", 45 44 id: props.id, 46 45 attributes: props.attributes, 47 - {props.children} 48 46 svg { 49 47 class: "accordion-expand-icon", 50 48 view_box: "0 0 24 24", 51 49 xmlns: "http://www.w3.org/2000/svg", 52 50 polyline { points: "6 9 12 15 18 9" } 53 51 } 52 + {props.children} 54 53 } 55 54 } 56 55 } ··· 60 59 rsx! { 61 60 accordion::AccordionContent { 62 61 class: "accordion-content", 63 - style: "--collapsible-content-width: 140px", 64 62 id: props.id, 65 63 attributes: props.attributes, 66 64 {props.children}
+31 -67
crates/weaver-app/src/components/accordion/style.css
··· 1 1 .accordion-trigger { 2 - display: flex; 3 - width: 100%; 4 - box-sizing: border-box; 5 - flex-direction: row; 6 - align-items: center; 7 - justify-content: space-between; 8 - padding: 0; 9 - padding-top: 1rem; 10 - padding-bottom: 1rem; 11 - border: none; 12 - background-color: transparent; 13 - color: var(--secondary-color-4); 14 - outline: none; 15 - text-align: left; 16 - } 17 - 18 - .accordion-trigger:focus-visible { 19 - border: none; 20 - box-shadow: inset 0 0 0 2px var(--focused-border-color); 2 + display: inline-flex; 3 + box-sizing: border-box; 4 + flex-direction: row; 5 + align-items: center; 6 + padding: 0; 7 + border: none; 8 + background-color: transparent; 9 + outline: none; 10 + text-align: left; 21 11 } 22 12 23 13 .accordion-trigger:hover { 24 - cursor: pointer; 25 - text-decoration-line: underline; 14 + cursor: pointer; 26 15 } 27 16 28 17 .accordion-content { 29 - display: grid; 30 - height: 0; 31 - } 32 - 33 - .accordion-content[data-open="false"] { 34 - animation: accordion-slide-down 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; 18 + display: grid; 19 + overflow: hidden; 20 + background-color: transparent; 21 + transition: grid-template-rows 300ms cubic-bezier(0.87, 0, 0.13, 1); 22 + grid-template-rows: 0fr; 35 23 } 36 24 37 25 .accordion-content[data-open="true"] { 38 - animation: accordion-slide-up 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; 39 - } 40 - 41 - @keyframes accordion-slide-down { 42 - from { 43 - height: var(--collapsible-content-width); 44 - } 45 - 46 - to { 47 - height: 0; 48 - } 26 + grid-template-rows: 1fr; 49 27 } 50 28 51 - @keyframes accordion-slide-up { 52 - from { 53 - height: 0; 54 - } 55 - 56 - to { 57 - height: var(--collapsible-content-width); 58 - } 29 + .accordion-content > * { 30 + overflow: hidden; 59 31 } 60 32 61 33 .accordion-item { 62 - overflow: hidden; 63 - box-sizing: border-box; 64 - border-bottom: 1px solid var(--primary-color-6); 65 - margin-top: 1px; 66 - } 67 - 68 - .accordion-item:first-child { 69 - margin-top: 0; 70 - } 71 - 72 - .accordion-item:last-child { 73 - border-bottom: none; 34 + box-sizing: border-box; 74 35 } 75 36 76 37 .accordion-expand-icon { 77 - width: 20px; 78 - height: 20px; 79 - fill: none; 80 - stroke: var(--secondary-color-4); 81 - stroke-linecap: round; 82 - stroke-linejoin: round; 83 - stroke-width: 2; 84 - transition: rotate 150ms cubic-bezier(0.4, 0, 0.2, 1); 38 + width: 16px; 39 + height: 16px; 40 + flex-shrink: 0; 41 + fill: none; 42 + stroke: var(--color-subtle); 43 + stroke-linecap: round; 44 + stroke-linejoin: round; 45 + stroke-width: 2; 46 + transition: rotate 150ms cubic-bezier(0.4, 0, 0.2, 1); 47 + margin-right: 0.35rem; 48 + opacity: 0.7; 85 49 } 86 50 87 51 .accordion-item[data-open="true"] .accordion-expand-icon { 88 - rotate: 180deg; 52 + rotate: 180deg; 89 53 }
+186 -136
crates/weaver-app/src/views/record.rs
··· 1 1 use crate::Route; 2 2 use crate::auth::AuthState; 3 + use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; 3 4 use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; 4 5 use crate::fetch::CachedFetcher; 5 6 use dioxus::{CapturedError, prelude::*}; ··· 130 131 131 132 // Find refs in the schema and resolve them 132 133 if let Ok(schema_data) = to_data(&schema.doc) { 133 - for ref_val in schema_data.query("..ref").values() { 134 + for ref_val in schema_data.query("...ref").values() { 135 + if let Some(ref_str) = ref_val.as_str() { 136 + if ref_str.contains('.') { 137 + resolve_schema_with_refs(fetcher, ref_str, validator, resolved) 138 + .await; 139 + } 140 + } 141 + } 142 + for ref_val in schema_data.query("...refs").values() { 134 143 if let Some(ref_str) = ref_val.as_str() { 135 144 if ref_str.contains('.') { 136 145 resolve_schema_with_refs(fetcher, ref_str, validator, resolved) ··· 596 605 let label = path.split('.').last().unwrap_or(&path); 597 606 rsx! { 598 607 div { class: "record-section", 599 - div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}] " } } 600 - if has_errors { 601 - for error in &errors { 602 - div { class: "field-error-message", "{error}" } 603 - } 604 - } 605 - div { class: "section-content", 606 - for (idx, item) in arr.iter().enumerate() { 607 - { 608 - let item_path = format!("{}[{}]", label, idx); 609 - let is_object = matches!(item, Data::Object(_)); 608 + Accordion { 609 + id: "array-{path}", 610 + collapsible: true, 611 + AccordionItem { 612 + default_open: true, 613 + index: 0, 614 + AccordionTrigger { 615 + div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}]" } } 616 + } 617 + AccordionContent { 618 + if has_errors { 619 + for error in &errors { 620 + div { class: "field-error-message", "{error}" } 621 + } 622 + } 623 + div { class: "section-content", 624 + for (idx, item) in arr.iter().enumerate() { 625 + { 626 + let item_path = format!("{}[{}]", label, idx); 627 + let is_object = matches!(item, Data::Object(_)); 610 628 611 - if is_object { 612 - rsx! { 613 - 614 - div { 615 - class: "array-item", 616 - div { class: "record-section", 617 - div { class: "section-label", "{item_path}" } 618 - div { class: "section-content", 619 - DataView { 620 - data: item.clone(), 621 - root_data, 622 - path: item_path.clone(), 623 - did: did.clone() 629 + if is_object { 630 + rsx! { 631 + div { 632 + class: "array-item", 633 + div { class: "record-section", 634 + div { class: "section-label", "{item_path}" } 635 + div { class: "section-content", 636 + DataView { 637 + data: item.clone(), 638 + root_data, 639 + path: item_path.clone(), 640 + did: did.clone() 641 + } 642 + } 643 + } 644 + } 645 + } 646 + } else { 647 + rsx! { 648 + div { 649 + class: "array-item", 650 + DataView { 651 + data: item.clone(), 652 + root_data, 653 + path: item_path, 654 + did: did.clone() 655 + } 656 + } 624 657 } 625 658 } 626 - } 627 - } 628 - } 629 - } else { 630 - 631 - rsx! { 632 - 633 - div { 634 - class: "array-item", 635 - DataView { 636 - data: item.clone(), 637 - root_data, 638 - path: item_path, 639 - did: did.clone() 640 - } 641 659 } 642 660 } 643 661 } ··· 679 697 // Nested object (not array item): wrap in section 680 698 let label = path.split('.').last().unwrap_or(&path); 681 699 rsx! { 682 - 683 - div { class: "section-label", "{label}" } 684 - if has_errors { 685 - for error in &errors { 686 - div { class: "field-error-message", "{error}" } 687 - } 688 - } 689 700 div { class: "record-section", 690 - div { class: "section-content", 691 - for (key, value) in obj.iter() { 692 - { 693 - let new_path = format!("{}.{}", path, key); 694 - let did_clone = did.clone(); 695 - rsx! { 696 - DataView { data: value.clone(), root_data, path: new_path, did: did_clone } 701 + Accordion { 702 + id: "object-{path}", 703 + collapsible: true, 704 + AccordionItem { 705 + default_open: true, 706 + index: 0, 707 + AccordionTrigger { 708 + div { class: "section-label", "{label}" } 709 + } 710 + AccordionContent { 711 + if has_errors { 712 + for error in &errors { 713 + div { class: "field-error-message", "{error}" } 714 + } 715 + } 716 + div { class: "section-content", 717 + for (key, value) in obj.iter() { 718 + { 719 + let new_path = format!("{}.{}", path, key); 720 + let did_clone = did.clone(); 721 + rsx! { 722 + DataView { data: value.clone(), root_data, path: new_path, did: did_clone } 723 + } 724 + } 725 + } 697 726 } 698 727 } 699 728 } ··· 1815 1844 1816 1845 match text.parse::<usize>() { 1817 1846 Ok(new_size) => { 1847 + size_input.set(format_size(new_size, humansize::BINARY)); 1818 1848 size_error.set(None); 1819 1849 root.with_mut(|data| { 1820 1850 if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) { ··· 2275 2305 2276 2306 rsx! { 2277 2307 div { class: "record-section array-section", 2278 - div { class: "section-header", 2279 - div { class: "section-label", 2280 - { 2281 - let parts: Vec<&str> = path.split('.').collect(); 2282 - let final_part = parts.last().unwrap_or(&""); 2283 - rsx! { "{final_part}" } 2308 + Accordion { 2309 + id: "edit-array-{path}", 2310 + collapsible: true, 2311 + AccordionItem { 2312 + default_open: true, 2313 + index: 0, 2314 + AccordionTrigger { 2315 + div { class: "section-header", 2316 + div { class: "section-label", 2317 + { 2318 + let parts: Vec<&str> = path.split('.').collect(); 2319 + let final_part = parts.last().unwrap_or(&""); 2320 + rsx! { "{final_part}" } 2321 + } 2322 + } 2323 + span { class: "array-length", "[{array_len}]" } 2324 + } 2284 2325 } 2285 - } 2286 - span { class: "array-length", "[{array_len}]" } 2287 - } 2326 + AccordionContent { 2327 + div { class: "section-content", 2328 + for idx in 0..array_len() { 2329 + { 2330 + let item_path = format!("{}[{}]", path, idx); 2331 + let path_for_remove = path.clone(); 2288 2332 2289 - div { class: "section-content", 2290 - for idx in 0..array_len() { 2291 - { 2292 - let item_path = format!("{}[{}]", path, idx); 2293 - let path_for_remove = path.clone(); 2333 + rsx! { 2334 + div { 2335 + class: "array-item", 2336 + key: "{item_path}", 2294 2337 2295 - rsx! { 2296 - div { 2297 - class: "array-item", 2298 - key: "{item_path}", 2299 - 2300 - EditableDataView { 2301 - root: root, 2302 - path: item_path.clone(), 2303 - did: did.clone(), 2304 - remove_button: rsx! { 2305 - button { 2306 - class: "field-remove-button", 2307 - onclick: move |_| { 2308 - root.with_mut(|data| { 2309 - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { 2310 - arr.0.remove(idx); 2338 + EditableDataView { 2339 + root: root, 2340 + path: item_path.clone(), 2341 + did: did.clone(), 2342 + remove_button: rsx! { 2343 + button { 2344 + class: "field-remove-button", 2345 + onclick: move |_| { 2346 + root.with_mut(|data| { 2347 + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { 2348 + arr.0.remove(idx); 2349 + } 2350 + }); 2351 + }, 2352 + "Remove" 2311 2353 } 2312 - }); 2313 - }, 2314 - "Remove" 2354 + } 2355 + } 2315 2356 } 2316 2357 } 2317 2358 } 2318 2359 } 2319 - } 2320 - } 2321 - } 2322 - div { 2323 - class: "array-item", 2324 - div { 2325 - class: "add-field-widget", 2326 - button { 2327 - 2328 - onclick: move |_| { 2329 - root.with_mut(|data| { 2330 - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { 2331 - let new_item = create_array_item_default(arr); 2332 - arr.0.push(new_item); 2360 + div { 2361 + class: "array-item", 2362 + div { 2363 + class: "add-field-widget", 2364 + button { 2365 + onclick: move |_| { 2366 + root.with_mut(|data| { 2367 + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { 2368 + let new_item = create_array_item_default(arr); 2369 + arr.0.push(new_item); 2370 + } 2371 + }); 2372 + }, 2373 + "+ Add Item" 2333 2374 } 2334 - }); 2335 - }, 2336 - "+ Add Item" 2375 + } 2376 + } 2337 2377 } 2338 2378 } 2339 2379 } 2340 - 2341 - 2342 2380 } 2343 2381 } 2344 2382 } ··· 2370 2408 rsx! { 2371 2409 if !is_root { 2372 2410 div { class: "record-section object-section", 2373 - div { class: "section-header", 2374 - div { class: "section-label", 2375 - { 2376 - let parts: Vec<&str> = path.split('.').collect(); 2377 - let final_part = parts.last().unwrap_or(&""); 2378 - rsx! { "{final_part}" } 2411 + Accordion { 2412 + id: "edit-object-{path}", 2413 + collapsible: true, 2414 + AccordionItem { 2415 + default_open: true, 2416 + index: 0, 2417 + AccordionTrigger { 2418 + div { class: "section-header", 2419 + div { class: "section-label", 2420 + { 2421 + let parts: Vec<&str> = path.split('.').collect(); 2422 + let final_part = parts.last().unwrap_or(&""); 2423 + rsx! { "{final_part}" } 2424 + } 2425 + } 2426 + {remove_button} 2427 + } 2379 2428 } 2380 - } 2381 - {remove_button} 2382 - } 2383 - div { class: "section-content", 2384 - for key in field_keys() { 2385 - { 2386 - let field_path = if path.is_empty() { 2387 - key.to_string() 2388 - } else { 2389 - format!("{}.{}", path, key) 2390 - }; 2391 - let is_type_field = key == "$type"; 2429 + AccordionContent { 2430 + div { class: "section-content", 2431 + for key in field_keys() { 2432 + { 2433 + let field_path = if path.is_empty() { 2434 + key.to_string() 2435 + } else { 2436 + format!("{}.{}", path, key) 2437 + }; 2438 + let is_type_field = key == "$type"; 2392 2439 2393 - rsx! { 2394 - FieldWithRemove { 2395 - key: "{field_path}", 2396 - root: root, 2397 - path: field_path.clone(), 2398 - did: did.clone(), 2399 - is_removable: !is_type_field, 2400 - parent_path: path.clone(), 2401 - field_key: key.clone(), 2440 + rsx! { 2441 + FieldWithRemove { 2442 + key: "{field_path}", 2443 + root: root, 2444 + path: field_path.clone(), 2445 + did: did.clone(), 2446 + is_removable: !is_type_field, 2447 + parent_path: path.clone(), 2448 + field_key: key.clone(), 2449 + } 2450 + } 2451 + } 2452 + } 2453 + 2454 + AddFieldWidget { root: root, path: path.clone() } 2402 2455 } 2403 2456 } 2404 2457 } 2405 - } 2406 - 2407 - AddFieldWidget { root: root, path: path.clone() } 2408 2458 } 2409 2459 } 2410 2460 } else {