tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
collapsible objects & arrays, improved lexicon resolution
Orual
2 months ago
1f3e4380
da465d03
+259
-207
4 changed files
expand all
collapse all
unified
split
crates
weaver-app
assets
styling
record-view.css
src
components
accordion
component.rs
style.css
views
record.rs
+41
-1
crates/weaver-app/assets/styling/record-view.css
···
7
.record-header {
8
margin-bottom: 2rem;
9
padding-bottom: 0.5rem;
10
-
11
font-family: var(--font-mono);
12
border-bottom: 1px solid var(--color-border);
13
}
···
931
color: var(--color-error, #ff5252);
932
border-bottom-color: var(--color-error, #ff6b6b);
933
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
7
.record-header {
8
margin-bottom: 2rem;
9
padding-bottom: 0.5rem;
0
10
font-family: var(--font-mono);
11
border-bottom: 1px solid var(--color-border);
12
}
···
930
color: var(--color-error, #ff5252);
931
border-bottom-color: var(--color-error, #ff6b6b);
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
document::Link { rel: "stylesheet", href: asset!("./style.css") }
10
accordion::Accordion {
11
class: "accordion",
12
-
width: "15rem",
13
id: props.id,
14
allow_multiple_open: props.allow_multiple_open,
15
disabled: props.disabled,
···
44
class: "accordion-trigger",
45
id: props.id,
46
attributes: props.attributes,
47
-
{props.children}
48
svg {
49
class: "accordion-expand-icon",
50
view_box: "0 0 24 24",
51
xmlns: "http://www.w3.org/2000/svg",
52
polyline { points: "6 9 12 15 18 9" }
53
}
0
54
}
55
}
56
}
···
60
rsx! {
61
accordion::AccordionContent {
62
class: "accordion-content",
63
-
style: "--collapsible-content-width: 140px",
64
id: props.id,
65
attributes: props.attributes,
66
{props.children}
···
9
document::Link { rel: "stylesheet", href: asset!("./style.css") }
10
accordion::Accordion {
11
class: "accordion",
0
12
id: props.id,
13
allow_multiple_open: props.allow_multiple_open,
14
disabled: props.disabled,
···
43
class: "accordion-trigger",
44
id: props.id,
45
attributes: props.attributes,
0
46
svg {
47
class: "accordion-expand-icon",
48
view_box: "0 0 24 24",
49
xmlns: "http://www.w3.org/2000/svg",
50
polyline { points: "6 9 12 15 18 9" }
51
}
52
+
{props.children}
53
}
54
}
55
}
···
59
rsx! {
60
accordion::AccordionContent {
61
class: "accordion-content",
0
62
id: props.id,
63
attributes: props.attributes,
64
{props.children}
+31
-67
crates/weaver-app/src/components/accordion/style.css
···
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);
21
}
22
23
.accordion-trigger:hover {
24
-
cursor: pointer;
25
-
text-decoration-line: underline;
26
}
27
28
.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;
35
}
36
37
.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
-
}
49
}
50
51
-
@keyframes accordion-slide-up {
52
-
from {
53
-
height: 0;
54
-
}
55
-
56
-
to {
57
-
height: var(--collapsible-content-width);
58
-
}
59
}
60
61
.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;
74
}
75
76
.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);
0
0
0
85
}
86
87
.accordion-item[data-open="true"] .accordion-expand-icon {
88
-
rotate: 180deg;
89
}
···
1
.accordion-trigger {
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;
0
0
0
0
0
0
0
0
0
0
11
}
12
13
.accordion-trigger:hover {
14
+
cursor: pointer;
0
15
}
16
17
.accordion-content {
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;
0
23
}
24
25
.accordion-content[data-open="true"] {
26
+
grid-template-rows: 1fr;
0
0
0
0
0
0
0
0
0
0
27
}
28
29
+
.accordion-content > * {
30
+
overflow: hidden;
0
0
0
0
0
0
31
}
32
33
.accordion-item {
34
+
box-sizing: border-box;
0
0
0
0
0
0
0
0
0
0
0
35
}
36
37
.accordion-expand-icon {
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;
49
}
50
51
.accordion-item[data-open="true"] .accordion-expand-icon {
52
+
rotate: 180deg;
53
}
+186
-136
crates/weaver-app/src/views/record.rs
···
1
use crate::Route;
2
use crate::auth::AuthState;
0
3
use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle};
4
use crate::fetch::CachedFetcher;
5
use dioxus::{CapturedError, prelude::*};
···
130
131
// Find refs in the schema and resolve them
132
if let Ok(schema_data) = to_data(&schema.doc) {
133
-
for ref_val in schema_data.query("..ref").values() {
0
0
0
0
0
0
0
0
134
if let Some(ref_str) = ref_val.as_str() {
135
if ref_str.contains('.') {
136
resolve_schema_with_refs(fetcher, ref_str, validator, resolved)
···
596
let label = path.split('.').last().unwrap_or(&path);
597
rsx! {
598
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(_));
0
0
0
0
0
0
0
0
0
610
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()
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
624
}
625
}
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
}
642
}
643
}
···
679
// Nested object (not array item): wrap in section
680
let label = path.split('.').last().unwrap_or(&path);
681
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
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 }
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
697
}
698
}
699
}
···
1815
1816
match text.parse::<usize>() {
1817
Ok(new_size) => {
0
1818
size_error.set(None);
1819
root.with_mut(|data| {
1820
if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) {
···
2275
2276
rsx! {
2277
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}" }
0
0
0
0
0
0
0
0
0
0
0
2284
}
2285
-
}
2286
-
span { class: "array-length", "[{array_len}]" }
2287
-
}
0
0
0
2288
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();
2294
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);
2311
}
2312
-
});
2313
-
},
2314
-
"Remove"
2315
}
2316
}
2317
}
2318
}
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);
2333
}
2334
-
});
2335
-
},
2336
-
"+ Add Item"
2337
}
2338
}
2339
}
2340
-
2341
-
2342
}
2343
}
2344
}
···
2370
rsx! {
2371
if !is_root {
2372
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}" }
0
0
0
0
0
0
0
0
0
0
0
2379
}
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";
2392
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(),
0
0
0
0
0
0
2402
}
2403
}
2404
}
2405
-
}
2406
-
2407
-
AddFieldWidget { root: root, path: path.clone() }
2408
}
2409
}
2410
} else {
···
1
use crate::Route;
2
use crate::auth::AuthState;
3
+
use crate::components::accordion::{Accordion, AccordionContent, AccordionItem, AccordionTrigger};
4
use crate::components::dialog::{DialogContent, DialogDescription, DialogRoot, DialogTitle};
5
use crate::fetch::CachedFetcher;
6
use dioxus::{CapturedError, prelude::*};
···
131
132
// Find refs in the schema and resolve them
133
if let Ok(schema_data) = to_data(&schema.doc) {
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() {
143
if let Some(ref_str) = ref_val.as_str() {
144
if ref_str.contains('.') {
145
resolve_schema_with_refs(fetcher, ref_str, validator, resolved)
···
605
let label = path.split('.').last().unwrap_or(&path);
606
rsx! {
607
div { class: "record-section",
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(_));
628
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
+
}
657
}
658
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
659
}
660
}
661
}
···
697
// Nested object (not array item): wrap in section
698
let label = path.split('.').last().unwrap_or(&path);
699
rsx! {
0
0
0
0
0
0
0
700
div { class: "record-section",
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
+
}
726
}
727
}
728
}
···
1844
1845
match text.parse::<usize>() {
1846
Ok(new_size) => {
1847
+
size_input.set(format_size(new_size, humansize::BINARY));
1848
size_error.set(None);
1849
root.with_mut(|data| {
1850
if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) {
···
2305
2306
rsx! {
2307
div { class: "record-section array-section",
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
+
}
2325
}
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();
2332
2333
+
rsx! {
2334
+
div {
2335
+
class: "array-item",
2336
+
key: "{item_path}",
0
2337
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"
0
2353
}
2354
+
}
2355
+
}
0
2356
}
2357
}
2358
}
2359
}
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"
2374
}
2375
+
}
2376
+
}
0
2377
}
2378
}
2379
}
0
0
2380
}
2381
}
2382
}
···
2408
rsx! {
2409
if !is_root {
2410
div { class: "record-section object-section",
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
+
}
2428
}
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";
0
0
2439
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() }
2455
}
2456
}
2457
}
0
0
0
2458
}
2459
}
2460
} else {