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
4079
[[package]]
4080
4080
name = "jacquard"
4081
4081
version = "0.9.0"
4082
4082
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4083
4082
dependencies = [
4084
4083
"bytes",
4085
4084
"getrandom 0.2.16",
···
4109
4108
[[package]]
4110
4109
name = "jacquard-api"
4111
4110
version = "0.9.0"
4112
4112
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4113
4111
dependencies = [
4114
4112
"bon",
4115
4113
"bytes",
···
4127
4125
[[package]]
4128
4126
name = "jacquard-axum"
4129
4127
version = "0.9.0"
4130
4130
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4131
4128
dependencies = [
4132
4129
"axum",
4133
4130
"bytes",
···
4149
4146
[[package]]
4150
4147
name = "jacquard-common"
4151
4148
version = "0.9.0"
4152
4152
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4153
4149
dependencies = [
4154
4150
"base64 0.22.1",
4155
4151
"bon",
···
4192
4188
[[package]]
4193
4189
name = "jacquard-derive"
4194
4190
version = "0.9.0"
4195
4195
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4196
4191
dependencies = [
4197
4192
"heck 0.5.0",
4198
4193
"jacquard-lexicon",
···
4204
4199
[[package]]
4205
4200
name = "jacquard-identity"
4206
4201
version = "0.9.1"
4207
4207
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4208
4202
dependencies = [
4209
4203
"bon",
4210
4204
"bytes",
···
4232
4226
[[package]]
4233
4227
name = "jacquard-lexicon"
4234
4228
version = "0.9.1"
4235
4235
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4236
4229
dependencies = [
4237
4230
"cid",
4238
4231
"dashmap",
···
4258
4251
[[package]]
4259
4252
name = "jacquard-oauth"
4260
4253
version = "0.9.0"
4261
4261
-
source = "git+https://tangled.org/@nonbinary.computer/jacquard#5c79bb76de544cbd4fa8d5d8b01ba6e828f8ba65"
4262
4254
dependencies = [
4263
4255
"base64 0.22.1",
4264
4256
"bytes",
+12
-12
Cargo.toml
···
40
40
markdown-weaver = { git = "https://github.com/rsform/markdown-weaver" }
41
41
markdown-weaver-escape = { git = "https://github.com/rsform/markdown-weaver" }
42
42
43
43
-
jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] }
44
44
-
jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
45
45
-
jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
46
46
-
jacquard-axum = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
47
47
-
jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
48
48
-
jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false }
43
43
+
# jacquard = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] }
44
44
+
# jacquard-api = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
45
45
+
# jacquard-common = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
46
46
+
# jacquard-axum = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
47
47
+
# jacquard-derive = { git = "https://tangled.org/@nonbinary.computer/jacquard" }
48
48
+
# jacquard-lexicon = { git = "https://tangled.org/@nonbinary.computer/jacquard", default-features = false }
49
49
50
50
-
# jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] }
51
51
-
# jacquard-api = { path = "../jacquard/crates/jacquard-api" }
52
52
-
# jacquard-common = { path = "../jacquard/crates/jacquard-common" }
53
53
-
# jacquard-axum = {path = "../jacquard/crates/jacquard-axum" }
54
54
-
# jacquard-derive = { path = "../jacquard/crates/jacquard-derive" }
55
55
-
# jacquard-lexicon = { path = "../jacquard/crates/jacquard-lexicon", default-features = false }
50
50
+
jacquard = { path = "../jacquard/crates/jacquard", default-features = false, features = ["derive", "api_bluesky", "tracing"] }
51
51
+
jacquard-api = { path = "../jacquard/crates/jacquard-api" }
52
52
+
jacquard-common = { path = "../jacquard/crates/jacquard-common" }
53
53
+
jacquard-axum = {path = "../jacquard/crates/jacquard-axum" }
54
54
+
jacquard-derive = { path = "../jacquard/crates/jacquard-derive" }
55
55
+
jacquard-lexicon = { path = "../jacquard/crates/jacquard-lexicon", default-features = false }
56
56
57
57
[profile]
58
58
+1
-2
crates/weaver-app/Cargo.toml
···
20
20
dashmap = "6.1.0"
21
21
22
22
dioxus = { version = "0.7.1", features = ["router"] }
23
23
+
#dioxus-router = { version = "0.7.1", features = ["wasm-split"] }
23
24
weaver-common = { path = "../weaver-common" }
24
25
jacquard = { workspace = true, features = ["streaming"] }
25
26
jacquard-lexicon = { workspace = true }
···
48
49
serde_html_form = "0.2.8"
49
50
webbrowser = "1.0.6"
50
51
tracing.workspace = true
51
51
-
52
52
-
53
52
54
53
55
54
[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies]
+30
-7
crates/weaver-app/assets/styling/record-view.css
···
1
1
.record-view-container {
2
2
-
font-family: var(--font-mono);
3
2
max-width: 1200px;
4
3
margin: 2rem auto;
5
4
padding: 0 1rem;
···
8
7
.record-header {
9
8
margin-bottom: 2rem;
10
9
padding-bottom: 0.5rem;
10
10
+
11
11
+
font-family: var(--font-mono);
11
12
border-bottom: 1px solid var(--color-border);
12
13
}
13
14
···
62
63
}
63
64
64
65
.metadata-label {
66
66
+
font-family: var(--font-mono);
65
67
color: var(--color-subtle);
66
68
font-size: 0.85rem;
67
69
text-transform: uppercase;
···
612
614
border-bottom-color: var(--color-error, #ff6b6b);
613
615
}
614
616
615
615
-
.record-field input[type="checkbox"] {
616
616
-
width: 1.1rem;
617
617
-
height: 1.1rem;
618
618
-
margin-top: 0.3rem;
619
619
-
margin-bottom: 0.3rem;
617
617
+
.boolean-toggle {
618
618
+
font-family: var(--font-mono);
619
619
+
font-size: 0.9rem;
620
620
+
color: var(--color-text);
621
621
+
background: var(--color-surface);
622
622
+
border: 1px solid var(--color-border);
623
623
+
padding: 0.25rem 0.25rem;
624
624
+
margin-right: 0.5rem;
625
625
+
margin-bottom: 0.2rem;
620
626
cursor: pointer;
621
621
-
accent-color: var(--color-primary);
627
627
+
transition:
628
628
+
background-color 0.2s,
629
629
+
border-color 0.2s;
630
630
+
}
631
631
+
632
632
+
.boolean-toggle-false {
633
633
+
color: var(--color-error);
634
634
+
border: 1px solid var(--color-error);
635
635
+
}
636
636
+
637
637
+
.boolean-toggle-true {
638
638
+
border: 1px solid var(--color-success);
639
639
+
}
640
640
+
641
641
+
.boolean-toggle:hover {
642
642
+
border: 1px solid var(--color-primary);
643
643
+
background-color: var(--color-primary);
644
644
+
color: var(--color-surface);
622
645
}
623
646
624
647
.field-error {
+1
-1
crates/weaver-app/src/components/login.rs
···
37
37
use crate::Route;
38
38
use gloo_storage::Storage;
39
39
let full_route = use_route::<Route>();
40
40
-
gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route));
40
40
+
gloo_storage::LocalStorage::set("cached_route", format!("{}", full_route)).ok();
41
41
}
42
42
43
43
use_effect(move || {
+3
-3
crates/weaver-app/src/data.rs
···
8
8
use dioxus::prelude::*;
9
9
#[cfg(feature = "fullstack-server")]
10
10
#[allow(unused_imports)]
11
11
-
use dioxus::{fullstack::extract::Extension, CapturedError};
11
11
+
use dioxus::{CapturedError, fullstack::extract::Extension};
12
12
use jacquard::types::{did::Did, string::Handle};
13
13
#[allow(unused_imports)]
14
14
use jacquard::{
···
18
18
};
19
19
#[allow(unused_imports)]
20
20
use std::sync::Arc;
21
21
-
use weaver_api::sh_weaver::notebook::{entry::Entry, BookEntryView};
21
21
+
use weaver_api::sh_weaver::notebook::{BookEntryView, entry::Entry};
22
22
// ============================================================================
23
23
// Wrapper Hooks (feature-gated)
24
24
// ============================================================================
···
216
216
async fn render_markdown_impl(content: Entry<'static>, did: Did<'static>) -> String {
217
217
use n0_future::stream::StreamExt;
218
218
use weaver_renderer::{
219
219
-
atproto::{ClientContext, ClientWriter},
220
219
ContextIterator, NotebookProcessor,
220
220
+
atproto::{ClientContext, ClientWriter},
221
221
};
222
222
223
223
let ctx = ClientContext::<()>::new(content.clone(), did);
+1
-1
crates/weaver-app/src/main.rs
···
65
65
#[layout(RecordIndex)]
66
66
#[route("/:..uri")]
67
67
RecordView { uri: Vec<String> },
68
68
-
#[end_layout]
68
68
+
#[end_layout]
69
69
#[end_nest]
70
70
#[route("/callback?:state&:iss&:code")]
71
71
Callback { state: SmolStr, iss: SmolStr, code: SmolStr },
-1
crates/weaver-app/src/service_worker.rs
···
3
3
4
4
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
5
5
use wasm_bindgen_futures::JsFuture;
6
6
-
7
6
#[cfg(all(target_family = "wasm", target_os = "unknown"))]
8
7
use web_sys::{RegistrationOptions, ServiceWorkerContainer, Window};
9
8
+105
-27
crates/weaver-app/src/views/record.rs
···
6
6
use humansize::format_size;
7
7
use jacquard::api::com_atproto::repo::get_record::GetRecordOutput;
8
8
use jacquard::client::AgentError;
9
9
+
use jacquard::common::to_data;
9
10
use jacquard::prelude::*;
10
11
use jacquard::smol_str::ToSmolStr;
11
12
use jacquard::{
···
14
15
identity::lexicon_resolver::LexiconSchemaResolver,
15
16
types::{aturi::AtUri, cid::Cid, ident::AtIdentifier, string::Nsid},
16
17
};
18
18
+
use jacquard_lexicon::lexicon::LexiconDoc;
17
19
use mime_sniffer::MimeTypeSniffer;
18
20
use weaver_api::com_atproto::repo::{
19
21
create_record::CreateRecord, delete_record::DeleteRecord, put_record::PutRecord,
···
24
26
enum ViewMode {
25
27
Pretty,
26
28
Json,
29
29
+
Schema,
27
30
}
28
31
29
32
#[component]
30
33
pub fn RecordIndex() -> Element {
31
34
let navigator = use_navigator();
32
35
let mut uri_input = use_signal(|| String::new());
33
33
-
34
36
let handle_uri_submit = move || {
35
37
let input_uri = uri_input.read().clone();
36
38
if !input_uri.is_empty() {
···
85
87
async move { client.fetch_record_slingshot(&*uri.read()).await }
86
88
});
87
89
90
90
+
// Fetch schema for the record
91
91
+
let schema_resource = use_resource(move || {
92
92
+
let fetcher = fetcher.clone();
93
93
+
async move {
94
94
+
let record_read = record_resource.read();
95
95
+
let record = record_read.as_ref()?.as_ref().ok()?;
96
96
+
97
97
+
let validator = jacquard_lexicon::validation::SchemaValidator::global();
98
98
+
let main_type = record.value.type_discriminator();
99
99
+
let mut main_schema = None;
100
100
+
101
101
+
// Find and resolve all schemas (including main and nested)
102
102
+
for type_val in record.value.query("...$type").values() {
103
103
+
if let Some(type_str) = type_val.as_str() {
104
104
+
// Skip non-NSID types (like "blob")
105
105
+
if !type_str.contains('.') {
106
106
+
continue;
107
107
+
}
108
108
+
109
109
+
if let Ok(nsid) = Nsid::new(type_str) {
110
110
+
// Fetch and register schema
111
111
+
if let Ok(schema) = fetcher.resolve_lexicon_schema(&nsid).await {
112
112
+
validator
113
113
+
.registry()
114
114
+
.insert(nsid.to_smolstr(), schema.doc.clone());
115
115
+
116
116
+
// Keep the main record schema
117
117
+
if Some(type_str) == main_type {
118
118
+
main_schema = Some(schema.doc);
119
119
+
}
120
120
+
}
121
121
+
}
122
122
+
}
123
123
+
}
124
124
+
125
125
+
main_schema
126
126
+
}
127
127
+
});
128
128
+
129
129
+
let schema_signal = use_memo(move || schema_resource.read().clone().flatten());
130
130
+
88
131
// Check ownership for edit access
89
132
let auth_state = use_context::<Signal<AuthState>>();
90
133
let is_owner = use_memo(move || {
···
119
162
view_mode: view_mode,
120
163
edit_mode: edit_mode,
121
164
record_resource: record_resource,
165
165
+
schema: schema_signal,
122
166
}
123
167
} else {
124
168
div {
···
133
177
onclick: move |_| view_mode.set(ViewMode::Json),
134
178
"JSON"
135
179
}
180
180
+
button {
181
181
+
class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" },
182
182
+
onclick: move |_| view_mode.set(ViewMode::Schema),
183
183
+
"Schema"
184
184
+
}
136
185
if is_owner() {
137
186
button {
138
187
class: "tab-button edit-button",
···
158
207
lang: Some("json".to_string()),
159
208
}
160
209
}
210
210
+
},
211
211
+
ViewMode::Schema => rsx! {
212
212
+
SchemaView { schema: schema_signal }
161
213
},
162
214
}
163
215
}
···
180
232
}
181
233
}
182
234
}
235
235
+
236
236
+
#[component]
237
237
+
fn SchemaView(schema: ReadSignal<Option<LexiconDoc<'static>>>) -> Element {
238
238
+
if let Some(schema_doc) = schema() {
239
239
+
// Convert LexiconDoc to Data for display
240
240
+
let schema_data = use_memo(move || to_data(&schema_doc).ok().map(|d| d.into_static()));
241
241
+
242
242
+
if let Some(data) = schema_data() {
243
243
+
rsx! {
244
244
+
div {
245
245
+
class: "pretty-record",
246
246
+
DataView { data: data, path: String::new(), did: String::new() }
247
247
+
}
248
248
+
}
249
249
+
} else {
250
250
+
rsx! {
251
251
+
div { class: "schema-error", "Failed to convert schema to displayable format" }
252
252
+
}
253
253
+
}
254
254
+
} else {
255
255
+
rsx! {
256
256
+
div { class: "schema-loading", "Loading schema..." }
257
257
+
}
258
258
+
}
259
259
+
}
183
260
fn get_hex_rep(byte_array: &mut [u8]) -> String {
184
261
let build_string_vec: Vec<String> = byte_array
185
262
.chunks(2)
···
563
640
}
564
641
565
642
#[component]
566
566
-
fn JsonEditor(data: Signal<Data<'static>>, nsid: ReadSignal<Option<String>>) -> Element {
643
643
+
fn JsonEditor(
644
644
+
data: Signal<Data<'static>>,
645
645
+
nsid: ReadSignal<Option<String>>,
646
646
+
schema: ReadSignal<Option<LexiconDoc<'static>>>,
647
647
+
) -> Element {
567
648
let mut json_text =
568
649
use_signal(|| serde_json::to_string_pretty(&*data.read()).unwrap_or_default());
569
569
-
let mut parse_error = use_signal(|| None::<String>);
570
650
571
651
let height = use_memo(move || {
572
652
let line_count = json_text().lines().count();
···
577
657
format!("{}px", lines * 22 + 32)
578
658
});
579
659
580
580
-
let fetcher = use_context::<CachedFetcher>();
581
581
-
582
660
let validation = use_resource(move || {
583
661
let text = json_text();
584
662
let nsid_val = nsid();
585
585
-
let fetcher = fetcher.clone();
663
663
+
let _ = schema(); // Track schema changes
586
664
587
665
async move {
588
666
// Only validate if we have an NSID
···
596
674
}
597
675
};
598
676
599
599
-
// Resolve lexicon if needed
600
600
-
let registry = jacquard_lexicon::schema::SchemaRegistry::from_inventory();
601
601
-
if registry.get(&nsid_str).is_none() {
602
602
-
let nsid_str = nsid_str.split('#').next();
603
603
-
if let Some(Ok(nsid_parsed)) = nsid_str.map(|s| Nsid::new(s)) {
604
604
-
if let Ok(schema) = fetcher.resolve_lexicon_schema(&nsid_parsed).await {
605
605
-
registry.insert(nsid_parsed.to_smolstr(), schema.doc);
606
606
-
}
607
607
-
}
608
608
-
}
609
609
-
610
610
-
// Validate
611
611
-
let validator = jacquard_lexicon::validation::SchemaValidator::from_registry(registry);
677
677
+
// Use global validator (schema already registered)
678
678
+
let validator = jacquard_lexicon::validation::SchemaValidator::global();
612
679
let result = validator.validate_by_nsid(&nsid_str, &parsed);
613
680
614
681
Some((Some(result), None))
···
1133
1200
}
1134
1201
}
1135
1202
1136
1136
-
/// Boolean field (checkbox)
1203
1203
+
/// Boolean field (toggle button)
1137
1204
#[component]
1138
1205
fn EditableBooleanField(
1139
1206
root: Signal<Data<'static>>,
···
1155
1222
PathLabel { path: path.clone() }
1156
1223
{remove_button}
1157
1224
}
1158
1158
-
input {
1159
1159
-
r#type: "checkbox",
1160
1160
-
checked: current_value(),
1161
1161
-
onchange: move |evt| {
1225
1225
+
button {
1226
1226
+
class: if current_value() { "boolean-toggle boolean-toggle-true" } else { "boolean-toggle boolean-toggle-false" },
1227
1227
+
onclick: move |_| {
1162
1228
root.with_mut(|data| {
1163
1229
if let Some(target) = data.get_at_path_mut(path_for_mutation.as_str()) {
1164
1164
-
*target = Data::Boolean(evt.checked());
1230
1230
+
if let Some(bool_val) = target.as_boolean() {
1231
1231
+
*target = Data::Boolean(!bool_val);
1232
1232
+
}
1165
1233
}
1166
1234
});
1167
1167
-
}
1235
1235
+
},
1236
1236
+
"{current_value()}"
1168
1237
}
1169
1238
}
1170
1239
}
···
2133
2202
view_mode: Signal<ViewMode>,
2134
2203
edit_mode: Signal<bool>,
2135
2204
record_resource: Resource<Result<GetRecordOutput<'static>, AgentError>>,
2205
2205
+
schema: ReadSignal<Option<LexiconDoc<'static>>>,
2136
2206
) -> Element {
2137
2207
let mut edit_data = use_signal(use_reactive!(|record_value| record_value.clone()));
2138
2208
let nsid = use_memo(move || edit_data().type_discriminator().map(|s| s.to_string()));
···
2156
2226
class: if view_mode() == ViewMode::Json { "tab-button active" } else { "tab-button" },
2157
2227
onclick: move |_| view_mode.set(ViewMode::Json),
2158
2228
"JSON"
2229
2229
+
}
2230
2230
+
button {
2231
2231
+
class: if view_mode() == ViewMode::Schema { "tab-button active" } else { "tab-button" },
2232
2232
+
onclick: move |_| view_mode.set(ViewMode::Schema),
2233
2233
+
"Schema"
2159
2234
}
2160
2235
ActionButtons {
2161
2236
on_update: move |_| {
···
2323
2398
}
2324
2399
},
2325
2400
ViewMode::Json => rsx! {
2326
2326
-
JsonEditor { data: edit_data, nsid }
2401
2401
+
JsonEditor { data: edit_data, nsid, schema }
2402
2402
+
},
2403
2403
+
ViewMode::Schema => rsx! {
2404
2404
+
SchemaView { schema }
2327
2405
},
2328
2406
}
2329
2407
}