tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
26
fork
atom
overview
issues
2
pulls
pipelines
good progress on pretty editor
Orual
2 months ago
735d9d74
0b325539
+462
-17
2 changed files
expand all
collapse all
unified
split
crates
weaver-app
assets
styling
record-view.css
src
views
record.rs
+132
-5
crates/weaver-app/assets/styling/record-view.css
···
169
display: flex;
170
flex-direction: column;
171
padding: 0rem 0 0rem 1rem;
172
-
173
padding-right: 1rem;
174
border-left: 2px solid var(--color-secondary);
175
border-bottom: 1px dashed var(--color-subtle);
···
241
padding-bottom: 0.25rem;
242
border-left: 2px solid var(--color-primary);
243
border-bottom: 1px dashed var(--color-muted);
244
-
margin-left: -1.51rem;
245
}
246
247
.section-content .section-label {
···
251
font-weight: 600;
252
padding-left: 1rem;
253
padding-top: 0.5rem;
254
-
margin-left: -0.01rem;
255
border-left: 2px solid var(--color-secondary);
256
}
257
···
265
.section-content .record-field {
266
border-left-color: var(--color-secondary);
267
opacity: 0.95;
0
0
268
}
269
270
.blob-image {
···
462
margin: 0.25rem 0;
463
}
464
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
465
/* Pretty Editor Input Fields */
466
.record-field input[type="text"],
467
.record-field input[type="number"] {
···
474
padding: 0.2rem 0 0.1rem 0;
475
margin-top: 0.1rem;
476
outline: none;
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
477
width: 100%;
0
478
transition: border-color 0.2s;
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
479
}
480
481
.record-field input[type="text"]:focus,
482
-
.record-field input[type="number"]:focus {
0
483
border-bottom-color: var(--color-primary);
484
}
485
486
.record-field input[type="text"].invalid,
487
-
.record-field input[type="number"].invalid {
0
488
border-bottom-color: var(--color-error, #ff6b6b);
489
}
490
···
592
.field-remove-button:hover {
593
color: var(--color-error, #ff6b6b);
594
}
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
169
display: flex;
170
flex-direction: column;
171
padding: 0rem 0 0rem 1rem;
0
172
padding-right: 1rem;
173
border-left: 2px solid var(--color-secondary);
174
border-bottom: 1px dashed var(--color-subtle);
···
240
padding-bottom: 0.25rem;
241
border-left: 2px solid var(--color-primary);
242
border-bottom: 1px dashed var(--color-muted);
243
+
margin-left: -1.55rem;
244
}
245
246
.section-content .section-label {
···
250
font-weight: 600;
251
padding-left: 1rem;
252
padding-top: 0.5rem;
253
+
margin-left: -0.02rem;
254
border-left: 2px solid var(--color-secondary);
255
}
256
···
264
.section-content .record-field {
265
border-left-color: var(--color-secondary);
266
opacity: 0.95;
267
+
align-self: stretch;
268
+
width: 100%;
269
}
270
271
.blob-image {
···
463
margin: 0.25rem 0;
464
}
465
466
+
/* Array Section Styling */
467
+
.section-header {
468
+
display: flex;
469
+
align-items: baseline;
470
+
gap: 0.5rem;
471
+
}
472
+
473
+
.array-item .section-header {
474
+
margin-left: -1.52rem;
475
+
}
476
+
477
+
.array-length {
478
+
font-family: var(--font-mono);
479
+
font-size: 0.8rem;
480
+
color: var(--color-subtle);
481
+
font-weight: normal;
482
+
}
483
+
484
+
.array-item {
485
+
width: 100%;
486
+
display: flex;
487
+
flex-direction: column;
488
+
}
489
+
490
/* Pretty Editor Input Fields */
491
.record-field input[type="text"],
492
.record-field input[type="number"] {
···
499
padding: 0.2rem 0 0.1rem 0;
500
margin-top: 0.1rem;
501
outline: none;
502
+
min-width: 10ch;
503
+
max-width: 100%;
504
+
width: auto;
505
+
flex: 1 1 auto;
506
+
transition: border-color 0.2s;
507
+
}
508
+
509
+
/* Hide number input spinner arrows */
510
+
.record-field input[type="number"] {
511
+
-moz-appearance: textfield;
512
+
}
513
+
514
+
.record-field input[type="number"]::-webkit-outer-spin-button,
515
+
.record-field input[type="number"]::-webkit-inner-spin-button {
516
+
-webkit-appearance: none;
517
+
margin: 0;
518
+
}
519
+
520
+
.record-field textarea {
521
+
font-family: var(--font-mono);
522
+
font-size: 0.9rem;
523
+
color: var(--color-text);
524
+
background: transparent;
525
+
border: none;
526
+
border-bottom: 1px solid transparent;
527
+
padding: 0.2rem 0 0.1rem 0;
528
+
margin-top: 0.1rem;
529
+
outline: none;
530
width: 100%;
531
+
min-height: 1.5em;
532
transition: border-color 0.2s;
533
+
overflow-y: hidden;
534
+
}
535
+
536
+
/* Style textarea resize handle - webkit (Chrome/Safari) */
537
+
.record-field textarea::-webkit-resizer {
538
+
background: transparent;
539
+
border: 2px solid var(--color-border);
540
+
border-left: none;
541
+
border-bottom: none;
542
+
border-top: none;
543
+
}
544
+
545
+
/* Style textarea resize handle - Firefox */
546
+
.record-field textarea {
547
+
scrollbar-color: var(--color-border) transparent;
548
+
scrollbar-width: thin;
549
}
550
551
.record-field input[type="text"]:focus,
552
+
.record-field input[type="number"]:focus,
553
+
.record-field textarea:focus {
554
border-bottom-color: var(--color-primary);
555
}
556
557
.record-field input[type="text"].invalid,
558
+
.record-field input[type="number"].invalid,
559
+
.record-field textarea.invalid {
560
border-bottom-color: var(--color-error, #ff6b6b);
561
}
562
···
664
.field-remove-button:hover {
665
color: var(--color-error, #ff6b6b);
666
}
667
+
668
+
/* Blob Field Styling */
669
+
.blob-field {
670
+
/* Inherits from .record-field */
671
+
}
672
+
673
+
.blob-fields {
674
+
display: flex;
675
+
flex-direction: column;
676
+
gap: 0.5rem;
677
+
padding-top: 0.5rem;
678
+
width: 100%;
679
+
}
680
+
681
+
.blob-field-row {
682
+
display: flex;
683
+
align-items: baseline;
684
+
gap: 0.5rem;
685
+
width: 100%;
686
+
}
687
+
688
+
.blob-field-row label {
689
+
font-family: var(--font-mono);
690
+
font-size: 0.75rem;
691
+
color: var(--color-subtle);
692
+
min-width: 4rem;
693
+
text-transform: uppercase;
694
+
letter-spacing: 0.05em;
695
+
flex-shrink: 0;
696
+
}
697
+
698
+
.blob-field-cid {
699
+
flex-wrap: wrap;
700
+
}
701
+
702
+
.blob-field-cid input {
703
+
flex: 1 1 auto;
704
+
width: 100% !important;
705
+
min-width: unset !important;
706
+
max-width: unset !important;
707
+
font-size: 0.75rem;
708
+
}
709
+
710
+
.blob-field-row .readonly {
711
+
font-family: var(--font-mono);
712
+
font-size: 0.85rem;
713
+
color: var(--color-text);
714
+
opacity: 0.7;
715
+
}
716
+
717
+
.blob-upload-note {
718
+
font-size: 0.75rem;
719
+
font-style: italic;
720
+
padding-top: 0.25rem;
721
+
}
+330
-12
crates/weaver-app/src/views/record.rs
···
813
}
814
}
815
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
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
816
// ============================================================================
817
// Pretty Editor: Component Hierarchy
818
// ============================================================================
···
832
.get_at_path(&path_for_memo)
833
.map(|d| d.clone().into_static())
834
{
835
-
Some(Data::Object(_)) => rsx! { EditableObjectField { root, path: path.clone(), did } },
0
836
Some(Data::String(_)) => {
837
rsx! { EditableStringField { root, path: path.clone(), remove_button } }
838
}
···
843
rsx! { EditableBooleanField { root, path: path.clone(), remove_button } }
844
}
845
Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } },
0
846
847
// Not yet implemented - fall back to read-only DataView
848
Some(data) => {
···
917
};
918
919
let type_label = format!("{:?}", string_type()).to_lowercase();
0
920
921
rsx! {
922
div { class: "record-field",
···
927
}
928
{remove_button}
929
}
930
-
input {
931
-
r#type: "text",
932
-
value: "{input_text}",
933
-
oninput: handle_input,
934
-
class: if parse_error().is_some() { "invalid" } else { "" },
0
0
0
0
0
0
0
0
0
935
}
936
if let Some(err) = parse_error() {
937
span { class: "field-error", " ❌ {err}" }
···
1086
}
1087
}
1088
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
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
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
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
1089
// ============================================================================
1090
// Field with Remove Button Wrapper
1091
// ============================================================================
···
1129
}
1130
1131
// ============================================================================
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
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
0
0
0
0
1132
// Object Field Editor (enables recursion)
1133
// ============================================================================
1134
1135
/// Object field - iterates fields and renders child EditableDataView for each
1136
#[component]
1137
-
fn EditableObjectField(root: Signal<Data<'static>>, path: String, did: String) -> Element {
0
0
0
0
0
1138
let path_for_memo = path.clone();
1139
let field_keys = use_memo(move || {
1140
root.read()
···
1149
rsx! {
1150
if !is_root {
1151
div { class: "record-section object-section",
1152
-
div { class: "section-label",
1153
-
{
1154
-
let parts: Vec<&str> = path.split('.').collect();
1155
-
let final_part = parts.last().unwrap_or(&"");
1156
-
rsx! { "{final_part}" }
0
0
1157
}
0
1158
}
1159
div { class: "section-content",
1160
for key in field_keys() {
···
813
}
814
}
815
816
+
/// Create default value for new array item by cloning structure of existing items
817
+
fn create_array_item_default(arr: &jacquard::types::value::Array) -> Data<'static> {
818
+
if let Some(existing) = arr.0.first() {
819
+
clone_structure(existing)
820
+
} else {
821
+
// Empty array, default to null (user can change type)
822
+
Data::Null
823
+
}
824
+
}
825
+
826
+
/// Clone structure of Data, setting sensible defaults for leaf values
827
+
fn clone_structure(data: &Data) -> Data<'static> {
828
+
use jacquard::types::string::*;
829
+
use jacquard::types::value::{Array, Object};
830
+
use jacquard::types::{LexiconStringType, blob::*};
831
+
use std::collections::BTreeMap;
832
+
833
+
match data {
834
+
Data::Object(obj) => {
835
+
let mut new_obj = BTreeMap::new();
836
+
for (key, value) in obj.0.iter() {
837
+
new_obj.insert(key.clone(), clone_structure(value));
838
+
}
839
+
Data::Object(Object(new_obj))
840
+
}
841
+
842
+
Data::Array(_) => Data::Array(Array(Vec::new())),
843
+
844
+
Data::String(s) => match s.string_type() {
845
+
LexiconStringType::Datetime => {
846
+
// Sensible default: now
847
+
Data::String(AtprotoStr::Datetime(Datetime::now()))
848
+
}
849
+
_ => {
850
+
// Empty string, type inference will handle it
851
+
Data::String(AtprotoStr::String("".into()))
852
+
}
853
+
},
854
+
855
+
Data::Integer(_) => Data::Integer(0),
856
+
Data::Boolean(_) => Data::Boolean(false),
857
+
858
+
Data::Blob(blob) => {
859
+
// Placeholder blob
860
+
Data::Blob(
861
+
Blob {
862
+
r#ref: CidLink::str(
863
+
"bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
864
+
),
865
+
mime_type: blob.mime_type.clone(),
866
+
size: 0,
867
+
}
868
+
.into_static(),
869
+
)
870
+
}
871
+
872
+
Data::Bytes(_) | Data::CidLink(_) | Data::Null => Data::Null,
873
+
}
874
+
}
875
+
876
// ============================================================================
877
// Pretty Editor: Component Hierarchy
878
// ============================================================================
···
892
.get_at_path(&path_for_memo)
893
.map(|d| d.clone().into_static())
894
{
895
+
Some(Data::Object(_)) => rsx! { EditableObjectField { root, path: path.clone(), did, remove_button } },
896
+
Some(Data::Array(_)) => rsx! { EditableArrayField { root, path: path.clone(), did } },
897
Some(Data::String(_)) => {
898
rsx! { EditableStringField { root, path: path.clone(), remove_button } }
899
}
···
904
rsx! { EditableBooleanField { root, path: path.clone(), remove_button } }
905
}
906
Some(Data::Null) => rsx! { EditableNullField { root, path: path.clone(), remove_button } },
907
+
Some(Data::Blob(_)) => rsx! { EditableBlobField { root, path: path.clone(), did, remove_button } },
908
909
// Not yet implemented - fall back to read-only DataView
910
Some(data) => {
···
979
};
980
981
let type_label = format!("{:?}", string_type()).to_lowercase();
982
+
let is_plain_string = string_type() == LexiconStringType::String;
983
984
rsx! {
985
div { class: "record-field",
···
990
}
991
{remove_button}
992
}
993
+
if is_plain_string {
994
+
textarea {
995
+
value: "{input_text}",
996
+
oninput: handle_input,
997
+
class: if parse_error().is_some() { "invalid" } else { "" },
998
+
rows: "1",
999
+
}
1000
+
} else {
1001
+
input {
1002
+
r#type: "text",
1003
+
value: "{input_text}",
1004
+
oninput: handle_input,
1005
+
class: if parse_error().is_some() { "invalid" } else { "" },
1006
+
}
1007
}
1008
if let Some(err) = parse_error() {
1009
span { class: "field-error", " ❌ {err}" }
···
1158
}
1159
}
1160
1161
+
/// Blob field - shows CID, size (editable), mime type (read-only)
1162
+
#[component]
1163
+
fn EditableBlobField(
1164
+
root: Signal<Data<'static>>,
1165
+
path: String,
1166
+
did: String,
1167
+
#[props(default)] remove_button: Option<Element>,
1168
+
) -> Element {
1169
+
let path_for_memo = path.clone();
1170
+
let blob_data = use_memo(move || {
1171
+
root.read()
1172
+
.get_at_path(&path_for_memo)
1173
+
.and_then(|d| match d {
1174
+
Data::Blob(blob) => Some((
1175
+
blob.r#ref.to_string(),
1176
+
blob.size,
1177
+
blob.mime_type.as_str().to_string(),
1178
+
)),
1179
+
_ => None,
1180
+
})
1181
+
});
1182
+
1183
+
let mut cid_input = use_signal(|| String::new());
1184
+
let mut size_input = use_signal(|| String::new());
1185
+
let mut cid_error = use_signal(|| None::<String>);
1186
+
let mut size_error = use_signal(|| None::<String>);
1187
+
1188
+
// Sync inputs when blob data changes
1189
+
use_effect(move || {
1190
+
if let Some((cid, size, _)) = blob_data() {
1191
+
cid_input.set(cid);
1192
+
size_input.set(size.to_string());
1193
+
}
1194
+
});
1195
+
1196
+
let path_for_cid = path.clone();
1197
+
let handle_cid_change = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| {
1198
+
let text = evt.value();
1199
+
cid_input.set(text.clone());
1200
+
1201
+
match jacquard::types::cid::CidLink::new_owned(text.as_bytes()) {
1202
+
Ok(new_cid_link) => {
1203
+
cid_error.set(None);
1204
+
root.with_mut(|data| {
1205
+
if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_cid) {
1206
+
blob.r#ref = new_cid_link;
1207
+
}
1208
+
});
1209
+
}
1210
+
Err(_) => {
1211
+
cid_error.set(Some("Invalid CID format".to_string()));
1212
+
}
1213
+
}
1214
+
};
1215
+
1216
+
let path_for_size = path.clone();
1217
+
let handle_size_change = move |evt: dioxus::prelude::Event<dioxus::prelude::FormData>| {
1218
+
let text = evt.value();
1219
+
size_input.set(text.clone());
1220
+
1221
+
match text.parse::<usize>() {
1222
+
Ok(new_size) => {
1223
+
size_error.set(None);
1224
+
root.with_mut(|data| {
1225
+
if let Some(Data::Blob(blob)) = data.get_at_path_mut(&path_for_size) {
1226
+
blob.size = new_size;
1227
+
}
1228
+
});
1229
+
}
1230
+
Err(_) => {
1231
+
size_error.set(Some("Must be a non-negative integer".to_string()));
1232
+
}
1233
+
}
1234
+
};
1235
+
1236
+
let placeholder_cid = "bafkreiaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
1237
+
let is_placeholder = blob_data()
1238
+
.map(|(cid, _, _)| cid == placeholder_cid)
1239
+
.unwrap_or(true);
1240
+
let is_image = blob_data()
1241
+
.map(|(_, _, mime)| mime.starts_with("image/"))
1242
+
.unwrap_or(false);
1243
+
1244
+
let image_url = if !is_placeholder && is_image {
1245
+
blob_data().map(|(cid, _, mime)| {
1246
+
let format = mime.strip_prefix("image/").unwrap_or("jpeg");
1247
+
format!(
1248
+
"https://cdn.bsky.app/img/feed_fullsize/plain/{}/{}@{}",
1249
+
did,
1250
+
cid,
1251
+
format
1252
+
)
1253
+
})
1254
+
} else {
1255
+
None
1256
+
};
1257
+
1258
+
rsx! {
1259
+
div { class: "record-field blob-field",
1260
+
div { class: "field-header",
1261
+
PathLabel { path: path.clone() }
1262
+
span { class: "string-type-tag", " [blob]" }
1263
+
{remove_button}
1264
+
}
1265
+
div { class: "blob-fields",
1266
+
div { class: "blob-field-row blob-field-cid",
1267
+
label { "CID:" }
1268
+
input {
1269
+
r#type: "text",
1270
+
value: "{cid_input}",
1271
+
oninput: handle_cid_change,
1272
+
class: if cid_error().is_some() { "invalid" } else { "" },
1273
+
}
1274
+
if let Some(err) = cid_error() {
1275
+
span { class: "field-error", " ❌ {err}" }
1276
+
}
1277
+
}
1278
+
div { class: "blob-field-row",
1279
+
label { "Size:" }
1280
+
input {
1281
+
r#type: "number",
1282
+
value: "{size_input}",
1283
+
oninput: handle_size_change,
1284
+
class: if size_error().is_some() { "invalid" } else { "" },
1285
+
}
1286
+
if let Some(err) = size_error() {
1287
+
span { class: "field-error", " ❌ {err}" }
1288
+
}
1289
+
}
1290
+
div { class: "blob-field-row",
1291
+
label { "MIME Type:" }
1292
+
span { class: "readonly",
1293
+
"{blob_data().map(|(_, _, mime)| mime).unwrap_or_default()}"
1294
+
}
1295
+
}
1296
+
if let Some(url) = image_url {
1297
+
img {
1298
+
src: "{url}",
1299
+
alt: "Blob preview",
1300
+
class: "blob-image",
1301
+
}
1302
+
}
1303
+
if is_placeholder {
1304
+
div { class: "muted blob-upload-note",
1305
+
"File upload coming soon"
1306
+
}
1307
+
}
1308
+
}
1309
+
}
1310
+
}
1311
+
}
1312
+
1313
// ============================================================================
1314
// Field with Remove Button Wrapper
1315
// ============================================================================
···
1353
}
1354
1355
// ============================================================================
1356
+
// Array Field Editor (enables recursion)
1357
+
// ============================================================================
1358
+
1359
+
/// Array field - iterates items and renders child EditableDataView for each
1360
+
#[component]
1361
+
fn EditableArrayField(root: Signal<Data<'static>>, path: String, did: String) -> Element {
1362
+
let path_for_memo = path.clone();
1363
+
let array_len = use_memo(move || {
1364
+
root.read()
1365
+
.get_at_path(&path_for_memo)
1366
+
.and_then(|d| d.as_array())
1367
+
.map(|arr| arr.0.len())
1368
+
.unwrap_or(0)
1369
+
});
1370
+
1371
+
let path_for_add = path.clone();
1372
+
1373
+
rsx! {
1374
+
div { class: "record-section array-section",
1375
+
div { class: "section-header",
1376
+
div { class: "section-label",
1377
+
{
1378
+
let parts: Vec<&str> = path.split('.').collect();
1379
+
let final_part = parts.last().unwrap_or(&"");
1380
+
rsx! { "{final_part}" }
1381
+
}
1382
+
}
1383
+
span { class: "array-length", "[{array_len}]" }
1384
+
}
1385
+
1386
+
div { class: "section-content",
1387
+
for idx in 0..array_len() {
1388
+
{
1389
+
let item_path = format!("{}[{}]", path, idx);
1390
+
let path_for_remove = path.clone();
1391
+
1392
+
rsx! {
1393
+
div {
1394
+
class: "array-item",
1395
+
key: "{item_path}",
1396
+
1397
+
EditableDataView {
1398
+
root: root,
1399
+
path: item_path.clone(),
1400
+
did: did.clone(),
1401
+
remove_button: rsx! {
1402
+
button {
1403
+
class: "field-remove-button",
1404
+
onclick: move |_| {
1405
+
root.with_mut(|data| {
1406
+
if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_remove) {
1407
+
arr.0.remove(idx);
1408
+
}
1409
+
});
1410
+
},
1411
+
"Remove"
1412
+
}
1413
+
}
1414
+
}
1415
+
}
1416
+
}
1417
+
}
1418
+
}
1419
+
div {
1420
+
class: "add-field-widget",
1421
+
button {
1422
+
1423
+
onclick: move |_| {
1424
+
root.with_mut(|data| {
1425
+
if let Some(Data::Array(arr)) = data.get_at_path_mut(&path_for_add) {
1426
+
let new_item = create_array_item_default(arr);
1427
+
arr.0.push(new_item);
1428
+
}
1429
+
});
1430
+
},
1431
+
"+ Add Item"
1432
+
}
1433
+
}
1434
+
1435
+
1436
+
}
1437
+
}
1438
+
}
1439
+
}
1440
+
1441
+
// ============================================================================
1442
// Object Field Editor (enables recursion)
1443
// ============================================================================
1444
1445
/// Object field - iterates fields and renders child EditableDataView for each
1446
#[component]
1447
+
fn EditableObjectField(
1448
+
root: Signal<Data<'static>>,
1449
+
path: String,
1450
+
did: String,
1451
+
#[props(default)] remove_button: Option<Element>,
1452
+
) -> Element {
1453
let path_for_memo = path.clone();
1454
let field_keys = use_memo(move || {
1455
root.read()
···
1464
rsx! {
1465
if !is_root {
1466
div { class: "record-section object-section",
1467
+
div { class: "section-header",
1468
+
div { class: "section-label",
1469
+
{
1470
+
let parts: Vec<&str> = path.split('.').collect();
1471
+
let final_part = parts.last().unwrap_or(&"");
1472
+
rsx! { "{final_part}" }
1473
+
}
1474
}
1475
+
{remove_button}
1476
}
1477
div { class: "section-content",
1478
for key in field_keys() {