diff --git a/crates/weaver-app/assets/styling/record-view.css b/crates/weaver-app/assets/styling/record-view.css index 9e765ff..386e441 100644 --- a/crates/weaver-app/assets/styling/record-view.css +++ b/crates/weaver-app/assets/styling/record-view.css @@ -7,7 +7,6 @@ .record-header { margin-bottom: 2rem; padding-bottom: 0.5rem; - font-family: var(--font-mono); border-bottom: 1px solid var(--color-border); } @@ -931,3 +930,44 @@ color: var(--color-error, #ff5252); border-bottom-color: var(--color-error, #ff6b6b); } + +.accordion-content { + grid-template-rows: 1fr; + padding-left: 2.8rem; +} + +.accordion-content .section-content { + margin-left: -1px; +} + +.accordion-content .record-field { + margin-left: 0px; +} + +.accordion-content .array-item { + margin-left: 0px; +} + +.accordion-content .array-item .section-content { + border-collapse: collapse; +} + +.accordion-content .array-item .record-section { + margin-left: 1.5rem; +} + +.accordion-content .array-item .record-section .record-field { + margin-left: -1px; +} + +.accordion-content .array-item .record-section .accordion-trigger .section-label { + margin-left: -2px; +} + +.accordion-trigger { + background-color: var(--color-base) !important; +} + +.accordion { + margin-left: -2.8rem; +} diff --git a/crates/weaver-app/src/components/accordion/component.rs b/crates/weaver-app/src/components/accordion/component.rs index f1bf89d..55fd0a9 100644 --- a/crates/weaver-app/src/components/accordion/component.rs +++ b/crates/weaver-app/src/components/accordion/component.rs @@ -9,7 +9,6 @@ pub fn Accordion(props: AccordionProps) -> Element { document::Link { rel: "stylesheet", href: asset!("./style.css") } accordion::Accordion { class: "accordion", - width: "15rem", id: props.id, allow_multiple_open: props.allow_multiple_open, disabled: props.disabled, @@ -44,13 +43,13 @@ pub fn AccordionTrigger(props: AccordionTriggerProps) -> Element { class: "accordion-trigger", id: props.id, attributes: props.attributes, - {props.children} svg { class: "accordion-expand-icon", view_box: "0 0 24 24", xmlns: "http://www.w3.org/2000/svg", polyline { points: "6 9 12 15 18 9" } } + {props.children} } } } @@ -60,7 +59,6 @@ pub fn AccordionContent(props: AccordionContentProps) -> Element { rsx! { accordion::AccordionContent { class: "accordion-content", - style: "--collapsible-content-width: 140px", id: props.id, attributes: props.attributes, {props.children} diff --git a/crates/weaver-app/src/components/accordion/style.css b/crates/weaver-app/src/components/accordion/style.css index e65b1a1..7f9a5e8 100644 --- a/crates/weaver-app/src/components/accordion/style.css +++ b/crates/weaver-app/src/components/accordion/style.css @@ -1,89 +1,53 @@ .accordion-trigger { - display: flex; - width: 100%; - box-sizing: border-box; - flex-direction: row; - align-items: center; - justify-content: space-between; - padding: 0; - padding-top: 1rem; - padding-bottom: 1rem; - border: none; - background-color: transparent; - color: var(--secondary-color-4); - outline: none; - text-align: left; -} - -.accordion-trigger:focus-visible { - border: none; - box-shadow: inset 0 0 0 2px var(--focused-border-color); + display: inline-flex; + box-sizing: border-box; + flex-direction: row; + align-items: center; + padding: 0; + border: none; + background-color: transparent; + outline: none; + text-align: left; } .accordion-trigger:hover { - cursor: pointer; - text-decoration-line: underline; + cursor: pointer; } .accordion-content { - display: grid; - height: 0; -} - -.accordion-content[data-open="false"] { - animation: accordion-slide-down 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; + display: grid; + overflow: hidden; + background-color: transparent; + transition: grid-template-rows 300ms cubic-bezier(0.87, 0, 0.13, 1); + grid-template-rows: 0fr; } .accordion-content[data-open="true"] { - animation: accordion-slide-up 300ms cubic-bezier(0.87, 0, 0.13, 1) forwards; -} - -@keyframes accordion-slide-down { - from { - height: var(--collapsible-content-width); - } - - to { - height: 0; - } + grid-template-rows: 1fr; } -@keyframes accordion-slide-up { - from { - height: 0; - } - - to { - height: var(--collapsible-content-width); - } +.accordion-content > * { + overflow: hidden; } .accordion-item { - overflow: hidden; - box-sizing: border-box; - border-bottom: 1px solid var(--primary-color-6); - margin-top: 1px; -} - -.accordion-item:first-child { - margin-top: 0; -} - -.accordion-item:last-child { - border-bottom: none; + box-sizing: border-box; } .accordion-expand-icon { - width: 20px; - height: 20px; - fill: none; - stroke: var(--secondary-color-4); - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 2; - transition: rotate 150ms cubic-bezier(0.4, 0, 0.2, 1); + width: 16px; + height: 16px; + flex-shrink: 0; + fill: none; + stroke: var(--color-subtle); + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; + transition: rotate 150ms cubic-bezier(0.4, 0, 0.2, 1); + margin-right: 0.35rem; + opacity: 0.7; } .accordion-item[data-open="true"] .accordion-expand-icon { - rotate: 180deg; + rotate: 180deg; } diff --git a/crates/weaver-app/src/views/record.rs b/crates/weaver-app/src/views/record.rs index 6318022..601d76a 100644 --- a/crates/weaver-app/src/views/record.rs +++ b/crates/weaver-app/src/views/record.rs @@ -1,5 +1,6 @@ use crate::Route; use crate::auth::AuthState; +use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger}; use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle}; use crate::fetch::CachedFetcher; use dioxus::{CapturedError, prelude::*}; @@ -130,7 +131,15 @@ pub fn RecordView(uri: ReadSignal>) -> Element { // Find refs in the schema and resolve them if let Ok(schema_data) = to_data(&schema.doc) { - for ref_val in schema_data.query("..ref").values() { + for ref_val in schema_data.query("...ref").values() { + if let Some(ref_str) = ref_val.as_str() { + if ref_str.contains('.') { + resolve_schema_with_refs(fetcher, ref_str, validator, resolved) + .await; + } + } + } + for ref_val in schema_data.query("...refs").values() { if let Some(ref_str) = ref_val.as_str() { if ref_str.contains('.') { resolve_schema_with_refs(fetcher, ref_str, validator, resolved) @@ -596,49 +605,58 @@ fn DataView( let label = path.split('.').last().unwrap_or(&path); rsx! { div { class: "record-section", - div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}] " } } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } - div { class: "section-content", - for (idx, item) in arr.iter().enumerate() { - { - let item_path = format!("{}[{}]", label, idx); - let is_object = matches!(item, Data::Object(_)); - - if is_object { - rsx! { - - div { - class: "array-item", - div { class: "record-section", - div { class: "section-label", "{item_path}" } - div { class: "section-content", - DataView { - data: item.clone(), - root_data, - path: item_path.clone(), - did: did.clone() + Accordion { + id: "array-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-label", "{label}" span { class: "array-len", "[{arr.len()}]" } } + } + AccordionContent { + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + div { class: "section-content", + for (idx, item) in arr.iter().enumerate() { + { + let item_path = format!("{}[{}]", label, idx); + let is_object = matches!(item, Data::Object(_)); + + if is_object { + rsx! { + div { + class: "array-item", + div { class: "record-section", + div { class: "section-label", "{item_path}" } + div { class: "section-content", + DataView { + data: item.clone(), + root_data, + path: item_path.clone(), + did: did.clone() + } + } + } + } + } + } else { + rsx! { + div { + class: "array-item", + DataView { + data: item.clone(), + root_data, + path: item_path, + did: did.clone() + } + } } } } - } - } - } else { - - rsx! { - - div { - class: "array-item", - DataView { - data: item.clone(), - root_data, - path: item_path, - did: did.clone() - } - } } } } @@ -679,21 +697,32 @@ fn DataView( // Nested object (not array item): wrap in section let label = path.split('.').last().unwrap_or(&path); rsx! { - - div { class: "section-label", "{label}" } - if has_errors { - for error in &errors { - div { class: "field-error-message", "{error}" } - } - } div { class: "record-section", - div { class: "section-content", - for (key, value) in obj.iter() { - { - let new_path = format!("{}.{}", path, key); - let did_clone = did.clone(); - rsx! { - DataView { data: value.clone(), root_data, path: new_path, did: did_clone } + Accordion { + id: "object-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-label", "{label}" } + } + AccordionContent { + if has_errors { + for error in &errors { + div { class: "field-error-message", "{error}" } + } + } + div { class: "section-content", + for (key, value) in obj.iter() { + { + let new_path = format!("{}.{}", path, key); + let did_clone = did.clone(); + rsx! { + DataView { data: value.clone(), root_data, path: new_path, did: did_clone } + } + } + } } } } @@ -1815,6 +1844,7 @@ fn EditableBlobField( match text.parse::() { Ok(new_size) => { + size_input.set(format_size(new_size, humansize::BINARY)); size_error.set(None); root.with_mut(|data| { if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) { @@ -2275,70 +2305,78 @@ fn EditableArrayField(root: Signal>, path: String, did: String) -> rsx! { div { class: "record-section array-section", - div { class: "section-header", - div { class: "section-label", - { - let parts: Vec<&str> = path.split('.').collect(); - let final_part = parts.last().unwrap_or(&""); - rsx! { "{final_part}" } + Accordion { + id: "edit-array-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-header", + div { class: "section-label", + { + let parts: Vec<&str> = path.split('.').collect(); + let final_part = parts.last().unwrap_or(&""); + rsx! { "{final_part}" } + } + } + span { class: "array-length", "[{array_len}]" } + } } - } - span { class: "array-length", "[{array_len}]" } - } - - div { class: "section-content", - for idx in 0..array_len() { - { - let item_path = format!("{}[{}]", path, idx); - let path_for_remove = path.clone(); + AccordionContent { + div { class: "section-content", + for idx in 0..array_len() { + { + let item_path = format!("{}[{}]", path, idx); + let path_for_remove = path.clone(); - rsx! { - div { - class: "array-item", - key: "{item_path}", - - EditableDataView { - root: root, - path: item_path.clone(), - did: did.clone(), - remove_button: rsx! { - button { - class: "field-remove-button", - onclick: move |_| { - root.with_mut(|data| { - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { - arr.0.remove(idx); + rsx! { + div { + class: "array-item", + key: "{item_path}", + + EditableDataView { + root: root, + path: item_path.clone(), + did: did.clone(), + remove_button: rsx! { + button { + class: "field-remove-button", + onclick: move |_| { + root.with_mut(|data| { + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) { + arr.0.remove(idx); + } + }); + }, + "Remove" } - }); - }, - "Remove" + } + } } } } } - } - } - } - div { - class: "array-item", - div { - class: "add-field-widget", - button { - - onclick: move |_| { - root.with_mut(|data| { - if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { - let new_item = create_array_item_default(arr); - arr.0.push(new_item); + div { + class: "array-item", + div { + class: "add-field-widget", + button { + onclick: move |_| { + root.with_mut(|data| { + if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) { + let new_item = create_array_item_default(arr); + arr.0.push(new_item); + } + }); + }, + "+ Add Item" } - }); - }, - "+ Add Item" + } + } } } } - - } } } @@ -2370,41 +2408,53 @@ fn EditableObjectField( rsx! { if !is_root { div { class: "record-section object-section", - div { class: "section-header", - div { class: "section-label", - { - let parts: Vec<&str> = path.split('.').collect(); - let final_part = parts.last().unwrap_or(&""); - rsx! { "{final_part}" } + Accordion { + id: "edit-object-{path}", + collapsible: true, + AccordionItem { + default_open: true, + index: 0, + AccordionTrigger { + div { class: "section-header", + div { class: "section-label", + { + let parts: Vec<&str> = path.split('.').collect(); + let final_part = parts.last().unwrap_or(&""); + rsx! { "{final_part}" } + } + } + {remove_button} + } } - } - {remove_button} - } - div { class: "section-content", - for key in field_keys() { - { - let field_path = if path.is_empty() { - key.to_string() - } else { - format!("{}.{}", path, key) - }; - let is_type_field = key == "$type"; - - rsx! { - FieldWithRemove { - key: "{field_path}", - root: root, - path: field_path.clone(), - did: did.clone(), - is_removable: !is_type_field, - parent_path: path.clone(), - field_key: key.clone(), + AccordionContent { + div { class: "section-content", + for key in field_keys() { + { + let field_path = if path.is_empty() { + key.to_string() + } else { + format!("{}.{}", path, key) + }; + let is_type_field = key == "$type"; + + rsx! { + FieldWithRemove { + key: "{field_path}", + root: root, + path: field_path.clone(), + did: did.clone(), + is_removable: !is_type_field, + parent_path: path.clone(), + field_key: key.clone(), + } + } + } + } + + AddFieldWidget { root: root, path: path.clone() } } } } - } - - AddFieldWidget { root: root, path: path.clone() } } } } else {