tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
fixed lexicon resolution, styling
Orual
2 months ago
9538a97b
d8f38532
+153
-62
9 changed files
expand all
collapse all
unified
split
Cargo.lock
Cargo.toml
crates
weaver-app
Cargo.toml
assets
styling
record-view.css
src
components
login.rs
data.rs
main.rs
service_worker.rs
views
record.rs
-8
Cargo.lock
···
4079
[[package]]
4080
name = "jacquard"
4081
version = "0.9.0"
4082
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4083
dependencies = [
4084
"bytes",
4085
"getrandom 0.2.16",
···
4109
[[package]]
4110
name = "jacquard-api"
4111
version = "0.9.0"
4112
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4113
dependencies = [
4114
"bon",
4115
"bytes",
···
4127
[[package]]
4128
name = "jacquard-axum"
4129
version = "0.9.0"
4130
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4131
dependencies = [
4132
"axum",
4133
"bytes",
···
4149
[[package]]
4150
name = "jacquard-common"
4151
version = "0.9.0"
4152
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4153
dependencies = [
4154
"base64 0.22.1",
4155
"bon",
···
4192
[[package]]
4193
name = "jacquard-derive"
4194
version = "0.9.0"
4195
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4196
dependencies = [
4197
"heck 0.5.0",
4198
"jacquard-lexicon",
···
4204
[[package]]
4205
name = "jacquard-identity"
4206
version = "0.9.1"
4207
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4208
dependencies = [
4209
"bon",
4210
"bytes",
···
4232
[[package]]
4233
name = "jacquard-lexicon"
4234
version = "0.9.1"
4235
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4236
dependencies = [
4237
"cid",
4238
"dashmap",
···
4258
[[package]]
4259
name = "jacquard-oauth"
4260
version = "0.9.0"
4261
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4262
dependencies = [
4263
"base64 0.22.1",
4264
"bytes",
···
4079
[[package]]
4080
name = "jacquard"
4081
version = "0.9.0"
0
4082
dependencies = [
4083
"bytes",
4084
"getrandom 0.2.16",
···
4108
[[package]]
4109
name = "jacquard-api"
4110
version = "0.9.0"
0
4111
dependencies = [
4112
"bon",
4113
"bytes",
···
4125
[[package]]
4126
name = "jacquard-axum"
4127
version = "0.9.0"
0
4128
dependencies = [
4129
"axum",
4130
"bytes",
···
4146
[[package]]
4147
name = "jacquard-common"
4148
version = "0.9.0"
0
4149
dependencies = [
4150
"base64 0.22.1",
4151
"bon",
···
4188
[[package]]
4189
name = "jacquard-derive"
4190
version = "0.9.0"
0
4191
dependencies = [
4192
"heck 0.5.0",
4193
"jacquard-lexicon",
···
4199
[[package]]
4200
name = "jacquard-identity"
4201
version = "0.9.1"
0
4202
dependencies = [
4203
"bon",
4204
"bytes",
···
4226
[[package]]
4227
name = "jacquard-lexicon"
4228
version = "0.9.1"
0
4229
dependencies = [
4230
"cid",
4231
"dashmap",
···
4251
[[package]]
4252
name = "jacquard-oauth"
4253
version = "0.9.0"
0
4254
dependencies = [
4255
"base64 0.22.1",
4256
"bytes",
+12
-12
Cargo.toml
···
40
markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" }
41
markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" }
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 }
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 }
56
57
[profile]
58
···
40
markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" }
41
markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" }
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 }
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 }
56
57
[profile]
58
+1
-2
crates/weaver-app/Cargo.toml
···
20
dashmap = "6.1.0"
21
22
dioxus = { version = "0.7.1", features = ["router"] }
0
23
weaver-common = { path = "../weaver-common" }
24
jacquard = { workspace = true, features = ["streaming"] }
25
jacquard-lexicon = { workspace = true }
···
48
serde_html_form = "0.2.8"
49
webbrowser = "1.0.6"
50
tracing.workspace = true
51
-
52
-
53
54
55
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
···
20
dashmap = "6.1.0"
21
22
dioxus = { version = "0.7.1", features = ["router"] }
23
+
#dioxus-router = { version = "0.7.1", features = ["wasm-split"] }
24
weaver-common = { path = "../weaver-common" }
25
jacquard = { workspace = true, features = ["streaming"] }
26
jacquard-lexicon = { workspace = true }
···
49
serde_html_form = "0.2.8"
50
webbrowser = "1.0.6"
51
tracing.workspace = true
0
0
52
53
54
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
+30
-7
crates/weaver-app/assets/styling/record-view.css
···
1
.record-view-container {
2
-
font-family: var(--font-mono);
3
max-width: 1200px;
4
margin: 2rem auto;
5
padding: 0 1rem;
···
8
.record-header {
9
margin-bottom: 2rem;
10
padding-bottom: 0.5rem;
0
0
11
border-bottom: 1px solid var(--color-border);
12
}
13
···
62
}
63
64
.metadata-label {
0
65
color: var(--color-subtle);
66
font-size: 0.85rem;
67
text-transform: uppercase;
···
612
border-bottom-color: var(--color-error, #ff6b6b);
613
}
614
615
-
.record-field input[type="checkbox"] {
616
-
width: 1.1rem;
617
-
height: 1.1rem;
618
-
margin-top: 0.3rem;
619
-
margin-bottom: 0.3rem;
0
0
0
0
620
cursor: pointer;
621
-
accent-color: var(--color-primary);
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
622
}
623
624
.field-error {
···
1
.record-view-container {
0
2
max-width: 1200px;
3
margin: 2rem auto;
4
padding: 0 1rem;
···
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
}
14
···
63
}
64
65
.metadata-label {
66
+
font-family: var(--font-mono);
67
color: var(--color-subtle);
68
font-size: 0.85rem;
69
text-transform: uppercase;
···
614
border-bottom-color: var(--color-error, #ff6b6b);
615
}
616
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;
626
cursor: pointer;
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);
645
}
646
647
.field-error {
+1
-1
crates/weaver-app/src/components/login.rs
···
37
use crate::Route;
38
use gloo_storage::Storage;
39
let full_route = use_route::<Route>();
40
-
gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route));
41
}
42
43
use_effect(move || {
···
37
use crate::Route;
38
use gloo_storage::Storage;
39
let full_route = use_route::<Route>();
40
+
gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok();
41
}
42
43
use_effect(move || {
+3
-3
crates/weaver-app/src/data.rs
···
8
use dioxus::prelude::*;
9
#[cfg(feature = "fullstack-server")]
10
#[allow(unused_imports)]
11
-
use dioxus::{fullstack::extract::Extension, CapturedError};
12
use jacquard::types::{did::Did, string::Handle};
13
#[allow(unused_imports)]
14
use jacquard::{
···
18
};
19
#[allow(unused_imports)]
20
use std::sync::Arc;
21
-
use weaver_api::sh_weaver::notebook::{entry::Entry, BookEntryView};
22
// ============================================================================
23
// Wrapper Hooks (feature-gated)
24
// ============================================================================
···
216
async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> String {
217
use n0_future::stream::StreamExt;
218
use weaver_renderer::{
219
-
atproto::{ClientContext, ClientWriter},
220
ContextIterator, NotebookProcessor,
0
221
};
222
223
let ctx = ClientContext::<()>::new(content.clone(), did);
···
8
use dioxus::prelude::*;
9
#[cfg(feature = "fullstack-server")]
10
#[allow(unused_imports)]
11
+
use dioxus::{CapturedError, fullstack::extract::Extension};
12
use jacquard::types::{did::Did, string::Handle};
13
#[allow(unused_imports)]
14
use jacquard::{
···
18
};
19
#[allow(unused_imports)]
20
use std::sync::Arc;
21
+
use weaver_api::sh_weaver::notebook::{BookEntryView, entry::Entry};
22
// ============================================================================
23
// Wrapper Hooks (feature-gated)
24
// ============================================================================
···
216
async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> String {
217
use n0_future::stream::StreamExt;
218
use weaver_renderer::{
0
219
ContextIterator, NotebookProcessor,
220
+
atproto::{ClientContext, ClientWriter},
221
};
222
223
let ctx = ClientContext::<()>::new(content.clone(), did);
+1
-1
crates/weaver-app/src/main.rs
···
65
#[layout(RecordIndex)]
66
#[route("/:..uri")]
67
RecordView { uri: Vec<String> },
68
-
#[end_layout]
69
#[end_nest]
70
#[route("/callback?:state&:iss&:code")]
71
Callback { state: SmolStr, iss: SmolStr, code: SmolStr },
···
65
#[layout(RecordIndex)]
66
#[route("/:..uri")]
67
RecordView { uri: Vec<String> },
68
+
#[end_layout]
69
#[end_nest]
70
#[route("/callback?:state&:iss&:code")]
71
Callback { state: SmolStr, iss: SmolStr, code: SmolStr },
-1
crates/weaver-app/src/service_worker.rs
···
3
4
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
5
use wasm_bindgen_futures::JsFuture;
6
-
7
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
8
use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window};
9
···
3
4
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
5
use wasm_bindgen_futures::JsFuture;
0
6
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7
use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window};
8
+105
-27
crates/weaver-app/src/views/record.rs
···
6
use humansize::format_size;
7
use jacquard::api::com_atproto::repo::get_record::GetRecordOutput;
8
use jacquard::client::AgentError;
0
9
use jacquard::prelude::*;
10
use jacquard::smol_str::ToSmolStr;
11
use jacquard::{
···
14
identity::lexicon_resolver::LexiconSchemaResolver,
15
types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid},
16
};
0
17
use mime_sniffer::MimeTypeSniffer;
18
use weaver_api::com_atproto::repo::{
19
create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord,
···
24
enum ViewMode {
25
Pretty,
26
Json,
0
27
}
28
29
#[component]
30
pub fn RecordIndex() -> Element {
31
let navigator = use_navigator();
32
let mut uri_input = use_signal(|| String::new());
33
-
34
let handle_uri_submit = move || {
35
let input_uri = uri_input.read().clone();
36
if !input_uri.is_empty() {
···
85
async move { client.fetch_record_slingshot(&*uri.read()).await }
86
});
87
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
88
// Check ownership for edit access
89
let auth_state = use_context::<Signal<AuthState>>();
90
let is_owner = use_memo(move || {
···
119
view_mode: view_mode,
120
edit_mode: edit_mode,
121
record_resource: record_resource,
0
122
}
123
} else {
124
div {
···
133
onclick: move |_| view_mode.set(ViewMode::Json),
134
"JSON"
135
}
0
0
0
0
0
136
if is_owner() {
137
button {
138
class: "tab-button edit-button",
···
158
lang: Some("json".to_string()),
159
}
160
}
0
0
0
161
},
162
}
163
}
···
180
}
181
}
182
}
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
183
fn get_hex_rep(byte_array: &mut [u8]) -> String {
184
let build_string_vec: Vec<String> = byte_array
185
.chunks(2)
···
563
}
564
565
#[component]
566
-
fn JsonEditor(data: Signal<Data<'static>>, nsid: ReadSignal<Option<String>>) -> Element {
0
0
0
0
567
let mut json_text =
568
use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default());
569
-
let mut parse_error = use_signal(|| None::<String>);
570
571
let height = use_memo(move || {
572
let line_count = json_text().lines().count();
···
577
format!("{}px", lines * 22 + 32)
578
});
579
580
-
let fetcher = use_context::<CachedFetcher>();
581
-
582
let validation = use_resource(move || {
583
let text = json_text();
584
let nsid_val = nsid();
585
-
let fetcher = fetcher.clone();
586
587
async move {
588
// Only validate if we have an NSID
···
596
}
597
};
598
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);
612
let result = validator.validate_by_nsid(&nsid_str, &parsed);
613
614
Some((Some(result), None))
···
1133
}
1134
}
1135
1136
-
/// Boolean field (checkbox)
1137
#[component]
1138
fn EditableBooleanField(
1139
root: Signal<Data<'static>>,
···
1155
PathLabel { path: path.clone() }
1156
{remove_button}
1157
}
1158
-
input {
1159
-
r#type: "checkbox",
1160
-
checked: current_value(),
1161
-
onchange: move |evt| {
1162
root.with_mut(|data| {
1163
if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) {
1164
-
*target = Data::Boolean(evt.checked());
0
0
1165
}
1166
});
1167
-
}
0
1168
}
1169
}
1170
}
···
2133
view_mode: Signal<ViewMode>,
2134
edit_mode: Signal<bool>,
2135
record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>,
0
2136
) -> Element {
2137
let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone()));
2138
let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string()));
···
2156
class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" },
2157
onclick: move |_| view_mode.set(ViewMode::Json),
2158
"JSON"
0
0
0
0
0
2159
}
2160
ActionButtons {
2161
on_update: move |_| {
···
2323
}
2324
},
2325
ViewMode::Json => rsx! {
2326
-
JsonEditor { data: edit_data, nsid }
0
0
0
2327
},
2328
}
2329
}
···
6
use humansize::format_size;
7
use jacquard::api::com_atproto::repo::get_record::GetRecordOutput;
8
use jacquard::client::AgentError;
9
+
use jacquard::common::to_data;
10
use jacquard::prelude::*;
11
use jacquard::smol_str::ToSmolStr;
12
use jacquard::{
···
15
identity::lexicon_resolver::LexiconSchemaResolver,
16
types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid},
17
};
18
+
use jacquard_lexicon::lexicon::LexiconDoc;
19
use mime_sniffer::MimeTypeSniffer;
20
use weaver_api::com_atproto::repo::{
21
create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord,
···
26
enum ViewMode {
27
Pretty,
28
Json,
29
+
Schema,
30
}
31
32
#[component]
33
pub fn RecordIndex() -> Element {
34
let navigator = use_navigator();
35
let mut uri_input = use_signal(|| String::new());
0
36
let handle_uri_submit = move || {
37
let input_uri = uri_input.read().clone();
38
if !input_uri.is_empty() {
···
87
async move { client.fetch_record_slingshot(&*uri.read()).await }
88
});
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
+
131
// Check ownership for edit access
132
let auth_state = use_context::<Signal<AuthState>>();
133
let is_owner = use_memo(move || {
···
162
view_mode: view_mode,
163
edit_mode: edit_mode,
164
record_resource: record_resource,
165
+
schema: schema_signal,
166
}
167
} else {
168
div {
···
177
onclick: move |_| view_mode.set(ViewMode::Json),
178
"JSON"
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
+
}
185
if is_owner() {
186
button {
187
class: "tab-button edit-button",
···
207
lang: Some("json".to_string()),
208
}
209
}
210
+
},
211
+
ViewMode::Schema => rsx! {
212
+
SchemaView { schema: schema_signal }
213
},
214
}
215
}
···
232
}
233
}
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
+
}
260
fn get_hex_rep(byte_array: &mut [u8]) -> String {
261
let build_string_vec: Vec<String> = byte_array
262
.chunks(2)
···
640
}
641
642
#[component]
643
+
fn JsonEditor(
644
+
data: Signal<Data<'static>>,
645
+
nsid: ReadSignal<Option<String>>,
646
+
schema: ReadSignal<Option<LexiconDoc<'static>>>,
647
+
) -> Element {
648
let mut json_text =
649
use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default());
0
650
651
let height = use_memo(move || {
652
let line_count = json_text().lines().count();
···
657
format!("{}px", lines * 22 + 32)
658
});
659
0
0
660
let validation = use_resource(move || {
661
let text = json_text();
662
let nsid_val = nsid();
663
+
let _ = schema(); // Track schema changes
664
665
async move {
666
// Only validate if we have an NSID
···
674
}
675
};
676
677
+
// Use global validator (schema already registered)
678
+
let validator = jacquard_lexicon::validation::SchemaValidator::global();
0
0
0
0
0
0
0
0
0
0
0
679
let result = validator.validate_by_nsid(&nsid_str, &parsed);
680
681
Some((Some(result), None))
···
1200
}
1201
}
1202
1203
+
/// Boolean field (toggle button)
1204
#[component]
1205
fn EditableBooleanField(
1206
root: Signal<Data<'static>>,
···
1222
PathLabel { path: path.clone() }
1223
{remove_button}
1224
}
1225
+
button {
1226
+
class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" },
1227
+
onclick: move |_| {
0
1228
root.with_mut(|data| {
1229
if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) {
1230
+
if let Some(bool_val) = target.as_boolean() {
1231
+
*target = Data::Boolean(!bool_val);
1232
+
}
1233
}
1234
});
1235
+
},
1236
+
"{current_value()}"
1237
}
1238
}
1239
}
···
2202
view_mode: Signal<ViewMode>,
2203
edit_mode: Signal<bool>,
2204
record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>,
2205
+
schema: ReadSignal<Option<LexiconDoc<'static>>>,
2206
) -> Element {
2207
let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone()));
2208
let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string()));
···
2226
class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" },
2227
onclick: move |_| view_mode.set(ViewMode::Json),
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"
2234
}
2235
ActionButtons {
2236
on_update: move |_| {
···
2398
}
2399
},
2400
ViewMode::Json => rsx! {
2401
+
JsonEditor { data: edit_data, nsid, schema }
2402
+
},
2403
+
ViewMode::Schema => rsx! {
2404
+
SchemaView { schema }
2405
},
2406
}
2407
}