new theming scheme, better css, blob cdn endpoints

Orual f219fec8 dd28dcc0

+2928 -505
+38 -5
Cargo.lock
··· 1724 "global-hotkey", 1725 "infer", 1726 "jni", 1727 - "lazy-js-bundle", 1728 "libc", 1729 "muda", 1730 "ndk", ··· 1796 "futures-channel", 1797 "futures-util", 1798 "generational-box", 1799 - "lazy-js-bundle", 1800 "serde", 1801 "serde_json", 1802 "tracing", ··· 1955 "futures-util", 1956 "generational-box", 1957 "keyboard-types", 1958 - "lazy-js-bundle", 1959 "rustversion", 1960 "serde", 1961 "serde_json", ··· 1985 "dioxus-core-types", 1986 "dioxus-html", 1987 "js-sys", 1988 - "lazy-js-bundle", 1989 "rustc-hash 2.1.1", 1990 "serde", 1991 "sledgehammer_bindgen", ··· 2036 ] 2037 2038 [[package]] 2039 name = "dioxus-router" 2040 version = "0.7.0" 2041 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2193 ] 2194 2195 [[package]] 2196 name = "dioxus-web" 2197 version = "0.7.0" 2198 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2213 "generational-box", 2214 "gloo-timers", 2215 "js-sys", 2216 - "lazy-js-bundle", 2217 "rustc-hash 2.1.1", 2218 "send_wrapper", 2219 "serde", ··· 4323 "static-regular-grammar", 4324 "thiserror 1.0.69", 4325 ] 4326 4327 [[package]] 4328 name = "lazy-js-bundle" ··· 8620 "console_error_panic_hook", 8621 "dashmap", 8622 "dioxus", 8623 "jacquard", 8624 "jacquard-axum", 8625 "markdown-weaver", 8626 "mini-moka", 8627 "n0-future", 8628 "time", 8629 "weaver-api", 8630 "weaver-common",
··· 1724 "global-hotkey", 1725 "infer", 1726 "jni", 1727 + "lazy-js-bundle 0.7.0", 1728 "libc", 1729 "muda", 1730 "ndk", ··· 1796 "futures-channel", 1797 "futures-util", 1798 "generational-box", 1799 + "lazy-js-bundle 0.7.0", 1800 "serde", 1801 "serde_json", 1802 "tracing", ··· 1955 "futures-util", 1956 "generational-box", 1957 "keyboard-types", 1958 + "lazy-js-bundle 0.7.0", 1959 "rustversion", 1960 "serde", 1961 "serde_json", ··· 1985 "dioxus-core-types", 1986 "dioxus-html", 1987 "js-sys", 1988 + "lazy-js-bundle 0.7.0", 1989 "rustc-hash 2.1.1", 1990 "serde", 1991 "sledgehammer_bindgen", ··· 2036 ] 2037 2038 [[package]] 2039 + name = "dioxus-primitives" 2040 + version = "0.0.1" 2041 + source = "git+https://github.com/DioxusLabs/components#8e25631c7d4234ee070509156ed2abebb7b1d6e9" 2042 + dependencies = [ 2043 + "dioxus", 2044 + "dioxus-time", 2045 + "lazy-js-bundle 0.6.2", 2046 + "num-integer", 2047 + "time", 2048 + "tracing", 2049 + ] 2050 + 2051 + [[package]] 2052 name = "dioxus-router" 2053 version = "0.7.0" 2054 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2206 ] 2207 2208 [[package]] 2209 + name = "dioxus-time" 2210 + version = "0.7.0" 2211 + source = "git+https://github.com/ealmloff/dioxus-std?branch=0.7#e5a74354cd36be440febdd6b701f584bd552bd62" 2212 + dependencies = [ 2213 + "dioxus", 2214 + "futures", 2215 + "gloo-timers", 2216 + "tokio", 2217 + ] 2218 + 2219 + [[package]] 2220 name = "dioxus-web" 2221 version = "0.7.0" 2222 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2237 "generational-box", 2238 "gloo-timers", 2239 "js-sys", 2240 + "lazy-js-bundle 0.7.0", 2241 "rustc-hash 2.1.1", 2242 "send_wrapper", 2243 "serde", ··· 4347 "static-regular-grammar", 4348 "thiserror 1.0.69", 4349 ] 4350 + 4351 + [[package]] 4352 + name = "lazy-js-bundle" 4353 + version = "0.6.2" 4354 + source = "registry+https://github.com/rust-lang/crates.io-index" 4355 + checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" 4356 4357 [[package]] 4358 name = "lazy-js-bundle" ··· 8650 "console_error_panic_hook", 8651 "dashmap", 8652 "dioxus", 8653 + "dioxus-primitives", 8654 "jacquard", 8655 "jacquard-axum", 8656 "markdown-weaver", 8657 + "mime-sniffer", 8658 "mini-moka", 8659 "n0-future", 8660 + "serde_json", 8661 "time", 8662 "weaver-api", 8663 "weaver-common",
+57
crates/weaver-api/lexicons/com_atproto_lexicon_resolveLexicon.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "com.atproto.lexicon.resolveLexicon", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Resolves an atproto lexicon (NSID) to a schema.", 8 + "parameters": { 9 + "type": "params", 10 + "required": [ 11 + "nsid" 12 + ], 13 + "properties": { 14 + "nsid": { 15 + "type": "string", 16 + "description": "The lexicon NSID to resolve.", 17 + "format": "nsid" 18 + } 19 + } 20 + }, 21 + "output": { 22 + "encoding": "application/json", 23 + "schema": { 24 + "type": "object", 25 + "required": [ 26 + "uri", 27 + "cid", 28 + "schema" 29 + ], 30 + "properties": { 31 + "cid": { 32 + "type": "string", 33 + "description": "The CID of the lexicon schema record.", 34 + "format": "cid" 35 + }, 36 + "schema": { 37 + "type": "ref", 38 + "description": "The resolved lexicon schema record.", 39 + "ref": "com.atproto.lexicon.schema#main" 40 + }, 41 + "uri": { 42 + "type": "string", 43 + "description": "The AT-URI of the lexicon schema record.", 44 + "format": "at-uri" 45 + } 46 + } 47 + } 48 + }, 49 + "errors": [ 50 + { 51 + "description": "No lexicon was resolved for the NSID.", 52 + "name": "LexiconNotFound" 53 + } 54 + ] 55 + } 56 + } 57 + }
+120
crates/weaver-api/lexicons/sh_weaver_notebook_colourScheme.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.weaver.notebook.colourScheme", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A colour palette for notebook theming", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": [ 12 + "name", 13 + "variant", 14 + "colours" 15 + ], 16 + "properties": { 17 + "colours": { 18 + "type": "object", 19 + "required": [ 20 + "base", 21 + "surface", 22 + "overlay", 23 + "text", 24 + "muted", 25 + "subtle", 26 + "emphasis", 27 + "primary", 28 + "secondary", 29 + "tertiary", 30 + "error", 31 + "warning", 32 + "success", 33 + "border", 34 + "link", 35 + "highlight" 36 + ], 37 + "properties": { 38 + "base": { 39 + "type": "string", 40 + "description": "Primary background for page/frame" 41 + }, 42 + "border": { 43 + "type": "string", 44 + "description": "Border/divider colour" 45 + }, 46 + "emphasis": { 47 + "type": "string", 48 + "description": "Emphasized text (bold, important)" 49 + }, 50 + "error": { 51 + "type": "string", 52 + "description": "Error state colour" 53 + }, 54 + "highlight": { 55 + "type": "string", 56 + "description": "Selection/highlight colour" 57 + }, 58 + "link": { 59 + "type": "string", 60 + "description": "Hyperlink colour" 61 + }, 62 + "muted": { 63 + "type": "string", 64 + "description": "De-emphasized text (disabled, metadata)" 65 + }, 66 + "overlay": { 67 + "type": "string", 68 + "description": "Tertiary background for popovers/dialogs" 69 + }, 70 + "primary": { 71 + "type": "string", 72 + "description": "Primary brand/accent colour" 73 + }, 74 + "secondary": { 75 + "type": "string", 76 + "description": "Secondary accent colour" 77 + }, 78 + "subtle": { 79 + "type": "string", 80 + "description": "Medium emphasis text (comments, labels)" 81 + }, 82 + "success": { 83 + "type": "string", 84 + "description": "Success state colour" 85 + }, 86 + "surface": { 87 + "type": "string", 88 + "description": "Secondary background for panels/cards" 89 + }, 90 + "tertiary": { 91 + "type": "string", 92 + "description": "Tertiary accent colour" 93 + }, 94 + "text": { 95 + "type": "string", 96 + "description": "Primary readable text colour" 97 + }, 98 + "warning": { 99 + "type": "string", 100 + "description": "Warning state colour" 101 + } 102 + } 103 + }, 104 + "name": { 105 + "type": "string", 106 + "description": "Human-readable name for the colour scheme" 107 + }, 108 + "variant": { 109 + "type": "string", 110 + "description": "Whether this is a dark or light colour scheme", 111 + "enum": [ 112 + "dark", 113 + "light" 114 + ] 115 + } 116 + } 117 + } 118 + } 119 + } 120 + }
+23 -33
crates/weaver-api/lexicons/sh_weaver_notebook_theme.json
··· 36 "record": { 37 "type": "object", 38 "required": [ 39 - "colours", 40 "fonts", 41 "spacing", 42 - "codeTheme" 43 ], 44 "properties": { 45 - "codeTheme": { 46 "type": "union", 47 "refs": [ 48 "#codeThemeName", 49 "#codeThemeFile" 50 ] 51 }, 52 - "colours": { 53 - "type": "object", 54 - "required": [ 55 - "background", 56 - "foreground", 57 - "primary", 58 - "secondary", 59 - "link", 60 - "link_hover" 61 - ], 62 - "properties": { 63 - "background": { 64 - "type": "string" 65 - }, 66 - "foreground": { 67 - "type": "string" 68 - }, 69 - "link": { 70 - "type": "string" 71 - }, 72 - "link_hover": { 73 - "type": "string" 74 - }, 75 - "primary": { 76 - "type": "string" 77 - }, 78 - "secondary": { 79 - "type": "string" 80 - } 81 - } 82 }, 83 "fonts": { 84 "type": "object", ··· 98 "type": "string" 99 } 100 } 101 }, 102 "spacing": { 103 "type": "object",
··· 36 "record": { 37 "type": "object", 38 "required": [ 39 + "darkScheme", 40 + "lightScheme", 41 "fonts", 42 "spacing", 43 + "darkCodeTheme", 44 + "lightCodeTheme" 45 ], 46 "properties": { 47 + "darkCodeTheme": { 48 "type": "union", 49 + "description": "Syntax highlighting theme for dark mode", 50 "refs": [ 51 "#codeThemeName", 52 "#codeThemeFile" 53 ] 54 }, 55 + "darkScheme": { 56 + "type": "ref", 57 + "description": "Reference to a dark colour scheme", 58 + "ref": "com.atproto.repo.strongRef" 59 }, 60 "fonts": { 61 "type": "object", ··· 75 "type": "string" 76 } 77 } 78 + }, 79 + "lightCodeTheme": { 80 + "type": "union", 81 + "description": "Syntax highlighting theme for light mode", 82 + "refs": [ 83 + "#codeThemeName", 84 + "#codeThemeFile" 85 + ] 86 + }, 87 + "lightScheme": { 88 + "type": "ref", 89 + "description": "Reference to a light colour scheme", 90 + "ref": "com.atproto.repo.strongRef" 91 }, 92 "spacing": { 93 "type": "object",
+1
crates/weaver-api/src/com_atproto/lexicon.rs
··· 3 // This file was automatically generated from Lexicon schemas. 4 // Any manual changes will be overwritten on the next regeneration. 5 6 pub mod schema;
··· 3 // This file was automatically generated from Lexicon schemas. 4 // Any manual changes will be overwritten on the next regeneration. 5 6 + pub mod resolve_lexicon; 7 pub mod schema;
+196
crates/weaver-api/src/com_atproto/lexicon/resolve_lexicon.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: com.atproto.lexicon.resolveLexicon 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + #[derive( 9 + serde::Serialize, 10 + serde::Deserialize, 11 + Debug, 12 + Clone, 13 + PartialEq, 14 + Eq, 15 + jacquard_derive::IntoStatic 16 + )] 17 + #[serde(rename_all = "camelCase")] 18 + pub struct ResolveLexicon<'a> { 19 + #[serde(borrow)] 20 + pub nsid: jacquard_common::types::string::Nsid<'a>, 21 + } 22 + 23 + pub mod resolve_lexicon_state { 24 + 25 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 26 + #[allow(unused)] 27 + use ::core::marker::PhantomData; 28 + mod sealed { 29 + pub trait Sealed {} 30 + } 31 + /// State trait tracking which required fields have been set 32 + pub trait State: sealed::Sealed { 33 + type Nsid; 34 + } 35 + /// Empty state - all required fields are unset 36 + pub struct Empty(()); 37 + impl sealed::Sealed for Empty {} 38 + impl State for Empty { 39 + type Nsid = Unset; 40 + } 41 + ///State transition - sets the `nsid` field to Set 42 + pub struct SetNsid<S: State = Empty>(PhantomData<fn() -> S>); 43 + impl<S: State> sealed::Sealed for SetNsid<S> {} 44 + impl<S: State> State for SetNsid<S> { 45 + type Nsid = Set<members::nsid>; 46 + } 47 + /// Marker types for field names 48 + #[allow(non_camel_case_types)] 49 + pub mod members { 50 + ///Marker type for the `nsid` field 51 + pub struct nsid(()); 52 + } 53 + } 54 + 55 + /// Builder for constructing an instance of this type 56 + pub struct ResolveLexiconBuilder<'a, S: resolve_lexicon_state::State> { 57 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 58 + __unsafe_private_named: ( 59 + ::core::option::Option<jacquard_common::types::string::Nsid<'a>>, 60 + ), 61 + _phantom: ::core::marker::PhantomData<&'a ()>, 62 + } 63 + 64 + impl<'a> ResolveLexicon<'a> { 65 + /// Create a new builder for this type 66 + pub fn new() -> ResolveLexiconBuilder<'a, resolve_lexicon_state::Empty> { 67 + ResolveLexiconBuilder::new() 68 + } 69 + } 70 + 71 + impl<'a> ResolveLexiconBuilder<'a, resolve_lexicon_state::Empty> { 72 + /// Create a new builder with all fields unset 73 + pub fn new() -> Self { 74 + ResolveLexiconBuilder { 75 + _phantom_state: ::core::marker::PhantomData, 76 + __unsafe_private_named: (None,), 77 + _phantom: ::core::marker::PhantomData, 78 + } 79 + } 80 + } 81 + 82 + impl<'a, S> ResolveLexiconBuilder<'a, S> 83 + where 84 + S: resolve_lexicon_state::State, 85 + S::Nsid: resolve_lexicon_state::IsUnset, 86 + { 87 + /// Set the `nsid` field (required) 88 + pub fn nsid( 89 + mut self, 90 + value: impl Into<jacquard_common::types::string::Nsid<'a>>, 91 + ) -> ResolveLexiconBuilder<'a, resolve_lexicon_state::SetNsid<S>> { 92 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 93 + ResolveLexiconBuilder { 94 + _phantom_state: ::core::marker::PhantomData, 95 + __unsafe_private_named: self.__unsafe_private_named, 96 + _phantom: ::core::marker::PhantomData, 97 + } 98 + } 99 + } 100 + 101 + impl<'a, S> ResolveLexiconBuilder<'a, S> 102 + where 103 + S: resolve_lexicon_state::State, 104 + S::Nsid: resolve_lexicon_state::IsSet, 105 + { 106 + /// Build the final struct 107 + pub fn build(self) -> ResolveLexicon<'a> { 108 + ResolveLexicon { 109 + nsid: self.__unsafe_private_named.0.unwrap(), 110 + } 111 + } 112 + } 113 + 114 + #[jacquard_derive::lexicon] 115 + #[derive( 116 + serde::Serialize, 117 + serde::Deserialize, 118 + Debug, 119 + Clone, 120 + PartialEq, 121 + Eq, 122 + jacquard_derive::IntoStatic 123 + )] 124 + #[serde(rename_all = "camelCase")] 125 + pub struct ResolveLexiconOutput<'a> { 126 + /// The CID of the lexicon schema record. 127 + #[serde(borrow)] 128 + pub cid: jacquard_common::types::string::Cid<'a>, 129 + /// The resolved lexicon schema record. 130 + #[serde(borrow)] 131 + pub schema: crate::com_atproto::lexicon::schema::Schema<'a>, 132 + /// The AT-URI of the lexicon schema record. 133 + #[serde(borrow)] 134 + pub uri: jacquard_common::types::string::AtUri<'a>, 135 + } 136 + 137 + #[jacquard_derive::open_union] 138 + #[derive( 139 + serde::Serialize, 140 + serde::Deserialize, 141 + Debug, 142 + Clone, 143 + PartialEq, 144 + Eq, 145 + thiserror::Error, 146 + miette::Diagnostic, 147 + jacquard_derive::IntoStatic 148 + )] 149 + #[serde(tag = "error", content = "message")] 150 + #[serde(bound(deserialize = "'de: 'a"))] 151 + pub enum ResolveLexiconError<'a> { 152 + /// No lexicon was resolved for the NSID. 153 + #[serde(rename = "LexiconNotFound")] 154 + LexiconNotFound(std::option::Option<String>), 155 + } 156 + 157 + impl std::fmt::Display for ResolveLexiconError<'_> { 158 + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 159 + match self { 160 + Self::LexiconNotFound(msg) => { 161 + write!(f, "LexiconNotFound")?; 162 + if let Some(msg) = msg { 163 + write!(f, ": {}", msg)?; 164 + } 165 + Ok(()) 166 + } 167 + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), 168 + } 169 + } 170 + } 171 + 172 + /// Response type for 173 + ///com.atproto.lexicon.resolveLexicon 174 + pub struct ResolveLexiconResponse; 175 + impl jacquard_common::xrpc::XrpcResp for ResolveLexiconResponse { 176 + const NSID: &'static str = "com.atproto.lexicon.resolveLexicon"; 177 + const ENCODING: &'static str = "application/json"; 178 + type Output<'de> = ResolveLexiconOutput<'de>; 179 + type Err<'de> = ResolveLexiconError<'de>; 180 + } 181 + 182 + impl<'a> jacquard_common::xrpc::XrpcRequest for ResolveLexicon<'a> { 183 + const NSID: &'static str = "com.atproto.lexicon.resolveLexicon"; 184 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; 185 + type Response = ResolveLexiconResponse; 186 + } 187 + 188 + /// Endpoint type for 189 + ///com.atproto.lexicon.resolveLexicon 190 + pub struct ResolveLexiconRequest; 191 + impl jacquard_common::xrpc::XrpcEndpoint for ResolveLexiconRequest { 192 + const PATH: &'static str = "/xrpc/com.atproto.lexicon.resolveLexicon"; 193 + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; 194 + type Request<'de> = ResolveLexicon<'de>; 195 + type Response = ResolveLexiconResponse; 196 + }
+1
crates/weaver-api/src/sh_weaver/notebook.rs
··· 8 pub mod authors; 9 pub mod book; 10 pub mod chapter; 11 pub mod entry; 12 pub mod page; 13 pub mod theme;
··· 8 pub mod authors; 9 pub mod book; 10 pub mod chapter; 11 + pub mod colour_scheme; 12 pub mod entry; 13 pub mod page; 14 pub mod theme;
+784
crates/weaver-api/src/sh_weaver/notebook/colour_scheme.rs
···
··· 1 + // @generated by jacquard-lexicon. DO NOT EDIT. 2 + // 3 + // Lexicon: sh.weaver.notebook.colourScheme 4 + // 5 + // This file was automatically generated from Lexicon schemas. 6 + // Any manual changes will be overwritten on the next regeneration. 7 + 8 + /// A colour palette for notebook theming 9 + #[jacquard_derive::lexicon] 10 + #[derive( 11 + serde::Serialize, 12 + serde::Deserialize, 13 + Debug, 14 + Clone, 15 + PartialEq, 16 + Eq, 17 + jacquard_derive::IntoStatic 18 + )] 19 + #[serde(rename_all = "camelCase")] 20 + pub struct ColourScheme<'a> { 21 + #[serde(borrow)] 22 + pub colours: ColourSchemeColours<'a>, 23 + /// Human-readable name for the colour scheme 24 + #[serde(borrow)] 25 + pub name: jacquard_common::CowStr<'a>, 26 + /// Whether this is a dark or light colour scheme 27 + #[serde(borrow)] 28 + pub variant: jacquard_common::CowStr<'a>, 29 + } 30 + 31 + pub mod colour_scheme_state { 32 + 33 + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; 34 + #[allow(unused)] 35 + use ::core::marker::PhantomData; 36 + mod sealed { 37 + pub trait Sealed {} 38 + } 39 + /// State trait tracking which required fields have been set 40 + pub trait State: sealed::Sealed { 41 + type Name; 42 + type Variant; 43 + type Colours; 44 + } 45 + /// Empty state - all required fields are unset 46 + pub struct Empty(()); 47 + impl sealed::Sealed for Empty {} 48 + impl State for Empty { 49 + type Name = Unset; 50 + type Variant = Unset; 51 + type Colours = Unset; 52 + } 53 + ///State transition - sets the `name` field to Set 54 + pub struct SetName<S: State = Empty>(PhantomData<fn() -> S>); 55 + impl<S: State> sealed::Sealed for SetName<S> {} 56 + impl<S: State> State for SetName<S> { 57 + type Name = Set<members::name>; 58 + type Variant = S::Variant; 59 + type Colours = S::Colours; 60 + } 61 + ///State transition - sets the `variant` field to Set 62 + pub struct SetVariant<S: State = Empty>(PhantomData<fn() -> S>); 63 + impl<S: State> sealed::Sealed for SetVariant<S> {} 64 + impl<S: State> State for SetVariant<S> { 65 + type Name = S::Name; 66 + type Variant = Set<members::variant>; 67 + type Colours = S::Colours; 68 + } 69 + ///State transition - sets the `colours` field to Set 70 + pub struct SetColours<S: State = Empty>(PhantomData<fn() -> S>); 71 + impl<S: State> sealed::Sealed for SetColours<S> {} 72 + impl<S: State> State for SetColours<S> { 73 + type Name = S::Name; 74 + type Variant = S::Variant; 75 + type Colours = Set<members::colours>; 76 + } 77 + /// Marker types for field names 78 + #[allow(non_camel_case_types)] 79 + pub mod members { 80 + ///Marker type for the `name` field 81 + pub struct name(()); 82 + ///Marker type for the `variant` field 83 + pub struct variant(()); 84 + ///Marker type for the `colours` field 85 + pub struct colours(()); 86 + } 87 + } 88 + 89 + /// Builder for constructing an instance of this type 90 + pub struct ColourSchemeBuilder<'a, S: colour_scheme_state::State> { 91 + _phantom_state: ::core::marker::PhantomData<fn() -> S>, 92 + __unsafe_private_named: ( 93 + ::core::option::Option<ColourSchemeColours<'a>>, 94 + ::core::option::Option<jacquard_common::CowStr<'a>>, 95 + ::core::option::Option<jacquard_common::CowStr<'a>>, 96 + ), 97 + _phantom: ::core::marker::PhantomData<&'a ()>, 98 + } 99 + 100 + impl<'a> ColourScheme<'a> { 101 + /// Create a new builder for this type 102 + pub fn new() -> ColourSchemeBuilder<'a, colour_scheme_state::Empty> { 103 + ColourSchemeBuilder::new() 104 + } 105 + } 106 + 107 + impl<'a> ColourSchemeBuilder<'a, colour_scheme_state::Empty> { 108 + /// Create a new builder with all fields unset 109 + pub fn new() -> Self { 110 + ColourSchemeBuilder { 111 + _phantom_state: ::core::marker::PhantomData, 112 + __unsafe_private_named: (None, None, None), 113 + _phantom: ::core::marker::PhantomData, 114 + } 115 + } 116 + } 117 + 118 + impl<'a, S> ColourSchemeBuilder<'a, S> 119 + where 120 + S: colour_scheme_state::State, 121 + S::Colours: colour_scheme_state::IsUnset, 122 + { 123 + /// Set the `colours` field (required) 124 + pub fn colours( 125 + mut self, 126 + value: impl Into<ColourSchemeColours<'a>>, 127 + ) -> ColourSchemeBuilder<'a, colour_scheme_state::SetColours<S>> { 128 + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 129 + ColourSchemeBuilder { 130 + _phantom_state: ::core::marker::PhantomData, 131 + __unsafe_private_named: self.__unsafe_private_named, 132 + _phantom: ::core::marker::PhantomData, 133 + } 134 + } 135 + } 136 + 137 + impl<'a, S> ColourSchemeBuilder<'a, S> 138 + where 139 + S: colour_scheme_state::State, 140 + S::Name: colour_scheme_state::IsUnset, 141 + { 142 + /// Set the `name` field (required) 143 + pub fn name( 144 + mut self, 145 + value: impl Into<jacquard_common::CowStr<'a>>, 146 + ) -> ColourSchemeBuilder<'a, colour_scheme_state::SetName<S>> { 147 + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 148 + ColourSchemeBuilder { 149 + _phantom_state: ::core::marker::PhantomData, 150 + __unsafe_private_named: self.__unsafe_private_named, 151 + _phantom: ::core::marker::PhantomData, 152 + } 153 + } 154 + } 155 + 156 + impl<'a, S> ColourSchemeBuilder<'a, S> 157 + where 158 + S: colour_scheme_state::State, 159 + S::Variant: colour_scheme_state::IsUnset, 160 + { 161 + /// Set the `variant` field (required) 162 + pub fn variant( 163 + mut self, 164 + value: impl Into<jacquard_common::CowStr<'a>>, 165 + ) -> ColourSchemeBuilder<'a, colour_scheme_state::SetVariant<S>> { 166 + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); 167 + ColourSchemeBuilder { 168 + _phantom_state: ::core::marker::PhantomData, 169 + __unsafe_private_named: self.__unsafe_private_named, 170 + _phantom: ::core::marker::PhantomData, 171 + } 172 + } 173 + } 174 + 175 + impl<'a, S> ColourSchemeBuilder<'a, S> 176 + where 177 + S: colour_scheme_state::State, 178 + S::Name: colour_scheme_state::IsSet, 179 + S::Variant: colour_scheme_state::IsSet, 180 + S::Colours: colour_scheme_state::IsSet, 181 + { 182 + /// Build the final struct 183 + pub fn build(self) -> ColourScheme<'a> { 184 + ColourScheme { 185 + colours: self.__unsafe_private_named.0.unwrap(), 186 + name: self.__unsafe_private_named.1.unwrap(), 187 + variant: self.__unsafe_private_named.2.unwrap(), 188 + extra_data: Default::default(), 189 + } 190 + } 191 + /// Build the final struct with custom extra_data 192 + pub fn build_with_data( 193 + self, 194 + extra_data: std::collections::BTreeMap< 195 + jacquard_common::smol_str::SmolStr, 196 + jacquard_common::types::value::Data<'a>, 197 + >, 198 + ) -> ColourScheme<'a> { 199 + ColourScheme { 200 + colours: self.__unsafe_private_named.0.unwrap(), 201 + name: self.__unsafe_private_named.1.unwrap(), 202 + variant: self.__unsafe_private_named.2.unwrap(), 203 + extra_data: Some(extra_data), 204 + } 205 + } 206 + } 207 + 208 + impl<'a> ColourScheme<'a> { 209 + pub fn uri( 210 + uri: impl Into<jacquard_common::CowStr<'a>>, 211 + ) -> Result< 212 + jacquard_common::types::uri::RecordUri<'a, ColourSchemeRecord>, 213 + jacquard_common::types::uri::UriError, 214 + > { 215 + jacquard_common::types::uri::RecordUri::try_from_uri( 216 + jacquard_common::types::string::AtUri::new_cow(uri.into())?, 217 + ) 218 + } 219 + } 220 + 221 + #[jacquard_derive::lexicon] 222 + #[derive( 223 + serde::Serialize, 224 + serde::Deserialize, 225 + Debug, 226 + Clone, 227 + PartialEq, 228 + Eq, 229 + jacquard_derive::IntoStatic, 230 + Default 231 + )] 232 + #[serde(rename_all = "camelCase")] 233 + pub struct ColourSchemeColours<'a> { 234 + /// Primary background for page/frame 235 + #[serde(borrow)] 236 + pub base: jacquard_common::CowStr<'a>, 237 + /// Border/divider colour 238 + #[serde(borrow)] 239 + pub border: jacquard_common::CowStr<'a>, 240 + /// Emphasized text (bold, important) 241 + #[serde(borrow)] 242 + pub emphasis: jacquard_common::CowStr<'a>, 243 + /// Error state colour 244 + #[serde(borrow)] 245 + pub error: jacquard_common::CowStr<'a>, 246 + /// Selection/highlight colour 247 + #[serde(borrow)] 248 + pub highlight: jacquard_common::CowStr<'a>, 249 + /// Hyperlink colour 250 + #[serde(borrow)] 251 + pub link: jacquard_common::CowStr<'a>, 252 + /// De-emphasized text (disabled, metadata) 253 + #[serde(borrow)] 254 + pub muted: jacquard_common::CowStr<'a>, 255 + /// Tertiary background for popovers/dialogs 256 + #[serde(borrow)] 257 + pub overlay: jacquard_common::CowStr<'a>, 258 + /// Primary brand/accent colour 259 + #[serde(borrow)] 260 + pub primary: jacquard_common::CowStr<'a>, 261 + /// Secondary accent colour 262 + #[serde(borrow)] 263 + pub secondary: jacquard_common::CowStr<'a>, 264 + /// Medium emphasis text (comments, labels) 265 + #[serde(borrow)] 266 + pub subtle: jacquard_common::CowStr<'a>, 267 + /// Success state colour 268 + #[serde(borrow)] 269 + pub success: jacquard_common::CowStr<'a>, 270 + /// Secondary background for panels/cards 271 + #[serde(borrow)] 272 + pub surface: jacquard_common::CowStr<'a>, 273 + /// Tertiary accent colour 274 + #[serde(borrow)] 275 + pub tertiary: jacquard_common::CowStr<'a>, 276 + /// Primary readable text colour 277 + #[serde(borrow)] 278 + pub text: jacquard_common::CowStr<'a>, 279 + /// Warning state colour 280 + #[serde(borrow)] 281 + pub warning: jacquard_common::CowStr<'a>, 282 + } 283 + 284 + fn lexicon_doc_sh_weaver_notebook_colourScheme() -> ::jacquard_lexicon::lexicon::LexiconDoc< 285 + 'static, 286 + > { 287 + ::jacquard_lexicon::lexicon::LexiconDoc { 288 + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, 289 + id: ::jacquard_common::CowStr::new_static("sh.weaver.notebook.colourScheme"), 290 + revision: None, 291 + description: None, 292 + defs: { 293 + let mut map = ::std::collections::BTreeMap::new(); 294 + map.insert( 295 + ::jacquard_common::smol_str::SmolStr::new_static("main"), 296 + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { 297 + description: Some( 298 + ::jacquard_common::CowStr::new_static( 299 + "A colour palette for notebook theming", 300 + ), 301 + ), 302 + key: Some(::jacquard_common::CowStr::new_static("tid")), 303 + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { 304 + description: None, 305 + required: Some( 306 + vec![ 307 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 308 + ::jacquard_common::smol_str::SmolStr::new_static("variant"), 309 + ::jacquard_common::smol_str::SmolStr::new_static("colours") 310 + ], 311 + ), 312 + nullable: None, 313 + properties: { 314 + #[allow(unused_mut)] 315 + let mut map = ::std::collections::BTreeMap::new(); 316 + map.insert( 317 + ::jacquard_common::smol_str::SmolStr::new_static("colours"), 318 + ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { 319 + description: None, 320 + required: Some( 321 + vec![ 322 + ::jacquard_common::smol_str::SmolStr::new_static("base"), 323 + ::jacquard_common::smol_str::SmolStr::new_static("surface"), 324 + ::jacquard_common::smol_str::SmolStr::new_static("overlay"), 325 + ::jacquard_common::smol_str::SmolStr::new_static("text"), 326 + ::jacquard_common::smol_str::SmolStr::new_static("muted"), 327 + ::jacquard_common::smol_str::SmolStr::new_static("subtle"), 328 + ::jacquard_common::smol_str::SmolStr::new_static("emphasis"), 329 + ::jacquard_common::smol_str::SmolStr::new_static("primary"), 330 + ::jacquard_common::smol_str::SmolStr::new_static("secondary"), 331 + ::jacquard_common::smol_str::SmolStr::new_static("tertiary"), 332 + ::jacquard_common::smol_str::SmolStr::new_static("error"), 333 + ::jacquard_common::smol_str::SmolStr::new_static("warning"), 334 + ::jacquard_common::smol_str::SmolStr::new_static("success"), 335 + ::jacquard_common::smol_str::SmolStr::new_static("border"), 336 + ::jacquard_common::smol_str::SmolStr::new_static("link"), 337 + ::jacquard_common::smol_str::SmolStr::new_static("highlight") 338 + ], 339 + ), 340 + nullable: None, 341 + properties: { 342 + #[allow(unused_mut)] 343 + let mut map = ::std::collections::BTreeMap::new(); 344 + map.insert( 345 + ::jacquard_common::smol_str::SmolStr::new_static("base"), 346 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 347 + description: Some( 348 + ::jacquard_common::CowStr::new_static( 349 + "Primary background for page/frame", 350 + ), 351 + ), 352 + format: None, 353 + default: None, 354 + min_length: None, 355 + max_length: None, 356 + min_graphemes: None, 357 + max_graphemes: None, 358 + r#enum: None, 359 + r#const: None, 360 + known_values: None, 361 + }), 362 + ); 363 + map.insert( 364 + ::jacquard_common::smol_str::SmolStr::new_static("border"), 365 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 366 + description: Some( 367 + ::jacquard_common::CowStr::new_static( 368 + "Border/divider colour", 369 + ), 370 + ), 371 + format: None, 372 + default: None, 373 + min_length: None, 374 + max_length: None, 375 + min_graphemes: None, 376 + max_graphemes: None, 377 + r#enum: None, 378 + r#const: None, 379 + known_values: None, 380 + }), 381 + ); 382 + map.insert( 383 + ::jacquard_common::smol_str::SmolStr::new_static( 384 + "emphasis", 385 + ), 386 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 387 + description: Some( 388 + ::jacquard_common::CowStr::new_static( 389 + "Emphasized text (bold, important)", 390 + ), 391 + ), 392 + format: None, 393 + default: None, 394 + min_length: None, 395 + max_length: None, 396 + min_graphemes: None, 397 + max_graphemes: None, 398 + r#enum: None, 399 + r#const: None, 400 + known_values: None, 401 + }), 402 + ); 403 + map.insert( 404 + ::jacquard_common::smol_str::SmolStr::new_static("error"), 405 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 406 + description: Some( 407 + ::jacquard_common::CowStr::new_static("Error state colour"), 408 + ), 409 + format: None, 410 + default: None, 411 + min_length: None, 412 + max_length: None, 413 + min_graphemes: None, 414 + max_graphemes: None, 415 + r#enum: None, 416 + r#const: None, 417 + known_values: None, 418 + }), 419 + ); 420 + map.insert( 421 + ::jacquard_common::smol_str::SmolStr::new_static( 422 + "highlight", 423 + ), 424 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 425 + description: Some( 426 + ::jacquard_common::CowStr::new_static( 427 + "Selection/highlight colour", 428 + ), 429 + ), 430 + format: None, 431 + default: None, 432 + min_length: None, 433 + max_length: None, 434 + min_graphemes: None, 435 + max_graphemes: None, 436 + r#enum: None, 437 + r#const: None, 438 + known_values: None, 439 + }), 440 + ); 441 + map.insert( 442 + ::jacquard_common::smol_str::SmolStr::new_static("link"), 443 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 444 + description: Some( 445 + ::jacquard_common::CowStr::new_static("Hyperlink colour"), 446 + ), 447 + format: None, 448 + default: None, 449 + min_length: None, 450 + max_length: None, 451 + min_graphemes: None, 452 + max_graphemes: None, 453 + r#enum: None, 454 + r#const: None, 455 + known_values: None, 456 + }), 457 + ); 458 + map.insert( 459 + ::jacquard_common::smol_str::SmolStr::new_static("muted"), 460 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 461 + description: Some( 462 + ::jacquard_common::CowStr::new_static( 463 + "De-emphasized text (disabled, metadata)", 464 + ), 465 + ), 466 + format: None, 467 + default: None, 468 + min_length: None, 469 + max_length: None, 470 + min_graphemes: None, 471 + max_graphemes: None, 472 + r#enum: None, 473 + r#const: None, 474 + known_values: None, 475 + }), 476 + ); 477 + map.insert( 478 + ::jacquard_common::smol_str::SmolStr::new_static("overlay"), 479 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 480 + description: Some( 481 + ::jacquard_common::CowStr::new_static( 482 + "Tertiary background for popovers/dialogs", 483 + ), 484 + ), 485 + format: None, 486 + default: None, 487 + min_length: None, 488 + max_length: None, 489 + min_graphemes: None, 490 + max_graphemes: None, 491 + r#enum: None, 492 + r#const: None, 493 + known_values: None, 494 + }), 495 + ); 496 + map.insert( 497 + ::jacquard_common::smol_str::SmolStr::new_static("primary"), 498 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 499 + description: Some( 500 + ::jacquard_common::CowStr::new_static( 501 + "Primary brand/accent colour", 502 + ), 503 + ), 504 + format: None, 505 + default: None, 506 + min_length: None, 507 + max_length: None, 508 + min_graphemes: None, 509 + max_graphemes: None, 510 + r#enum: None, 511 + r#const: None, 512 + known_values: None, 513 + }), 514 + ); 515 + map.insert( 516 + ::jacquard_common::smol_str::SmolStr::new_static( 517 + "secondary", 518 + ), 519 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 520 + description: Some( 521 + ::jacquard_common::CowStr::new_static( 522 + "Secondary accent colour", 523 + ), 524 + ), 525 + format: None, 526 + default: None, 527 + min_length: None, 528 + max_length: None, 529 + min_graphemes: None, 530 + max_graphemes: None, 531 + r#enum: None, 532 + r#const: None, 533 + known_values: None, 534 + }), 535 + ); 536 + map.insert( 537 + ::jacquard_common::smol_str::SmolStr::new_static("subtle"), 538 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 539 + description: Some( 540 + ::jacquard_common::CowStr::new_static( 541 + "Medium emphasis text (comments, labels)", 542 + ), 543 + ), 544 + format: None, 545 + default: None, 546 + min_length: None, 547 + max_length: None, 548 + min_graphemes: None, 549 + max_graphemes: None, 550 + r#enum: None, 551 + r#const: None, 552 + known_values: None, 553 + }), 554 + ); 555 + map.insert( 556 + ::jacquard_common::smol_str::SmolStr::new_static("success"), 557 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 558 + description: Some( 559 + ::jacquard_common::CowStr::new_static( 560 + "Success state colour", 561 + ), 562 + ), 563 + format: None, 564 + default: None, 565 + min_length: None, 566 + max_length: None, 567 + min_graphemes: None, 568 + max_graphemes: None, 569 + r#enum: None, 570 + r#const: None, 571 + known_values: None, 572 + }), 573 + ); 574 + map.insert( 575 + ::jacquard_common::smol_str::SmolStr::new_static("surface"), 576 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 577 + description: Some( 578 + ::jacquard_common::CowStr::new_static( 579 + "Secondary background for panels/cards", 580 + ), 581 + ), 582 + format: None, 583 + default: None, 584 + min_length: None, 585 + max_length: None, 586 + min_graphemes: None, 587 + max_graphemes: None, 588 + r#enum: None, 589 + r#const: None, 590 + known_values: None, 591 + }), 592 + ); 593 + map.insert( 594 + ::jacquard_common::smol_str::SmolStr::new_static( 595 + "tertiary", 596 + ), 597 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 598 + description: Some( 599 + ::jacquard_common::CowStr::new_static( 600 + "Tertiary accent colour", 601 + ), 602 + ), 603 + format: None, 604 + default: None, 605 + min_length: None, 606 + max_length: None, 607 + min_graphemes: None, 608 + max_graphemes: None, 609 + r#enum: None, 610 + r#const: None, 611 + known_values: None, 612 + }), 613 + ); 614 + map.insert( 615 + ::jacquard_common::smol_str::SmolStr::new_static("text"), 616 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 617 + description: Some( 618 + ::jacquard_common::CowStr::new_static( 619 + "Primary readable text colour", 620 + ), 621 + ), 622 + format: None, 623 + default: None, 624 + min_length: None, 625 + max_length: None, 626 + min_graphemes: None, 627 + max_graphemes: None, 628 + r#enum: None, 629 + r#const: None, 630 + known_values: None, 631 + }), 632 + ); 633 + map.insert( 634 + ::jacquard_common::smol_str::SmolStr::new_static("warning"), 635 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 636 + description: Some( 637 + ::jacquard_common::CowStr::new_static( 638 + "Warning state colour", 639 + ), 640 + ), 641 + format: None, 642 + default: None, 643 + min_length: None, 644 + max_length: None, 645 + min_graphemes: None, 646 + max_graphemes: None, 647 + r#enum: None, 648 + r#const: None, 649 + known_values: None, 650 + }), 651 + ); 652 + map 653 + }, 654 + }), 655 + ); 656 + map.insert( 657 + ::jacquard_common::smol_str::SmolStr::new_static("name"), 658 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 659 + description: Some( 660 + ::jacquard_common::CowStr::new_static( 661 + "Human-readable name for the colour scheme", 662 + ), 663 + ), 664 + format: None, 665 + default: None, 666 + min_length: None, 667 + max_length: None, 668 + min_graphemes: None, 669 + max_graphemes: None, 670 + r#enum: None, 671 + r#const: None, 672 + known_values: None, 673 + }), 674 + ); 675 + map.insert( 676 + ::jacquard_common::smol_str::SmolStr::new_static("variant"), 677 + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 678 + description: Some( 679 + ::jacquard_common::CowStr::new_static( 680 + "Whether this is a dark or light colour scheme", 681 + ), 682 + ), 683 + format: None, 684 + default: None, 685 + min_length: None, 686 + max_length: None, 687 + min_graphemes: None, 688 + max_graphemes: None, 689 + r#enum: None, 690 + r#const: None, 691 + known_values: None, 692 + }), 693 + ); 694 + map 695 + }, 696 + }), 697 + }), 698 + ); 699 + map 700 + }, 701 + } 702 + } 703 + 704 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ColourSchemeColours<'a> { 705 + fn nsid() -> &'static str { 706 + "sh.weaver.notebook.colourScheme" 707 + } 708 + fn def_name() -> &'static str { 709 + "ColourSchemeColours" 710 + } 711 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 712 + lexicon_doc_sh_weaver_notebook_colourScheme() 713 + } 714 + fn validate( 715 + &self, 716 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 717 + Ok(()) 718 + } 719 + } 720 + 721 + /// Typed wrapper for GetRecord response with this collection's record type. 722 + #[derive( 723 + serde::Serialize, 724 + serde::Deserialize, 725 + Debug, 726 + Clone, 727 + PartialEq, 728 + Eq, 729 + jacquard_derive::IntoStatic 730 + )] 731 + #[serde(rename_all = "camelCase")] 732 + pub struct ColourSchemeGetRecordOutput<'a> { 733 + #[serde(skip_serializing_if = "std::option::Option::is_none")] 734 + #[serde(borrow)] 735 + pub cid: std::option::Option<jacquard_common::types::string::Cid<'a>>, 736 + #[serde(borrow)] 737 + pub uri: jacquard_common::types::string::AtUri<'a>, 738 + #[serde(borrow)] 739 + pub value: ColourScheme<'a>, 740 + } 741 + 742 + impl From<ColourSchemeGetRecordOutput<'_>> for ColourScheme<'_> { 743 + fn from(output: ColourSchemeGetRecordOutput<'_>) -> Self { 744 + use jacquard_common::IntoStatic; 745 + output.value.into_static() 746 + } 747 + } 748 + 749 + impl jacquard_common::types::collection::Collection for ColourScheme<'_> { 750 + const NSID: &'static str = "sh.weaver.notebook.colourScheme"; 751 + type Record = ColourSchemeRecord; 752 + } 753 + 754 + /// Marker type for deserializing records from this collection. 755 + #[derive(Debug, serde::Serialize, serde::Deserialize)] 756 + pub struct ColourSchemeRecord; 757 + impl jacquard_common::xrpc::XrpcResp for ColourSchemeRecord { 758 + const NSID: &'static str = "sh.weaver.notebook.colourScheme"; 759 + const ENCODING: &'static str = "application/json"; 760 + type Output<'de> = ColourSchemeGetRecordOutput<'de>; 761 + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; 762 + } 763 + 764 + impl jacquard_common::types::collection::Collection for ColourSchemeRecord { 765 + const NSID: &'static str = "sh.weaver.notebook.colourScheme"; 766 + type Record = ColourSchemeRecord; 767 + } 768 + 769 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ColourScheme<'a> { 770 + fn nsid() -> &'static str { 771 + "sh.weaver.notebook.colourScheme" 772 + } 773 + fn def_name() -> &'static str { 774 + "main" 775 + } 776 + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 777 + lexicon_doc_sh_weaver_notebook_colourScheme() 778 + } 779 + fn validate( 780 + &self, 781 + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 782 + Ok(()) 783 + } 784 + }
+200 -208
crates/weaver-api/src/sh_weaver/notebook/theme.rs
··· 297 description: None, 298 required: Some( 299 vec![ 300 - ::jacquard_common::smol_str::SmolStr::new_static("colours"), 301 ::jacquard_common::smol_str::SmolStr::new_static("fonts"), 302 ::jacquard_common::smol_str::SmolStr::new_static("spacing"), 303 - ::jacquard_common::smol_str::SmolStr::new_static("codeTheme") 304 ], 305 ), 306 nullable: None, ··· 309 let mut map = ::std::collections::BTreeMap::new(); 310 map.insert( 311 ::jacquard_common::smol_str::SmolStr::new_static( 312 - "codeTheme", 313 ), 314 ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 315 - description: None, 316 refs: vec![ 317 ::jacquard_common::CowStr::new_static("#codeThemeName"), 318 ::jacquard_common::CowStr::new_static("#codeThemeFile") ··· 321 }), 322 ); 323 map.insert( 324 - ::jacquard_common::smol_str::SmolStr::new_static("colours"), 325 - ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { 326 description: None, 327 - required: Some( 328 - vec![ 329 - ::jacquard_common::smol_str::SmolStr::new_static("background"), 330 - ::jacquard_common::smol_str::SmolStr::new_static("foreground"), 331 - ::jacquard_common::smol_str::SmolStr::new_static("primary"), 332 - ::jacquard_common::smol_str::SmolStr::new_static("secondary"), 333 - ::jacquard_common::smol_str::SmolStr::new_static("link"), 334 - ::jacquard_common::smol_str::SmolStr::new_static("link_hover") 335 - ], 336 ), 337 - nullable: None, 338 - properties: { 339 - #[allow(unused_mut)] 340 - let mut map = ::std::collections::BTreeMap::new(); 341 - map.insert( 342 - ::jacquard_common::smol_str::SmolStr::new_static( 343 - "background", 344 - ), 345 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 346 - description: None, 347 - format: None, 348 - default: None, 349 - min_length: None, 350 - max_length: None, 351 - min_graphemes: None, 352 - max_graphemes: None, 353 - r#enum: None, 354 - r#const: None, 355 - known_values: None, 356 - }), 357 - ); 358 - map.insert( 359 - ::jacquard_common::smol_str::SmolStr::new_static( 360 - "foreground", 361 - ), 362 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 363 - description: None, 364 - format: None, 365 - default: None, 366 - min_length: None, 367 - max_length: None, 368 - min_graphemes: None, 369 - max_graphemes: None, 370 - r#enum: None, 371 - r#const: None, 372 - known_values: None, 373 - }), 374 - ); 375 - map.insert( 376 - ::jacquard_common::smol_str::SmolStr::new_static("link"), 377 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 378 - description: None, 379 - format: None, 380 - default: None, 381 - min_length: None, 382 - max_length: None, 383 - min_graphemes: None, 384 - max_graphemes: None, 385 - r#enum: None, 386 - r#const: None, 387 - known_values: None, 388 - }), 389 - ); 390 - map.insert( 391 - ::jacquard_common::smol_str::SmolStr::new_static( 392 - "link_hover", 393 - ), 394 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 395 - description: None, 396 - format: None, 397 - default: None, 398 - min_length: None, 399 - max_length: None, 400 - min_graphemes: None, 401 - max_graphemes: None, 402 - r#enum: None, 403 - r#const: None, 404 - known_values: None, 405 - }), 406 - ); 407 - map.insert( 408 - ::jacquard_common::smol_str::SmolStr::new_static("primary"), 409 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 410 - description: None, 411 - format: None, 412 - default: None, 413 - min_length: None, 414 - max_length: None, 415 - min_graphemes: None, 416 - max_graphemes: None, 417 - r#enum: None, 418 - r#const: None, 419 - known_values: None, 420 - }), 421 - ); 422 - map.insert( 423 - ::jacquard_common::smol_str::SmolStr::new_static( 424 - "secondary", 425 - ), 426 - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { 427 - description: None, 428 - format: None, 429 - default: None, 430 - min_length: None, 431 - max_length: None, 432 - min_graphemes: None, 433 - max_graphemes: None, 434 - r#enum: None, 435 - r#const: None, 436 - known_values: None, 437 - }), 438 - ); 439 - map 440 - }, 441 }), 442 ); 443 map.insert( ··· 507 }), 508 ); 509 map.insert( 510 ::jacquard_common::smol_str::SmolStr::new_static("spacing"), 511 ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { 512 description: None, ··· 615 )] 616 #[serde(rename_all = "camelCase")] 617 pub struct Theme<'a> { 618 #[serde(borrow)] 619 - pub code_theme: ThemeCodeTheme<'a>, 620 #[serde(borrow)] 621 - pub colours: ThemeColours<'a>, 622 #[serde(borrow)] 623 pub fonts: ThemeFonts<'a>, 624 #[serde(borrow)] 625 pub spacing: ThemeSpacing<'a>, 626 } ··· 635 } 636 /// State trait tracking which required fields have been set 637 pub trait State: sealed::Sealed { 638 - type Colours; 639 type Fonts; 640 type Spacing; 641 - type CodeTheme; 642 } 643 /// Empty state - all required fields are unset 644 pub struct Empty(()); 645 impl sealed::Sealed for Empty {} 646 impl State for Empty { 647 - type Colours = Unset; 648 type Fonts = Unset; 649 type Spacing = Unset; 650 - type CodeTheme = Unset; 651 } 652 - ///State transition - sets the `colours` field to Set 653 - pub struct SetColours<S: State = Empty>(PhantomData<fn() -> S>); 654 - impl<S: State> sealed::Sealed for SetColours<S> {} 655 - impl<S: State> State for SetColours<S> { 656 - type Colours = Set<members::colours>; 657 type Fonts = S::Fonts; 658 type Spacing = S::Spacing; 659 - type CodeTheme = S::CodeTheme; 660 } 661 ///State transition - sets the `fonts` field to Set 662 pub struct SetFonts<S: State = Empty>(PhantomData<fn() -> S>); 663 impl<S: State> sealed::Sealed for SetFonts<S> {} 664 impl<S: State> State for SetFonts<S> { 665 - type Colours = S::Colours; 666 type Fonts = Set<members::fonts>; 667 type Spacing = S::Spacing; 668 - type CodeTheme = S::CodeTheme; 669 } 670 ///State transition - sets the `spacing` field to Set 671 pub struct SetSpacing<S: State = Empty>(PhantomData<fn() -> S>); 672 impl<S: State> sealed::Sealed for SetSpacing<S> {} 673 impl<S: State> State for SetSpacing<S> { 674 - type Colours = S::Colours; 675 type Fonts = S::Fonts; 676 type Spacing = Set<members::spacing>; 677 - type CodeTheme = S::CodeTheme; 678 } 679 - ///State transition - sets the `code_theme` field to Set 680 - pub struct SetCodeTheme<S: State = Empty>(PhantomData<fn() -> S>); 681 - impl<S: State> sealed::Sealed for SetCodeTheme<S> {} 682 - impl<S: State> State for SetCodeTheme<S> { 683 - type Colours = S::Colours; 684 type Fonts = S::Fonts; 685 type Spacing = S::Spacing; 686 - type CodeTheme = Set<members::code_theme>; 687 } 688 /// Marker types for field names 689 #[allow(non_camel_case_types)] 690 pub mod members { 691 - ///Marker type for the `colours` field 692 - pub struct colours(()); 693 ///Marker type for the `fonts` field 694 pub struct fonts(()); 695 ///Marker type for the `spacing` field 696 pub struct spacing(()); 697 - ///Marker type for the `code_theme` field 698 - pub struct code_theme(()); 699 } 700 } 701 ··· 703 pub struct ThemeBuilder<'a, S: theme_state::State> { 704 _phantom_state: ::core::marker::PhantomData<fn() -> S>, 705 __unsafe_private_named: ( 706 - ::core::option::Option<ThemeCodeTheme<'a>>, 707 - ::core::option::Option<ThemeColours<'a>>, 708 ::core::option::Option<ThemeFonts<'a>>, 709 ::core::option::Option<ThemeSpacing<'a>>, 710 ), 711 _phantom: ::core::marker::PhantomData<&'a ()>, ··· 723 pub fn new() -> Self { 724 ThemeBuilder { 725 _phantom_state: ::core::marker::PhantomData, 726 - __unsafe_private_named: (None, None, None, None), 727 _phantom: ::core::marker::PhantomData, 728 } 729 } ··· 732 impl<'a, S> ThemeBuilder<'a, S> 733 where 734 S: theme_state::State, 735 - S::CodeTheme: theme_state::IsUnset, 736 { 737 - /// Set the `codeTheme` field (required) 738 - pub fn code_theme( 739 mut self, 740 - value: impl Into<ThemeCodeTheme<'a>>, 741 - ) -> ThemeBuilder<'a, theme_state::SetCodeTheme<S>> { 742 self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 743 ThemeBuilder { 744 _phantom_state: ::core::marker::PhantomData, ··· 751 impl<'a, S> ThemeBuilder<'a, S> 752 where 753 S: theme_state::State, 754 - S::Colours: theme_state::IsUnset, 755 { 756 - /// Set the `colours` field (required) 757 - pub fn colours( 758 mut self, 759 - value: impl Into<ThemeColours<'a>>, 760 - ) -> ThemeBuilder<'a, theme_state::SetColours<S>> { 761 self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 762 ThemeBuilder { 763 _phantom_state: ::core::marker::PhantomData, ··· 789 impl<'a, S> ThemeBuilder<'a, S> 790 where 791 S: theme_state::State, 792 S::Spacing: theme_state::IsUnset, 793 { 794 /// Set the `spacing` field (required) ··· 796 mut self, 797 value: impl Into<ThemeSpacing<'a>>, 798 ) -> ThemeBuilder<'a, theme_state::SetSpacing<S>> { 799 - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); 800 ThemeBuilder { 801 _phantom_state: ::core::marker::PhantomData, 802 __unsafe_private_named: self.__unsafe_private_named, ··· 808 impl<'a, S> ThemeBuilder<'a, S> 809 where 810 S: theme_state::State, 811 - S::Colours: theme_state::IsSet, 812 S::Fonts: theme_state::IsSet, 813 S::Spacing: theme_state::IsSet, 814 - S::CodeTheme: theme_state::IsSet, 815 { 816 /// Build the final struct 817 pub fn build(self) -> Theme<'a> { 818 Theme { 819 - code_theme: self.__unsafe_private_named.0.unwrap(), 820 - colours: self.__unsafe_private_named.1.unwrap(), 821 fonts: self.__unsafe_private_named.2.unwrap(), 822 - spacing: self.__unsafe_private_named.3.unwrap(), 823 extra_data: Default::default(), 824 } 825 } ··· 832 >, 833 ) -> Theme<'a> { 834 Theme { 835 - code_theme: self.__unsafe_private_named.0.unwrap(), 836 - colours: self.__unsafe_private_named.1.unwrap(), 837 fonts: self.__unsafe_private_named.2.unwrap(), 838 - spacing: self.__unsafe_private_named.3.unwrap(), 839 extra_data: Some(extra_data), 840 } 841 } ··· 866 )] 867 #[serde(tag = "$type")] 868 #[serde(bound(deserialize = "'de: 'a"))] 869 - pub enum ThemeCodeTheme<'a> { 870 #[serde(rename = "sh.weaver.notebook.theme#codeThemeName")] 871 CodeThemeName(Box<crate::sh_weaver::notebook::theme::CodeThemeName<'a>>), 872 #[serde(rename = "sh.weaver.notebook.theme#codeThemeFile")] ··· 885 Default 886 )] 887 #[serde(rename_all = "camelCase")] 888 - pub struct ThemeColours<'a> { 889 - #[serde(borrow)] 890 - pub background: jacquard_common::CowStr<'a>, 891 - #[serde(borrow)] 892 - pub foreground: jacquard_common::CowStr<'a>, 893 #[serde(borrow)] 894 - pub link: jacquard_common::CowStr<'a>, 895 #[serde(borrow)] 896 - pub link_hover: jacquard_common::CowStr<'a>, 897 #[serde(borrow)] 898 - pub primary: jacquard_common::CowStr<'a>, 899 - #[serde(borrow)] 900 - pub secondary: jacquard_common::CowStr<'a>, 901 } 902 903 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeColours<'a> { 904 fn nsid() -> &'static str { 905 "sh.weaver.notebook.theme" 906 } 907 fn def_name() -> &'static str { 908 - "ThemeColours" 909 } 910 fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 911 lexicon_doc_sh_weaver_notebook_theme() ··· 917 } 918 } 919 920 - #[jacquard_derive::lexicon] 921 #[derive( 922 serde::Serialize, 923 serde::Deserialize, ··· 925 Clone, 926 PartialEq, 927 Eq, 928 - jacquard_derive::IntoStatic, 929 - Default 930 )] 931 - #[serde(rename_all = "camelCase")] 932 - pub struct ThemeFonts<'a> { 933 - #[serde(borrow)] 934 - pub body: jacquard_common::CowStr<'a>, 935 - #[serde(borrow)] 936 - pub heading: jacquard_common::CowStr<'a>, 937 - #[serde(borrow)] 938 - pub monospace: jacquard_common::CowStr<'a>, 939 - } 940 - 941 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeFonts<'a> { 942 - fn nsid() -> &'static str { 943 - "sh.weaver.notebook.theme" 944 - } 945 - fn def_name() -> &'static str { 946 - "ThemeFonts" 947 - } 948 - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 949 - lexicon_doc_sh_weaver_notebook_theme() 950 - } 951 - fn validate( 952 - &self, 953 - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { 954 - Ok(()) 955 - } 956 } 957 958 #[jacquard_derive::lexicon]
··· 297 description: None, 298 required: Some( 299 vec![ 300 + ::jacquard_common::smol_str::SmolStr::new_static("darkScheme"), 301 + ::jacquard_common::smol_str::SmolStr::new_static("lightScheme"), 302 ::jacquard_common::smol_str::SmolStr::new_static("fonts"), 303 ::jacquard_common::smol_str::SmolStr::new_static("spacing"), 304 + ::jacquard_common::smol_str::SmolStr::new_static("darkCodeTheme"), 305 + ::jacquard_common::smol_str::SmolStr::new_static("lightCodeTheme") 306 ], 307 ), 308 nullable: None, ··· 311 let mut map = ::std::collections::BTreeMap::new(); 312 map.insert( 313 ::jacquard_common::smol_str::SmolStr::new_static( 314 + "darkCodeTheme", 315 ), 316 ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 317 + description: Some( 318 + ::jacquard_common::CowStr::new_static( 319 + "Syntax highlighting theme for dark mode", 320 + ), 321 + ), 322 refs: vec![ 323 ::jacquard_common::CowStr::new_static("#codeThemeName"), 324 ::jacquard_common::CowStr::new_static("#codeThemeFile") ··· 327 }), 328 ); 329 map.insert( 330 + ::jacquard_common::smol_str::SmolStr::new_static( 331 + "darkScheme", 332 + ), 333 + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 334 description: None, 335 + r#ref: ::jacquard_common::CowStr::new_static( 336 + "com.atproto.repo.strongRef", 337 ), 338 }), 339 ); 340 map.insert( ··· 404 }), 405 ); 406 map.insert( 407 + ::jacquard_common::smol_str::SmolStr::new_static( 408 + "lightCodeTheme", 409 + ), 410 + ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 411 + description: Some( 412 + ::jacquard_common::CowStr::new_static( 413 + "Syntax highlighting theme for light mode", 414 + ), 415 + ), 416 + refs: vec![ 417 + ::jacquard_common::CowStr::new_static("#codeThemeName"), 418 + ::jacquard_common::CowStr::new_static("#codeThemeFile") 419 + ], 420 + closed: None, 421 + }), 422 + ); 423 + map.insert( 424 + ::jacquard_common::smol_str::SmolStr::new_static( 425 + "lightScheme", 426 + ), 427 + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 428 + description: None, 429 + r#ref: ::jacquard_common::CowStr::new_static( 430 + "com.atproto.repo.strongRef", 431 + ), 432 + }), 433 + ); 434 + map.insert( 435 ::jacquard_common::smol_str::SmolStr::new_static("spacing"), 436 ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { 437 description: None, ··· 540 )] 541 #[serde(rename_all = "camelCase")] 542 pub struct Theme<'a> { 543 + /// Syntax highlighting theme for dark mode 544 #[serde(borrow)] 545 + pub dark_code_theme: ThemeDarkCodeTheme<'a>, 546 + /// Reference to a dark colour scheme 547 #[serde(borrow)] 548 + pub dark_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 549 #[serde(borrow)] 550 pub fonts: ThemeFonts<'a>, 551 + /// Syntax highlighting theme for light mode 552 + #[serde(borrow)] 553 + pub light_code_theme: ThemeLightCodeTheme<'a>, 554 + /// Reference to a light colour scheme 555 + #[serde(borrow)] 556 + pub light_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 557 #[serde(borrow)] 558 pub spacing: ThemeSpacing<'a>, 559 } ··· 568 } 569 /// State trait tracking which required fields have been set 570 pub trait State: sealed::Sealed { 571 + type DarkScheme; 572 + type LightScheme; 573 type Fonts; 574 type Spacing; 575 + type DarkCodeTheme; 576 + type LightCodeTheme; 577 } 578 /// Empty state - all required fields are unset 579 pub struct Empty(()); 580 impl sealed::Sealed for Empty {} 581 impl State for Empty { 582 + type DarkScheme = Unset; 583 + type LightScheme = Unset; 584 type Fonts = Unset; 585 type Spacing = Unset; 586 + type DarkCodeTheme = Unset; 587 + type LightCodeTheme = Unset; 588 } 589 + ///State transition - sets the `dark_scheme` field to Set 590 + pub struct SetDarkScheme<S: State = Empty>(PhantomData<fn() -> S>); 591 + impl<S: State> sealed::Sealed for SetDarkScheme<S> {} 592 + impl<S: State> State for SetDarkScheme<S> { 593 + type DarkScheme = Set<members::dark_scheme>; 594 + type LightScheme = S::LightScheme; 595 type Fonts = S::Fonts; 596 type Spacing = S::Spacing; 597 + type DarkCodeTheme = S::DarkCodeTheme; 598 + type LightCodeTheme = S::LightCodeTheme; 599 + } 600 + ///State transition - sets the `light_scheme` field to Set 601 + pub struct SetLightScheme<S: State = Empty>(PhantomData<fn() -> S>); 602 + impl<S: State> sealed::Sealed for SetLightScheme<S> {} 603 + impl<S: State> State for SetLightScheme<S> { 604 + type DarkScheme = S::DarkScheme; 605 + type LightScheme = Set<members::light_scheme>; 606 + type Fonts = S::Fonts; 607 + type Spacing = S::Spacing; 608 + type DarkCodeTheme = S::DarkCodeTheme; 609 + type LightCodeTheme = S::LightCodeTheme; 610 } 611 ///State transition - sets the `fonts` field to Set 612 pub struct SetFonts<S: State = Empty>(PhantomData<fn() -> S>); 613 impl<S: State> sealed::Sealed for SetFonts<S> {} 614 impl<S: State> State for SetFonts<S> { 615 + type DarkScheme = S::DarkScheme; 616 + type LightScheme = S::LightScheme; 617 type Fonts = Set<members::fonts>; 618 type Spacing = S::Spacing; 619 + type DarkCodeTheme = S::DarkCodeTheme; 620 + type LightCodeTheme = S::LightCodeTheme; 621 } 622 ///State transition - sets the `spacing` field to Set 623 pub struct SetSpacing<S: State = Empty>(PhantomData<fn() -> S>); 624 impl<S: State> sealed::Sealed for SetSpacing<S> {} 625 impl<S: State> State for SetSpacing<S> { 626 + type DarkScheme = S::DarkScheme; 627 + type LightScheme = S::LightScheme; 628 type Fonts = S::Fonts; 629 type Spacing = Set<members::spacing>; 630 + type DarkCodeTheme = S::DarkCodeTheme; 631 + type LightCodeTheme = S::LightCodeTheme; 632 + } 633 + ///State transition - sets the `dark_code_theme` field to Set 634 + pub struct SetDarkCodeTheme<S: State = Empty>(PhantomData<fn() -> S>); 635 + impl<S: State> sealed::Sealed for SetDarkCodeTheme<S> {} 636 + impl<S: State> State for SetDarkCodeTheme<S> { 637 + type DarkScheme = S::DarkScheme; 638 + type LightScheme = S::LightScheme; 639 + type Fonts = S::Fonts; 640 + type Spacing = S::Spacing; 641 + type DarkCodeTheme = Set<members::dark_code_theme>; 642 + type LightCodeTheme = S::LightCodeTheme; 643 } 644 + ///State transition - sets the `light_code_theme` field to Set 645 + pub struct SetLightCodeTheme<S: State = Empty>(PhantomData<fn() -> S>); 646 + impl<S: State> sealed::Sealed for SetLightCodeTheme<S> {} 647 + impl<S: State> State for SetLightCodeTheme<S> { 648 + type DarkScheme = S::DarkScheme; 649 + type LightScheme = S::LightScheme; 650 type Fonts = S::Fonts; 651 type Spacing = S::Spacing; 652 + type DarkCodeTheme = S::DarkCodeTheme; 653 + type LightCodeTheme = Set<members::light_code_theme>; 654 } 655 /// Marker types for field names 656 #[allow(non_camel_case_types)] 657 pub mod members { 658 + ///Marker type for the `dark_scheme` field 659 + pub struct dark_scheme(()); 660 + ///Marker type for the `light_scheme` field 661 + pub struct light_scheme(()); 662 ///Marker type for the `fonts` field 663 pub struct fonts(()); 664 ///Marker type for the `spacing` field 665 pub struct spacing(()); 666 + ///Marker type for the `dark_code_theme` field 667 + pub struct dark_code_theme(()); 668 + ///Marker type for the `light_code_theme` field 669 + pub struct light_code_theme(()); 670 } 671 } 672 ··· 674 pub struct ThemeBuilder<'a, S: theme_state::State> { 675 _phantom_state: ::core::marker::PhantomData<fn() -> S>, 676 __unsafe_private_named: ( 677 + ::core::option::Option<ThemeDarkCodeTheme<'a>>, 678 + ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 679 ::core::option::Option<ThemeFonts<'a>>, 680 + ::core::option::Option<ThemeLightCodeTheme<'a>>, 681 + ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 682 ::core::option::Option<ThemeSpacing<'a>>, 683 ), 684 _phantom: ::core::marker::PhantomData<&'a ()>, ··· 696 pub fn new() -> Self { 697 ThemeBuilder { 698 _phantom_state: ::core::marker::PhantomData, 699 + __unsafe_private_named: (None, None, None, None, None, None), 700 _phantom: ::core::marker::PhantomData, 701 } 702 } ··· 705 impl<'a, S> ThemeBuilder<'a, S> 706 where 707 S: theme_state::State, 708 + S::DarkCodeTheme: theme_state::IsUnset, 709 { 710 + /// Set the `darkCodeTheme` field (required) 711 + pub fn dark_code_theme( 712 mut self, 713 + value: impl Into<ThemeDarkCodeTheme<'a>>, 714 + ) -> ThemeBuilder<'a, theme_state::SetDarkCodeTheme<S>> { 715 self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 716 ThemeBuilder { 717 _phantom_state: ::core::marker::PhantomData, ··· 724 impl<'a, S> ThemeBuilder<'a, S> 725 where 726 S: theme_state::State, 727 + S::DarkScheme: theme_state::IsUnset, 728 { 729 + /// Set the `darkScheme` field (required) 730 + pub fn dark_scheme( 731 mut self, 732 + value: impl Into<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 733 + ) -> ThemeBuilder<'a, theme_state::SetDarkScheme<S>> { 734 self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 735 ThemeBuilder { 736 _phantom_state: ::core::marker::PhantomData, ··· 762 impl<'a, S> ThemeBuilder<'a, S> 763 where 764 S: theme_state::State, 765 + S::LightCodeTheme: theme_state::IsUnset, 766 + { 767 + /// Set the `lightCodeTheme` field (required) 768 + pub fn light_code_theme( 769 + mut self, 770 + value: impl Into<ThemeLightCodeTheme<'a>>, 771 + ) -> ThemeBuilder<'a, theme_state::SetLightCodeTheme<S>> { 772 + self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); 773 + ThemeBuilder { 774 + _phantom_state: ::core::marker::PhantomData, 775 + __unsafe_private_named: self.__unsafe_private_named, 776 + _phantom: ::core::marker::PhantomData, 777 + } 778 + } 779 + } 780 + 781 + impl<'a, S> ThemeBuilder<'a, S> 782 + where 783 + S: theme_state::State, 784 + S::LightScheme: theme_state::IsUnset, 785 + { 786 + /// Set the `lightScheme` field (required) 787 + pub fn light_scheme( 788 + mut self, 789 + value: impl Into<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 790 + ) -> ThemeBuilder<'a, theme_state::SetLightScheme<S>> { 791 + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); 792 + ThemeBuilder { 793 + _phantom_state: ::core::marker::PhantomData, 794 + __unsafe_private_named: self.__unsafe_private_named, 795 + _phantom: ::core::marker::PhantomData, 796 + } 797 + } 798 + } 799 + 800 + impl<'a, S> ThemeBuilder<'a, S> 801 + where 802 + S: theme_state::State, 803 S::Spacing: theme_state::IsUnset, 804 { 805 /// Set the `spacing` field (required) ··· 807 mut self, 808 value: impl Into<ThemeSpacing<'a>>, 809 ) -> ThemeBuilder<'a, theme_state::SetSpacing<S>> { 810 + self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); 811 ThemeBuilder { 812 _phantom_state: ::core::marker::PhantomData, 813 __unsafe_private_named: self.__unsafe_private_named, ··· 819 impl<'a, S> ThemeBuilder<'a, S> 820 where 821 S: theme_state::State, 822 + S::DarkScheme: theme_state::IsSet, 823 + S::LightScheme: theme_state::IsSet, 824 S::Fonts: theme_state::IsSet, 825 S::Spacing: theme_state::IsSet, 826 + S::DarkCodeTheme: theme_state::IsSet, 827 + S::LightCodeTheme: theme_state::IsSet, 828 { 829 /// Build the final struct 830 pub fn build(self) -> Theme<'a> { 831 Theme { 832 + dark_code_theme: self.__unsafe_private_named.0.unwrap(), 833 + dark_scheme: self.__unsafe_private_named.1.unwrap(), 834 fonts: self.__unsafe_private_named.2.unwrap(), 835 + light_code_theme: self.__unsafe_private_named.3.unwrap(), 836 + light_scheme: self.__unsafe_private_named.4.unwrap(), 837 + spacing: self.__unsafe_private_named.5.unwrap(), 838 extra_data: Default::default(), 839 } 840 } ··· 847 >, 848 ) -> Theme<'a> { 849 Theme { 850 + dark_code_theme: self.__unsafe_private_named.0.unwrap(), 851 + dark_scheme: self.__unsafe_private_named.1.unwrap(), 852 fonts: self.__unsafe_private_named.2.unwrap(), 853 + light_code_theme: self.__unsafe_private_named.3.unwrap(), 854 + light_scheme: self.__unsafe_private_named.4.unwrap(), 855 + spacing: self.__unsafe_private_named.5.unwrap(), 856 extra_data: Some(extra_data), 857 } 858 } ··· 883 )] 884 #[serde(tag = "$type")] 885 #[serde(bound(deserialize = "'de: 'a"))] 886 + pub enum ThemeDarkCodeTheme<'a> { 887 #[serde(rename = "sh.weaver.notebook.theme#codeThemeName")] 888 CodeThemeName(Box<crate::sh_weaver::notebook::theme::CodeThemeName<'a>>), 889 #[serde(rename = "sh.weaver.notebook.theme#codeThemeFile")] ··· 902 Default 903 )] 904 #[serde(rename_all = "camelCase")] 905 + pub struct ThemeFonts<'a> { 906 #[serde(borrow)] 907 + pub body: jacquard_common::CowStr<'a>, 908 #[serde(borrow)] 909 + pub heading: jacquard_common::CowStr<'a>, 910 #[serde(borrow)] 911 + pub monospace: jacquard_common::CowStr<'a>, 912 } 913 914 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeFonts<'a> { 915 fn nsid() -> &'static str { 916 "sh.weaver.notebook.theme" 917 } 918 fn def_name() -> &'static str { 919 + "ThemeFonts" 920 } 921 fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 922 lexicon_doc_sh_weaver_notebook_theme() ··· 928 } 929 } 930 931 + #[jacquard_derive::open_union] 932 #[derive( 933 serde::Serialize, 934 serde::Deserialize, ··· 936 Clone, 937 PartialEq, 938 Eq, 939 + jacquard_derive::IntoStatic 940 )] 941 + #[serde(tag = "$type")] 942 + #[serde(bound(deserialize = "'de: 'a"))] 943 + pub enum ThemeLightCodeTheme<'a> { 944 + #[serde(rename = "sh.weaver.notebook.theme#codeThemeName")] 945 + CodeThemeName(Box<crate::sh_weaver::notebook::theme::CodeThemeName<'a>>), 946 + #[serde(rename = "sh.weaver.notebook.theme#codeThemeFile")] 947 + CodeThemeFile(Box<crate::sh_weaver::notebook::theme::CodeThemeFile<'a>>), 948 } 949 950 #[jacquard_derive::lexicon]
+46 -27
crates/weaver-common/src/lib.rs
··· 129 &self, 130 ident: &jacquard::types::ident::AtIdentifier<'_>, 131 title: &str, 132 - ) -> impl Future<Output = Result<Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, WeaverError>>; 133 } 134 135 impl<A: AgentSession + IdentityResolver> WeaverExt for Agent<A> { ··· 144 url_path: &'a str, 145 prev: Option<Tid>, 146 ) -> Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError> { 147 - let mime_type = MimeType::new_owned( 148 - blob.sniff_mime_type() 149 - .unwrap_or("applicaction/octet-stream"), 150 - ); 151 152 let blob = self.upload_blob(blob, mime_type).await?; 153 let publish_record = PublishedBlob::new() ··· 195 if let Ok(list) = resp.parse() { 196 for record in list.records { 197 let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { 198 - AgentError::from(ClientError::invalid_request("Failed to parse notebook record")) 199 })?; 200 if let Some(book_title) = notebook.title 201 && book_title == title ··· 265 266 // Add to notebook's entry_list 267 use weaver_api::sh_weaver::notebook::book::Book; 268 - let new_ref = StrongRef::new() 269 - .uri(response.uri) 270 - .cid(response.cid) 271 - .build(); 272 273 self.update_record::<Book>(&notebook_uri, |book| { 274 book.entry_list.push(new_ref); ··· 282 &self, 283 uri: &AtUri<'_>, 284 ) -> Result<(view::NotebookView<'static>, Vec<StrongRef<'static>>), WeaverError> { 285 use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 286 use weaver_api::sh_weaver::notebook::book::Book; 287 - use weaver_api::sh_weaver::notebook::AuthorListView; 288 - use jacquard::to_data; 289 290 let notebook = self 291 .get_record::<Book>(uri) ··· 302 let mut authors = Vec::new(); 303 304 for (index, author) in notebook.value.authors.iter().enumerate() { 305 - let author_uri = BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", author.did)) 306 - .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid author profile URI")))?; 307 let author_profile = self.fetch_record(&author_uri).await?; 308 309 authors.push( 310 AuthorListView::new() 311 .uri(author_uri.as_uri().clone()) 312 .record(to_data(&author_profile).map_err(|_| { 313 - AgentError::from(ClientError::invalid_request("Failed to serialize author profile")) 314 })?) 315 .index(index as i64) 316 .build(), ··· 347 notebook: &view::NotebookView<'a>, 348 entry_ref: &StrongRef<'_>, 349 ) -> Result<view::EntryView<'a>, WeaverError> { 350 - use weaver_api::sh_weaver::notebook::entry::Entry; 351 use jacquard::to_data; 352 353 let entry_uri = Entry::uri(entry_ref.uri.clone()) 354 .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid entry URI")))?; ··· 378 entries: &[StrongRef<'_>], 379 title: &str, 380 ) -> Result<Option<(view::BookEntryView<'a>, entry::Entry<'a>)>, WeaverError> { 381 - use weaver_api::sh_weaver::notebook::entry::Entry; 382 use weaver_api::sh_weaver::notebook::BookEntryRef; 383 384 for (index, entry_ref) in entries.iter().enumerate() { 385 let resp = self ··· 426 ident: &jacquard::types::ident::AtIdentifier<'_>, 427 title: &str, 428 ) -> Result<Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, WeaverError> { 429 use jacquard::types::collection::Collection; 430 use jacquard::types::nsid::Nsid; 431 use jacquard::xrpc::XrpcExt; 432 use weaver_api::com_atproto::repo::list_records::ListRecords; 433 - use weaver_api::sh_weaver::notebook::book::Book; 434 - use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 435 use weaver_api::sh_weaver::notebook::AuthorListView; 436 - use jacquard::to_data; 437 438 let (repo_did, pds_url) = match ident { 439 jacquard::types::ident::AtIdentifier::Did(did) => { 440 let pds = self.pds_for_did(did).await.map_err(|e| { 441 - AgentError::from(ClientError::from(e).with_context("Failed to resolve PDS for DID")) 442 })?; 443 (did.clone(), pds) 444 } 445 - jacquard::types::ident::AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| { 446 - AgentError::from(ClientError::from(e).with_context("Failed to resolve handle")) 447 - })?, 448 }; 449 450 // TODO: use the cursor to search through all records with this NSID for the repo ··· 463 if let Ok(list) = resp.parse() { 464 for record in list.records { 465 let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { 466 - AgentError::from(ClientError::invalid_request("Failed to parse notebook record")) 467 })?; 468 if let Some(book_title) = notebook.title 469 && book_title == title ··· 477 "at://{}/app.bsky.actor.profile/self", 478 author.did 479 )) 480 - .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid author profile URI")))?; 481 let author_profile = self.fetch_record(&author_uri).await?; 482 483 authors.push( 484 AuthorListView::new() 485 .uri(author_uri.as_uri().clone()) 486 .record(to_data(&author_profile).map_err(|_| { 487 - AgentError::from(ClientError::invalid_request("Failed to serialize author profile")) 488 })?) 489 .index(index as i64) 490 .build(),
··· 129 &self, 130 ident: &jacquard::types::ident::AtIdentifier<'_>, 131 title: &str, 132 + ) -> impl Future< 133 + Output = Result< 134 + Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, 135 + WeaverError, 136 + >, 137 + >; 138 } 139 140 impl<A: AgentSession + IdentityResolver> WeaverExt for Agent<A> { ··· 149 url_path: &'a str, 150 prev: Option<Tid>, 151 ) -> Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError> { 152 + let mime_type = 153 + MimeType::new_owned(blob.sniff_mime_type().unwrap_or("application/octet-stream")); 154 155 let blob = self.upload_blob(blob, mime_type).await?; 156 let publish_record = PublishedBlob::new() ··· 198 if let Ok(list) = resp.parse() { 199 for record in list.records { 200 let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { 201 + AgentError::from(ClientError::invalid_request( 202 + "Failed to parse notebook record", 203 + )) 204 })?; 205 if let Some(book_title) = notebook.title 206 && book_title == title ··· 270 271 // Add to notebook's entry_list 272 use weaver_api::sh_weaver::notebook::book::Book; 273 + let new_ref = StrongRef::new().uri(response.uri).cid(response.cid).build(); 274 275 self.update_record::<Book>(&notebook_uri, |book| { 276 book.entry_list.push(new_ref); ··· 284 &self, 285 uri: &AtUri<'_>, 286 ) -> Result<(view::NotebookView<'static>, Vec<StrongRef<'static>>), WeaverError> { 287 + use jacquard::to_data; 288 use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 289 + use weaver_api::sh_weaver::notebook::AuthorListView; 290 use weaver_api::sh_weaver::notebook::book::Book; 291 292 let notebook = self 293 .get_record::<Book>(uri) ··· 304 let mut authors = Vec::new(); 305 306 for (index, author) in notebook.value.authors.iter().enumerate() { 307 + let author_uri = 308 + BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", author.did)) 309 + .map_err(|_| { 310 + AgentError::from(ClientError::invalid_request("Invalid author profile URI")) 311 + })?; 312 let author_profile = self.fetch_record(&author_uri).await?; 313 314 authors.push( 315 AuthorListView::new() 316 .uri(author_uri.as_uri().clone()) 317 .record(to_data(&author_profile).map_err(|_| { 318 + AgentError::from(ClientError::invalid_request( 319 + "Failed to serialize author profile", 320 + )) 321 })?) 322 .index(index as i64) 323 .build(), ··· 354 notebook: &view::NotebookView<'a>, 355 entry_ref: &StrongRef<'_>, 356 ) -> Result<view::EntryView<'a>, WeaverError> { 357 use jacquard::to_data; 358 + use weaver_api::sh_weaver::notebook::entry::Entry; 359 360 let entry_uri = Entry::uri(entry_ref.uri.clone()) 361 .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid entry URI")))?; ··· 385 entries: &[StrongRef<'_>], 386 title: &str, 387 ) -> Result<Option<(view::BookEntryView<'a>, entry::Entry<'a>)>, WeaverError> { 388 use weaver_api::sh_weaver::notebook::BookEntryRef; 389 + use weaver_api::sh_weaver::notebook::entry::Entry; 390 391 for (index, entry_ref) in entries.iter().enumerate() { 392 let resp = self ··· 433 ident: &jacquard::types::ident::AtIdentifier<'_>, 434 title: &str, 435 ) -> Result<Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, WeaverError> { 436 + use jacquard::to_data; 437 use jacquard::types::collection::Collection; 438 use jacquard::types::nsid::Nsid; 439 use jacquard::xrpc::XrpcExt; 440 + use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 441 use weaver_api::com_atproto::repo::list_records::ListRecords; 442 use weaver_api::sh_weaver::notebook::AuthorListView; 443 + use weaver_api::sh_weaver::notebook::book::Book; 444 445 let (repo_did, pds_url) = match ident { 446 jacquard::types::ident::AtIdentifier::Did(did) => { 447 let pds = self.pds_for_did(did).await.map_err(|e| { 448 + AgentError::from( 449 + ClientError::from(e).with_context("Failed to resolve PDS for DID"), 450 + ) 451 })?; 452 (did.clone(), pds) 453 } 454 + jacquard::types::ident::AtIdentifier::Handle(handle) => { 455 + self.pds_for_handle(handle).await.map_err(|e| { 456 + AgentError::from(ClientError::from(e).with_context("Failed to resolve handle")) 457 + })? 458 + } 459 }; 460 461 // TODO: use the cursor to search through all records with this NSID for the repo ··· 474 if let Ok(list) = resp.parse() { 475 for record in list.records { 476 let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { 477 + AgentError::from(ClientError::invalid_request( 478 + "Failed to parse notebook record", 479 + )) 480 })?; 481 if let Some(book_title) = notebook.title 482 && book_title == title ··· 490 "at://{}/app.bsky.actor.profile/self", 491 author.did 492 )) 493 + .map_err(|_| { 494 + AgentError::from(ClientError::invalid_request( 495 + "Invalid author profile URI", 496 + )) 497 + })?; 498 let author_profile = self.fetch_record(&author_uri).await?; 499 500 authors.push( 501 AuthorListView::new() 502 .uri(author_uri.as_uri().clone()) 503 .record(to_data(&author_profile).map_err(|_| { 504 + AgentError::from(ClientError::invalid_request( 505 + "Failed to serialize author profile", 506 + )) 507 })?) 508 .index(index as i64) 509 .build(),
+67 -15
crates/weaver-renderer/src/atproto/writer.rs
··· 26 /// 27 /// This writer is designed for client-side rendering where embeds may have 28 /// pre-rendered content in their attributes. 29 - pub struct ClientWriter<W: StrWrite, E = ()> { 30 writer: W, 31 end_newline: bool, 32 in_non_writing_block: bool, ··· 40 embed_provider: Option<E>, 41 42 code_buffer: Option<(Option<String>, String)>, // (lang, content) 43 } 44 45 #[derive(Debug, Clone, Copy)] ··· 48 Body, 49 } 50 51 - impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 52 - pub fn new(writer: W) -> Self { 53 Self { 54 writer, 55 end_newline: true, 56 in_non_writing_block: false, ··· 60 numbers: HashMap::new(), 61 embed_provider: None, 62 code_buffer: None, 63 } 64 } 65 66 /// Add an embed content provider 67 - pub fn with_embed_provider(self, provider: E) -> ClientWriter<W, E> { 68 ClientWriter { 69 writer: self.writer, 70 end_newline: self.end_newline, 71 in_non_writing_block: self.in_non_writing_block, ··· 75 numbers: self.numbers, 76 embed_provider: Some(provider), 77 code_buffer: self.code_buffer, 78 } 79 } 80 - } 81 - 82 - impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 83 #[inline] 84 fn write_newline(&mut self) -> Result<(), W::Error> { 85 self.end_newline = true; ··· 96 } 97 98 /// Process markdown events and write HTML 99 - pub fn run<'a>(mut self, events: impl Iterator<Item = Event<'a>>) -> Result<W, W::Error> { 100 - for event in events { 101 self.process_event(event)?; 102 } 103 Ok(self.writer) 104 } 105 106 fn process_event(&mut self, event: Event<'_>) -> Result<(), W::Error> { 107 use Event::*; 108 match event { ··· 368 self.write("<img src=\"")?; 369 escape_href(&mut self.writer, &dest_url)?; 370 self.write("\" alt=\"")?; 371 if !title.is_empty() { 372 escape_html(&mut self.writer, &title)?; 373 } 374 if let Some(attrs) = attrs { 375 if !attrs.classes.is_empty() { 376 - self.write("\" class=\"")?; 377 for (i, class) in attrs.classes.iter().enumerate() { 378 if i > 0 { 379 self.write(" ")?; 380 } 381 escape_html(&mut self.writer, class)?; 382 } 383 } 384 - self.write("\"")?; 385 for (attr, value) in &attrs.attrs { 386 self.write(" ")?; 387 escape_html(&mut self.writer, attr)?; ··· 389 escape_html(&mut self.writer, value)?; 390 self.write("\"")?; 391 } 392 - } else { 393 - self.write("\"")?; 394 } 395 self.write(" />") 396 } ··· 501 TagEnd::Strong => self.write("</strong>"), 502 TagEnd::Strikethrough => self.write("</del>"), 503 TagEnd::Link => self.write("</a>"), 504 - TagEnd::Image => Ok(()), 505 TagEnd::Embed => Ok(()), 506 TagEnd::WeaverBlock(_) => { 507 self.in_non_writing_block = false; ··· 516 } 517 } 518 519 - impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 520 fn write_embed( 521 &mut self, 522 embed_type: EmbedType,
··· 26 /// 27 /// This writer is designed for client-side rendering where embeds may have 28 /// pre-rendered content in their attributes. 29 + pub struct ClientWriter<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E = ()> { 30 + events: I, 31 writer: W, 32 end_newline: bool, 33 in_non_writing_block: bool, ··· 41 embed_provider: Option<E>, 42 43 code_buffer: Option<(Option<String>, String)>, // (lang, content) 44 + _phantom: std::marker::PhantomData<&'a ()>, 45 } 46 47 #[derive(Debug, Clone, Copy)] ··· 50 Body, 51 } 52 53 + impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { 54 + pub fn new(events: I, writer: W) -> Self { 55 Self { 56 + events, 57 writer, 58 end_newline: true, 59 in_non_writing_block: false, ··· 63 numbers: HashMap::new(), 64 embed_provider: None, 65 code_buffer: None, 66 + _phantom: std::marker::PhantomData, 67 } 68 } 69 70 /// Add an embed content provider 71 + pub fn with_embed_provider(self, provider: E) -> ClientWriter<'a, I, W, E> { 72 ClientWriter { 73 + events: self.events, 74 writer: self.writer, 75 end_newline: self.end_newline, 76 in_non_writing_block: self.in_non_writing_block, ··· 80 numbers: self.numbers, 81 embed_provider: Some(provider), 82 code_buffer: self.code_buffer, 83 + _phantom: std::marker::PhantomData, 84 } 85 } 86 #[inline] 87 fn write_newline(&mut self) -> Result<(), W::Error> { 88 self.end_newline = true; ··· 99 } 100 101 /// Process markdown events and write HTML 102 + pub fn run(mut self) -> Result<W, W::Error> { 103 + while let Some(event) = self.events.next() { 104 self.process_event(event)?; 105 } 106 Ok(self.writer) 107 } 108 109 + // Consume raw text events until end tag, for alt attributes 110 + fn raw_text(&mut self) -> Result<(), W::Error> { 111 + use Event::*; 112 + let mut nest = 0; 113 + while let Some(event) = self.events.next() { 114 + match event { 115 + Start(_) => nest += 1, 116 + End(_) => { 117 + if nest == 0 { 118 + break; 119 + } 120 + nest -= 1; 121 + } 122 + Html(_) => {} 123 + InlineHtml(text) | Code(text) | Text(text) => { 124 + // Don't use escape_html_body_text here. 125 + // The output of this function is used in the `alt` attribute. 126 + escape_html(&mut self.writer, &text)?; 127 + self.end_newline = text.ends_with('\n'); 128 + } 129 + InlineMath(text) => { 130 + self.write("$")?; 131 + escape_html(&mut self.writer, &text)?; 132 + self.write("$")?; 133 + } 134 + DisplayMath(text) => { 135 + self.write("$$")?; 136 + escape_html(&mut self.writer, &text)?; 137 + self.write("$$")?; 138 + } 139 + SoftBreak | HardBreak | Rule => { 140 + self.write(" ")?; 141 + } 142 + FootnoteReference(name) => { 143 + let len = self.numbers.len() + 1; 144 + let number = *self.numbers.entry(name.into_string()).or_insert(len); 145 + write!(&mut self.writer, "[{}]", number)?; 146 + } 147 + TaskListMarker(true) => self.write("[x]")?, 148 + TaskListMarker(false) => self.write("[ ]")?, 149 + WeaverBlock(_) => {} 150 + } 151 + } 152 + Ok(()) 153 + } 154 + 155 fn process_event(&mut self, event: Event<'_>) -> Result<(), W::Error> { 156 use Event::*; 157 match event { ··· 417 self.write("<img src=\"")?; 418 escape_href(&mut self.writer, &dest_url)?; 419 self.write("\" alt=\"")?; 420 + // Consume text events for alt attribute 421 + self.raw_text()?; 422 + self.write("\"")?; 423 if !title.is_empty() { 424 + self.write(" title=\"")?; 425 escape_html(&mut self.writer, &title)?; 426 + self.write("\"")?; 427 } 428 if let Some(attrs) = attrs { 429 if !attrs.classes.is_empty() { 430 + self.write(" class=\"")?; 431 for (i, class) in attrs.classes.iter().enumerate() { 432 if i > 0 { 433 self.write(" ")?; 434 } 435 escape_html(&mut self.writer, class)?; 436 } 437 + self.write("\"")?; 438 } 439 for (attr, value) in &attrs.attrs { 440 self.write(" ")?; 441 escape_html(&mut self.writer, attr)?; ··· 443 escape_html(&mut self.writer, value)?; 444 self.write("\"")?; 445 } 446 } 447 self.write(" />") 448 } ··· 553 TagEnd::Strong => self.write("</strong>"), 554 TagEnd::Strikethrough => self.write("</del>"), 555 TagEnd::Link => self.write("</a>"), 556 + TagEnd::Image => Ok(()), // No-op: raw_text() already consumed the End(Image) event 557 TagEnd::Embed => Ok(()), 558 TagEnd::WeaverBlock(_) => { 559 self.in_non_writing_block = false; ··· 568 } 569 } 570 571 + impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { 572 fn write_embed( 573 &mut self, 574 embed_type: EmbedType,
+1 -1
crates/weaver-renderer/src/code_pretty.rs
··· 27 .find_syntax_by_first_line(code.as_ref()) 28 .unwrap_or_else(|| syn_set.find_syntax_plain_text()) 29 }; 30 - writer.write_str("<pre><code class=\"language-")?; 31 writer.write_str(&lang_syn.name)?; 32 writer.write_str("\">")?; 33
··· 27 .find_syntax_by_first_line(code.as_ref()) 28 .unwrap_or_else(|| syn_set.find_syntax_plain_text()) 29 }; 30 + writer.write_str("<pre><code class=\"wvrcode-code language-")?; 31 writer.write_str(&lang_syn.name)?; 32 writer.write_str("\">")?; 33
+224 -47
crates/weaver-renderer/src/css.rs
··· 1 - use crate::theme::Theme; 2 use miette::IntoDiagnostic; 3 use std::io::Cursor; 4 use syntect::highlighting::ThemeSet; 5 use syntect::html::{ClassStyle, css_for_theme_with_class_style}; 6 use weaver_api::com_atproto::sync::get_blob::GetBlob; 7 - use weaver_api::sh_weaver::notebook::theme::ThemeCodeTheme; 8 use weaver_common::jacquard::client::BasicClient; 9 use weaver_common::jacquard::prelude::*; 10 use weaver_common::jacquard::xrpc::XrpcExt; ··· 13 const ROSE_PINE_THEME: &str = include_str!("../themes/rose-pine.tmTheme"); 14 const ROSE_PINE_DAWN_THEME: &str = include_str!("../themes/rose-pine-dawn.tmTheme"); 15 16 - pub fn generate_base_css(theme: &Theme) -> String { 17 format!( 18 r#"/* CSS Reset */ 19 *, *::before, *::after {{ ··· 22 padding: 0; 23 }} 24 25 - /* CSS Variables */ 26 :root {{ 27 - --color-background: {}; 28 - --color-foreground: {}; 29 - --color-link: {}; 30 - --color-link-hover: {}; 31 --color-primary: {}; 32 --color-secondary: {}; 33 34 --font-body: {}; 35 --font-heading: {}; ··· 40 --spacing-scale: {}; 41 }} 42 43 /* Base Styles */ 44 html {{ 45 font-size: var(--spacing-base); ··· 48 49 body {{ 50 font-family: var(--font-body); 51 - color: var(--color-foreground); 52 - background-color: var(--color-background); 53 max-width: 90ch; 54 margin: 0 auto; 55 padding: 2rem 1rem; ··· 95 }} 96 97 a:hover {{ 98 - color: var(--color-link-hover); 99 text-decoration: underline; 100 }} 101 102 /* Lists */ ··· 112 /* Code */ 113 code {{ 114 font-family: var(--font-mono); 115 - background-color: rgba(0, 0, 0, 0.05); 116 padding: 0.125rem 0.25rem; 117 - border-radius: 3px; 118 font-size: 0.9em; 119 }} 120 121 pre {{ 122 overflow-x: auto; 123 margin-bottom: 1rem; 124 }} 125 126 pre code {{ 127 display: block; 128 padding: 1rem; 129 - background-color: rgba(0, 0, 0, 0.03); 130 - border-radius: 5px; 131 }} 132 133 /* Math */ ··· 143 144 /* Blockquotes */ 145 blockquote {{ 146 - border-left: 4px solid var(--color-link); 147 padding-left: 1rem; 148 padding-right: 1rem; 149 margin: 1rem 0; 150 - font-style: italic; 151 }} 152 153 /* Tables */ ··· 158 }} 159 160 th, td {{ 161 - border: 1px solid rgba(0, 0, 0, 0.1); 162 padding: 0.5rem; 163 text-align: left; 164 }} 165 166 th {{ 167 - background-color: rgba(0, 0, 0, 0.05); 168 font-weight: 600; 169 }} 170 171 /* Footnotes */ 172 .footnote-reference {{ 173 font-size: 0.8em; 174 }} 175 176 .footnote-definition {{ 177 margin-top: 2rem; 178 padding-top: 0.5rem; 179 - border-top: 1px solid rgba(0, 0, 0, 0.1); 180 font-size: 0.9em; 181 }} 182 183 .footnote-definition-label {{ 184 font-weight: 600; 185 margin-right: 0.5rem; 186 }} 187 188 /* Images */ ··· 191 height: auto; 192 display: block; 193 margin: 1rem 0; 194 }} 195 196 /* Horizontal Rule */ 197 hr {{ 198 border: none; 199 - border-top: 1px solid rgba(0, 0, 0, 0.1); 200 margin: 2rem 0; 201 }} 202 "#, 203 - theme.colours.background, 204 - theme.colours.foreground, 205 - theme.colours.link, 206 - theme.colours.link_hover, 207 - theme.colours.primary, 208 - theme.colours.secondary, 209 - theme.fonts.body, 210 - theme.fonts.heading, 211 - theme.fonts.monospace, 212 - theme.spacing.base_size, 213 - theme.spacing.line_height, 214 - theme.spacing.scale, 215 ) 216 } 217 218 - pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result<String> { 219 - let syntect_theme = match &theme.code_theme { 220 - ThemeCodeTheme::CodeThemeName(name) => { 221 match name.as_str() { 222 "rose-pine" => { 223 let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); ··· 225 .into_diagnostic() 226 .map_err(|e| { 227 miette::miette!("Failed to load embedded rose-pine theme: {}", e) 228 - })? 229 } 230 "rose-pine-dawn" => { 231 let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes()); ··· 233 .into_diagnostic() 234 .map_err(|e| { 235 miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e) 236 - })? 237 } 238 _ => { 239 // Fall back to syntect's built-in themes ··· 241 theme_set 242 .themes 243 .get(name.as_str()) 244 - .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name))? 245 - .clone() 246 } 247 } 248 } 249 - ThemeCodeTheme::CodeThemeFile(file) => { 250 let client = BasicClient::unauthenticated(); 251 let pds = client.pds_for_did(&file.did).await?; 252 let blob = client ··· 263 let mut cursor = Cursor::new(blob); 264 ThemeSet::load_from_reader(&mut cursor) 265 .into_diagnostic() 266 - .map_err(|e| miette::miette!("Failed to download theme: {}", e))? 267 } 268 _ => { 269 let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); 270 ThemeSet::load_from_reader(&mut cursor) 271 .into_diagnostic() 272 - .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e))? 273 } 274 - }; 275 276 - let css = css_for_theme_with_class_style( 277 - &syntect_theme, 278 ClassStyle::SpacedPrefixed { 279 prefix: crate::code_pretty::CSS_PREFIX, 280 }, 281 ) 282 .into_diagnostic()?; 283 284 - Ok(css) 285 }
··· 1 + use crate::theme::{ResolvedTheme, ThemeDarkCodeTheme, ThemeLightCodeTheme}; 2 use miette::IntoDiagnostic; 3 use std::io::Cursor; 4 use syntect::highlighting::ThemeSet; 5 use syntect::html::{ClassStyle, css_for_theme_with_class_style}; 6 use weaver_api::com_atproto::sync::get_blob::GetBlob; 7 use weaver_common::jacquard::client::BasicClient; 8 use weaver_common::jacquard::prelude::*; 9 use weaver_common::jacquard::xrpc::XrpcExt; ··· 12 const ROSE_PINE_THEME: &str = include_str!("../themes/rose-pine.tmTheme"); 13 const ROSE_PINE_DAWN_THEME: &str = include_str!("../themes/rose-pine-dawn.tmTheme"); 14 15 + pub fn generate_base_css(theme: &ResolvedTheme) -> String { 16 + let dark = &theme.dark_scheme; 17 + let light = &theme.light_scheme; 18 + let fonts = &theme.fonts; 19 + let spacing = &theme.spacing; 20 + 21 format!( 22 r#"/* CSS Reset */ 23 *, *::before, *::after {{ ··· 26 padding: 0; 27 }} 28 29 + /* CSS Variables - Light Mode (default) */ 30 :root {{ 31 + --color-base: {}; 32 + --color-surface: {}; 33 + --color-overlay: {}; 34 + --color-text: {}; 35 + --color-muted: {}; 36 + --color-subtle: {}; 37 + --color-emphasis: {}; 38 --color-primary: {}; 39 --color-secondary: {}; 40 + --color-tertiary: {}; 41 + --color-error: {}; 42 + --color-warning: {}; 43 + --color-success: {}; 44 + --color-border: {}; 45 + --color-link: {}; 46 + --color-highlight: {}; 47 48 --font-body: {}; 49 --font-heading: {}; ··· 54 --spacing-scale: {}; 55 }} 56 57 + /* CSS Variables - Dark Mode */ 58 + @media (prefers-color-scheme: dark) {{ 59 + :root {{ 60 + --color-base: {}; 61 + --color-surface: {}; 62 + --color-overlay: {}; 63 + --color-text: {}; 64 + --color-muted: {}; 65 + --color-subtle: {}; 66 + --color-emphasis: {}; 67 + --color-primary: {}; 68 + --color-secondary: {}; 69 + --color-tertiary: {}; 70 + --color-error: {}; 71 + --color-warning: {}; 72 + --color-success: {}; 73 + --color-border: {}; 74 + --color-link: {}; 75 + --color-highlight: {}; 76 + }} 77 + }} 78 + 79 /* Base Styles */ 80 html {{ 81 font-size: var(--spacing-base); ··· 84 85 body {{ 86 font-family: var(--font-body); 87 + color: var(--color-text); 88 + background-color: var(--color-base); 89 max-width: 90ch; 90 margin: 0 auto; 91 padding: 2rem 1rem; ··· 131 }} 132 133 a:hover {{ 134 + color: var(--color-emphasis); 135 text-decoration: underline; 136 + }} 137 + 138 + /* Selection */ 139 + ::selection {{ 140 + background: var(--color-highlight); 141 + color: var(--color-text); 142 }} 143 144 /* Lists */ ··· 154 /* Code */ 155 code {{ 156 font-family: var(--font-mono); 157 + background: var(--color-surface); 158 padding: 0.125rem 0.25rem; 159 + border-radius: 4px; 160 font-size: 0.9em; 161 }} 162 163 pre {{ 164 overflow-x: auto; 165 margin-bottom: 1rem; 166 + border-radius: 5px; 167 + border: 1px solid var(--color-border); 168 + box-sizing: border-box; 169 }} 170 171 + /* Code blocks inside pre are handled by syntax theme */ 172 pre code {{ 173 display: block; 174 + width: fit-content; 175 + min-width: 100%; 176 padding: 1rem; 177 + background: var(--color-surface); 178 }} 179 180 /* Math */ ··· 190 191 /* Blockquotes */ 192 blockquote {{ 193 + border-left: 2px solid var(--color-secondary); 194 + background: var(--color-surface); 195 padding-left: 1rem; 196 padding-right: 1rem; 197 + padding-top: 0.5rem; 198 + padding-bottom: 0.04rem; 199 margin: 1rem 0; 200 + font-size: 0.95em; 201 + border-bottom-right-radius: 5px; 202 + border-top-right-radius: 5px; 203 + }} 204 }} 205 206 /* Tables */ ··· 211 }} 212 213 th, td {{ 214 + border: 1px solid var(--color-border); 215 padding: 0.5rem; 216 text-align: left; 217 }} 218 219 th {{ 220 + background: var(--color-surface); 221 font-weight: 600; 222 }} 223 224 + tr:hover {{ 225 + background: var(--color-surface); 226 + }} 227 + 228 /* Footnotes */ 229 .footnote-reference {{ 230 font-size: 0.8em; 231 + color: var(--color-subtle); 232 }} 233 234 .footnote-definition {{ 235 margin-top: 2rem; 236 padding-top: 0.5rem; 237 + border-top: 1px solid var(--color-border); 238 font-size: 0.9em; 239 }} 240 241 .footnote-definition-label {{ 242 font-weight: 600; 243 margin-right: 0.5rem; 244 + color: var(--color-primary); 245 }} 246 247 /* Images */ ··· 250 height: auto; 251 display: block; 252 margin: 1rem 0; 253 + border-radius: 4px; 254 }} 255 256 /* Horizontal Rule */ 257 hr {{ 258 border: none; 259 + border-top: 2px solid var(--color-border); 260 margin: 2rem 0; 261 }} 262 "#, 263 + // Light mode colours 264 + light.base, 265 + light.surface, 266 + light.overlay, 267 + light.text, 268 + light.muted, 269 + light.subtle, 270 + light.emphasis, 271 + light.primary, 272 + light.secondary, 273 + light.tertiary, 274 + light.error, 275 + light.warning, 276 + light.success, 277 + light.border, 278 + light.link, 279 + light.highlight, 280 + // Fonts and spacing 281 + fonts.body, 282 + fonts.heading, 283 + fonts.monospace, 284 + spacing.base_size, 285 + spacing.line_height, 286 + spacing.scale, 287 + // Dark mode colours 288 + dark.base, 289 + dark.surface, 290 + dark.overlay, 291 + dark.text, 292 + dark.muted, 293 + dark.subtle, 294 + dark.emphasis, 295 + dark.primary, 296 + dark.secondary, 297 + dark.tertiary, 298 + dark.error, 299 + dark.warning, 300 + dark.success, 301 + dark.border, 302 + dark.link, 303 + dark.highlight, 304 ) 305 } 306 307 + async fn load_syntect_dark_theme( 308 + code_theme: &ThemeDarkCodeTheme<'_>, 309 + ) -> miette::Result<syntect::highlighting::Theme> { 310 + match code_theme { 311 + ThemeDarkCodeTheme::CodeThemeName(name) => { 312 + match name.as_str() { 313 + "rose-pine" => { 314 + let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); 315 + ThemeSet::load_from_reader(&mut cursor) 316 + .into_diagnostic() 317 + .map_err(|e| { 318 + miette::miette!("Failed to load embedded rose-pine theme: {}", e) 319 + }) 320 + } 321 + "rose-pine-dawn" => { 322 + let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes()); 323 + ThemeSet::load_from_reader(&mut cursor) 324 + .into_diagnostic() 325 + .map_err(|e| { 326 + miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e) 327 + }) 328 + } 329 + _ => { 330 + // Fall back to syntect's built-in themes 331 + let theme_set = ThemeSet::load_defaults(); 332 + theme_set 333 + .themes 334 + .get(name.as_str()) 335 + .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name)) 336 + .cloned() 337 + } 338 + } 339 + } 340 + ThemeDarkCodeTheme::CodeThemeFile(file) => { 341 + let client = BasicClient::unauthenticated(); 342 + let pds = client.pds_for_did(&file.did).await?; 343 + let blob = client 344 + .xrpc(pds) 345 + .send( 346 + &GetBlob::new() 347 + .did(file.did.clone()) 348 + .cid(file.content.blob().cid().clone()) 349 + .build(), 350 + ) 351 + .await? 352 + .buffer() 353 + .clone(); 354 + let mut cursor = Cursor::new(blob); 355 + ThemeSet::load_from_reader(&mut cursor) 356 + .into_diagnostic() 357 + .map_err(|e| miette::miette!("Failed to download theme: {}", e)) 358 + } 359 + _ => { 360 + let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); 361 + ThemeSet::load_from_reader(&mut cursor) 362 + .into_diagnostic() 363 + .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e)) 364 + } 365 + } 366 + } 367 + 368 + async fn load_syntect_light_theme( 369 + code_theme: &ThemeLightCodeTheme<'_>, 370 + ) -> miette::Result<syntect::highlighting::Theme> { 371 + match code_theme { 372 + ThemeLightCodeTheme::CodeThemeName(name) => { 373 match name.as_str() { 374 "rose-pine" => { 375 let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); ··· 377 .into_diagnostic() 378 .map_err(|e| { 379 miette::miette!("Failed to load embedded rose-pine theme: {}", e) 380 + }) 381 } 382 "rose-pine-dawn" => { 383 let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes()); ··· 385 .into_diagnostic() 386 .map_err(|e| { 387 miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e) 388 + }) 389 } 390 _ => { 391 // Fall back to syntect's built-in themes ··· 393 theme_set 394 .themes 395 .get(name.as_str()) 396 + .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name)) 397 + .cloned() 398 } 399 } 400 } 401 + ThemeLightCodeTheme::CodeThemeFile(file) => { 402 let client = BasicClient::unauthenticated(); 403 let pds = client.pds_for_did(&file.did).await?; 404 let blob = client ··· 415 let mut cursor = Cursor::new(blob); 416 ThemeSet::load_from_reader(&mut cursor) 417 .into_diagnostic() 418 + .map_err(|e| miette::miette!("Failed to download theme: {}", e)) 419 } 420 _ => { 421 let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); 422 ThemeSet::load_from_reader(&mut cursor) 423 .into_diagnostic() 424 + .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e)) 425 } 426 + } 427 + } 428 + 429 + pub async fn generate_syntax_css(theme: &ResolvedTheme<'_>) -> miette::Result<String> { 430 + // Load both themes 431 + let dark_syntect_theme = load_syntect_dark_theme(&theme.dark_code_theme).await?; 432 + let light_syntect_theme = load_syntect_light_theme(&theme.light_code_theme).await?; 433 + 434 + // Generate dark mode CSS (default) 435 + let dark_css = css_for_theme_with_class_style( 436 + &dark_syntect_theme, 437 + ClassStyle::SpacedPrefixed { 438 + prefix: crate::code_pretty::CSS_PREFIX, 439 + }, 440 + ) 441 + .into_diagnostic()?; 442 443 + // Generate light mode CSS 444 + let light_css = css_for_theme_with_class_style( 445 + &light_syntect_theme, 446 ClassStyle::SpacedPrefixed { 447 prefix: crate::code_pretty::CSS_PREFIX, 448 }, 449 ) 450 .into_diagnostic()?; 451 452 + // Combine with media queries 453 + let mut result = String::new(); 454 + result.push_str("/* Syntax highlighting - Light Mode (default) */\n"); 455 + result.push_str(&light_css); 456 + result.push_str("\n\n/* Syntax highlighting - Dark Mode */\n"); 457 + result.push_str("@media (prefers-color-scheme: dark) {\n"); 458 + result.push_str(&dark_css); 459 + result.push_str("}\n"); 460 + 461 + Ok(result) 462 }
+2 -2
crates/weaver-renderer/src/static_site.rs
··· 17 document::{CssMode, write_document_footer, write_document_head}, 18 writer::StaticPageWriter, 19 }, 20 - theme::default_theme, 21 utils::flatten_dir_to_just_one_parent, 22 walker::{WalkOptions, vault_contents}, 23 }; ··· 239 .await 240 .into_diagnostic()?; 241 242 - let default_theme = default_theme(); 243 let theme = self.context.theme.as_deref().unwrap_or(&default_theme); 244 245 // Write base.css
··· 17 document::{CssMode, write_document_footer, write_document_head}, 18 writer::StaticPageWriter, 19 }, 20 + theme::default_resolved_theme, 21 utils::flatten_dir_to_just_one_parent, 22 walker::{WalkOptions, vault_contents}, 23 }; ··· 239 .await 240 .into_diagnostic()?; 241 242 + let default_theme = default_resolved_theme(); 243 let theme = self.context.theme.as_deref().unwrap_or(&default_theme); 244 245 // Write base.css
+6 -4
crates/weaver-renderer/src/static_site/context.rs
··· 1 use crate::static_site::{StaticSiteOptions}; 2 - use crate::theme::Theme; 3 use crate::{Frontmatter, NotebookContext,default_md_options}; 4 use dashmap::DashMap; 5 use markdown_weaver::{CowStr, EmbedType, Tag, WeaverAttributes}; ··· 39 pub client: Option<reqwest::Client>, 40 agent: Option<Arc<Agent<A>>>, 41 42 - pub theme: Option<Arc<Theme<'static>>>, 43 pub katex_source: Option<KaTeXSource>, 44 pub syntax_set: Arc<SyntaxSet>, 45 pub index_file: Option<PathBuf>, ··· 122 } 123 } 124 pub fn new(root: PathBuf, destination: PathBuf, session: Option<A>) -> Self { 125 Self { 126 start_at: root.clone(), 127 root, ··· 136 position: 0, 137 client: Some(reqwest::Client::default()), 138 agent: session.map(|session| Arc::new(Agent::new(session))), 139 - theme: None, 140 katex_source: None, 141 syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()), 142 index_file: None, 143 } 144 } 145 146 - pub fn with_theme(mut self, theme: Theme<'static>) -> Self { 147 self.theme = Some(Arc::new(theme)); 148 self 149 }
··· 1 use crate::static_site::{StaticSiteOptions}; 2 + use crate::theme::ResolvedTheme; 3 use crate::{Frontmatter, NotebookContext,default_md_options}; 4 use dashmap::DashMap; 5 use markdown_weaver::{CowStr, EmbedType, Tag, WeaverAttributes}; ··· 39 pub client: Option<reqwest::Client>, 40 agent: Option<Arc<Agent<A>>>, 41 42 + pub theme: Option<Arc<ResolvedTheme<'static>>>, 43 pub katex_source: Option<KaTeXSource>, 44 pub syntax_set: Arc<SyntaxSet>, 45 pub index_file: Option<PathBuf>, ··· 122 } 123 } 124 pub fn new(root: PathBuf, destination: PathBuf, session: Option<A>) -> Self { 125 + use crate::theme::default_resolved_theme; 126 + 127 Self { 128 start_at: root.clone(), 129 root, ··· 138 position: 0, 139 client: Some(reqwest::Client::default()), 140 agent: session.map(|session| Arc::new(Agent::new(session))), 141 + theme: Some(Arc::new(default_resolved_theme())), 142 katex_source: None, 143 syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()), 144 index_file: None, 145 } 146 } 147 148 + pub fn with_theme(mut self, theme: ResolvedTheme<'static>) -> Self { 149 self.theme = Some(Arc::new(theme)); 150 self 151 }
+2 -2
crates/weaver-renderer/src/static_site/document.rs
··· 1 use crate::css::{generate_base_css, generate_syntax_css}; 2 use crate::static_site::context::{KaTeXSource, StaticSiteContext}; 3 - use crate::theme::{Theme, default_theme}; 4 use miette::IntoDiagnostic; 5 use weaver_common::jacquard::client::AgentSession; 6 ··· 98 .into_diagnostic()?; 99 } 100 CssMode::Inline => { 101 - let default_theme = default_theme(); 102 let theme = context.theme.as_deref().unwrap_or(&default_theme); 103 104 writer.write_all(b" <style>\n").await.into_diagnostic()?;
··· 1 use crate::css::{generate_base_css, generate_syntax_css}; 2 use crate::static_site::context::{KaTeXSource, StaticSiteContext}; 3 + use crate::theme::default_resolved_theme; 4 use miette::IntoDiagnostic; 5 use weaver_common::jacquard::client::AgentSession; 6 ··· 98 .into_diagnostic()?; 99 } 100 CssMode::Inline => { 101 + let default_theme = default_resolved_theme(); 102 let theme = context.theme.as_deref().unwrap_or(&default_theme); 103 104 writer.write_all(b" <style>\n").await.into_diagnostic()?;
+128 -31
crates/weaver-renderer/src/theme.rs
··· 1 pub use weaver_api::sh_weaver::notebook::theme::{ 2 - Theme, ThemeCodeTheme, ThemeColours, ThemeFonts, ThemeSpacing, 3 }; 4 use weaver_common::jacquard::CowStr; 5 use weaver_common::jacquard::cowstr::ToCowStr; 6 7 - pub fn default_theme() -> Theme<'static> { 8 - Theme::new() 9 - .code_theme(ThemeCodeTheme::CodeThemeName(Box::new( 10 - "rose-pine".to_cowstr(), 11 - ))) 12 - .colours(ThemeColours { 13 - background: CowStr::new_static("#191724"), 14 - foreground: CowStr::new_static("#e0def4"), 15 - link: CowStr::new_static("#31748f"), 16 - link_hover: CowStr::new_static("#9ccfd8"), 17 - primary: CowStr::new_static("#c4a7e7"), 18 - secondary: CowStr::new_static("#3e8fb0"), 19 20 - ..Default::default() 21 - }).fonts(ThemeFonts { 22 - body: CowStr::new_static( 23 - "IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 24 - ), 25 - heading:CowStr::new_static( 26 - "IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 27 - ), 28 - monospace: CowStr::new_static( 29 - "'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace", 30 - ), 31 - ..Default::default() 32 - }).spacing(ThemeSpacing { 33 - base_size: CowStr::new_static("16px"), 34 - line_height: CowStr::new_static("1.6"), 35 - scale: CowStr::new_static("1.25"), 36 - ..Default::default() 37 - }).build() 38 }
··· 1 + use miette::IntoDiagnostic; 2 + pub use weaver_api::sh_weaver::notebook::colour_scheme::{ColourScheme, ColourSchemeColours}; 3 pub use weaver_api::sh_weaver::notebook::theme::{ 4 + Theme, ThemeDarkCodeTheme, ThemeFonts, ThemeLightCodeTheme, ThemeSpacing, 5 }; 6 use weaver_common::jacquard::CowStr; 7 + use weaver_common::jacquard::IntoStatic; 8 + use weaver_common::jacquard::client::AgentSession; 9 use weaver_common::jacquard::cowstr::ToCowStr; 10 + use weaver_common::jacquard::prelude::*; 11 + 12 + /// A theme with resolved colour schemes (no strongRefs, actual data) 13 + #[derive(Clone, Debug)] 14 + pub struct ResolvedTheme<'a> { 15 + pub dark_scheme: ColourSchemeColours<'a>, 16 + pub light_scheme: ColourSchemeColours<'a>, 17 + pub fonts: ThemeFonts<'a>, 18 + pub spacing: ThemeSpacing<'a>, 19 + pub dark_code_theme: ThemeDarkCodeTheme<'a>, 20 + pub light_code_theme: ThemeLightCodeTheme<'a>, 21 + } 22 + 23 + pub fn default_colour_scheme_dark() -> ColourSchemeColours<'static> { 24 + ColourSchemeColours { 25 + base: CowStr::new_static("#191724"), 26 + surface: CowStr::new_static("#1f1d2e"), 27 + overlay: CowStr::new_static("#26233a"), 28 + text: CowStr::new_static("#e0def4"), 29 + muted: CowStr::new_static("#6e6a86"), 30 + subtle: CowStr::new_static("#908caa"), 31 + emphasis: CowStr::new_static("#e0def4"), 32 + primary: CowStr::new_static("#c4a7e7"), 33 + secondary: CowStr::new_static("#3e8fb0"), 34 + tertiary: CowStr::new_static("#9ccfd8"), 35 + error: CowStr::new_static("#eb6f92"), 36 + warning: CowStr::new_static("#f6c177"), 37 + success: CowStr::new_static("#31748f"), 38 + border: CowStr::new_static("#403d52"), 39 + link: CowStr::new_static("#ebbcba"), 40 + highlight: CowStr::new_static("#524f67"), 41 + ..Default::default() 42 + } 43 + } 44 45 + pub fn default_fonts() -> ThemeFonts<'static> { 46 + ThemeFonts { 47 + body: CowStr::new_static( 48 + "IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 49 + ), 50 + heading: CowStr::new_static( 51 + "IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif", 52 + ), 53 + monospace: CowStr::new_static( 54 + "'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace", 55 + ), 56 + ..Default::default() 57 + } 58 + } 59 + 60 + pub fn default_spacing() -> ThemeSpacing<'static> { 61 + ThemeSpacing { 62 + base_size: CowStr::new_static("16px"), 63 + line_height: CowStr::new_static("1.6"), 64 + scale: CowStr::new_static("1.25"), 65 + ..Default::default() 66 + } 67 + } 68 + 69 + pub fn default_colour_scheme_light() -> ColourSchemeColours<'static> { 70 + ColourSchemeColours { 71 + base: CowStr::new_static("#faf4ed"), 72 + surface: CowStr::new_static("#fffaf3"), 73 + overlay: CowStr::new_static("#f2e9e1"), 74 + text: CowStr::new_static("#575279"), 75 + muted: CowStr::new_static("#9893a5"), 76 + subtle: CowStr::new_static("#797593"), 77 + emphasis: CowStr::new_static("#575279"), 78 + primary: CowStr::new_static("#907aa9"), 79 + secondary: CowStr::new_static("#56949f"), 80 + tertiary: CowStr::new_static("#286983"), 81 + error: CowStr::new_static("#b4637a"), 82 + warning: CowStr::new_static("#ea9d34"), 83 + success: CowStr::new_static("#286983"), 84 + border: CowStr::new_static("#dfdad9"), 85 + link: CowStr::new_static("#d7827e"), 86 + highlight: CowStr::new_static("#cecacd"), 87 + ..Default::default() 88 + } 89 + } 90 + 91 + pub fn default_resolved_theme() -> ResolvedTheme<'static> { 92 + ResolvedTheme { 93 + dark_scheme: default_colour_scheme_dark(), 94 + light_scheme: default_colour_scheme_light(), 95 + fonts: default_fonts(), 96 + spacing: default_spacing(), 97 + dark_code_theme: ThemeDarkCodeTheme::CodeThemeName(Box::new("rose-pine".to_cowstr())), 98 + light_code_theme: ThemeLightCodeTheme::CodeThemeName(Box::new( 99 + "rose-pine-dawn".to_cowstr(), 100 + )), 101 + } 102 + } 103 + 104 + /// Resolve a theme by fetching its colour scheme records from the PDS 105 + pub async fn resolve_theme<A: AgentSession + IdentityResolver>( 106 + agent: &A, 107 + theme: &Theme<'_>, 108 + ) -> miette::Result<ResolvedTheme<'static>> { 109 + use weaver_common::jacquard::client::AgentSessionExt; 110 + 111 + // Fetch dark scheme 112 + let dark_response = agent 113 + .get_record::<ColourScheme>(&theme.dark_scheme.uri) 114 + .await 115 + .into_diagnostic()?; 116 + 117 + let dark_scheme: ColourScheme = dark_response.into_output().into_diagnostic()?.into(); 118 + 119 + // Fetch light scheme 120 + let light_response = agent 121 + .get_record::<ColourScheme>(&theme.light_scheme.uri) 122 + .await 123 + .into_diagnostic()?; 124 + 125 + let light_scheme: ColourScheme = light_response.into_output().into_diagnostic()?.into(); 126 127 + Ok(ResolvedTheme { 128 + dark_scheme: dark_scheme.colours.into_static(), 129 + light_scheme: light_scheme.colours.into_static(), 130 + fonts: theme.fonts.clone().into_static(), 131 + spacing: theme.spacing.clone().into_static(), 132 + dark_code_theme: theme.dark_code_theme.clone().into_static(), 133 + light_code_theme: theme.light_code_theme.clone().into_static(), 134 + }) 135 }
+3 -2
crates/weaver-server/Cargo.toml
··· 26 weaver-renderer = { path = "../weaver-renderer" } 27 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } 28 n0-future = { workspace = true } 29 - #dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } 30 axum = {version = "0.8.6", optional = true} 31 - 32 chrono = { version = "0.4" } 33 34 [target.'cfg(target_arch = "wasm32")'.dependencies] 35 time = { version = "0.3", features = ["wasm-bindgen"] }
··· 26 weaver-renderer = { path = "../weaver-renderer" } 27 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } 28 n0-future = { workspace = true } 29 + dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } 30 axum = {version = "0.8.6", optional = true} 31 + mime-sniffer = {version = "^0.1"} 32 chrono = { version = "0.4" } 33 + serde_json = "1.0" 34 35 [target.'cfg(target_arch = "wasm32")'.dependencies] 36 time = { version = "0.3", features = ["wasm-bindgen"] }
-8
crates/weaver-server/assets/styling/blog.css
··· 1 - #blog { 2 - margin-top: 50px; 3 - } 4 - 5 - #blog a { 6 - color: #ffffff; 7 - margin-top: 50px; 8 - }
···
-34
crates/weaver-server/assets/styling/echo.css
··· 1 - #echo { 2 - width: 360px; 3 - margin-left: auto; 4 - margin-right: auto; 5 - margin-top: 50px; 6 - background-color: #1e222d; 7 - padding: 20px; 8 - border-radius: 10px; 9 - } 10 - 11 - #echo>h4 { 12 - margin: 0px 0px 15px 0px; 13 - } 14 - 15 - 16 - #echo>input { 17 - border: none; 18 - border-bottom: 1px white solid; 19 - background-color: transparent; 20 - color: #ffffff; 21 - transition: border-bottom-color 0.2s ease; 22 - outline: none; 23 - display: block; 24 - padding: 0px 0px 5px 0px; 25 - width: 100%; 26 - } 27 - 28 - #echo>input:focus { 29 - border-bottom-color: #6d85c6; 30 - } 31 - 32 - #echo>p { 33 - margin: 20px 0px 0px auto; 34 - }
···
+234
crates/weaver-server/assets/styling/entry.css
···
··· 1 + /* Entry page layout with gutter navigation */ 2 + .entry-page-layout { 3 + display: grid; 4 + grid-template-columns: minmax(0, 1fr) minmax(0, 90ch) minmax(0, 1fr); 5 + gap: 0; 6 + width: 100%; 7 + min-height: 100vh; 8 + background: var(--color-base); 9 + } 10 + 11 + /* Main content area */ 12 + .entry-content-main { 13 + grid-column: 2; 14 + background: var(--color-base); 15 + } 16 + 17 + /* Navigation gutters */ 18 + .nav-gutter { 19 + position: sticky; 20 + top: auto; 21 + bottom: calc(2rem * var(--spacing-scale, 1.5)); 22 + height: fit-content; 23 + align-self: end; 24 + } 25 + 26 + .nav-prev { 27 + grid-column: 1; 28 + padding-left: calc(1rem * var(--spacing-scale, 1.5)); 29 + } 30 + 31 + .nav-next { 32 + grid-column: 3; 33 + padding-right: calc(1rem * var(--spacing-scale, 1.5)); 34 + } 35 + 36 + /* Navigation buttons */ 37 + .nav-button { 38 + display: flex; 39 + flex-direction: column; 40 + gap: calc(0.5rem * var(--spacing-scale, 1.5)); 41 + padding: calc(1rem * var(--spacing-scale, 1.5)); 42 + background: var(--color-surface); 43 + border: 2px solid var(--color-border); 44 + border-radius: 4px; 45 + text-decoration: none; 46 + color: var(--color-text); 47 + transition: all 0.2s ease; 48 + } 49 + 50 + .nav-button:hover { 51 + background: var(--color-overlay); 52 + border-color: var(--color-primary); 53 + box-shadow: 0 2px 8px color-mix(in srgb, var(--color-primary) 20%, transparent); 54 + } 55 + 56 + .nav-button-prev { 57 + align-items: flex-start; 58 + text-align: left; 59 + } 60 + 61 + .nav-button-next { 62 + align-items: flex-end; 63 + text-align: right; 64 + } 65 + 66 + .nav-arrow { 67 + font-size: 1.5rem; 68 + font-weight: bold; 69 + color: var(--color-primary); 70 + transition: color 0.2s ease; 71 + } 72 + 73 + .nav-button:hover .nav-arrow { 74 + color: var(--color-emphasis); 75 + } 76 + 77 + .nav-label { 78 + font-size: 0.875rem; 79 + font-weight: 600; 80 + text-transform: uppercase; 81 + letter-spacing: 0.05em; 82 + color: var(--color-subtle); 83 + transition: color 0.2s ease; 84 + } 85 + 86 + .nav-button:hover .nav-label { 87 + color: var(--color-secondary); 88 + } 89 + 90 + .nav-title { 91 + font-size: 0.95rem; 92 + font-weight: 500; 93 + max-width: 20ch; 94 + overflow: hidden; 95 + text-overflow: ellipsis; 96 + white-space: nowrap; 97 + } 98 + 99 + /* Entry metadata header */ 100 + .entry-metadata { 101 + margin-bottom: calc(2rem * var(--spacing-scale, 1.5)); 102 + padding-bottom: calc(1rem * var(--spacing-scale, 1.5)); 103 + border-bottom: 2px solid var(--color-border); 104 + } 105 + 106 + .entry-title { 107 + margin-bottom: calc(1rem * var(--spacing-scale, 1.5)); 108 + margin-top: calc(1rem * var(--spacing-scale, 1.5)); 109 + color: var(--color-text); 110 + } 111 + 112 + .entry-meta-info { 113 + display: flex; 114 + flex-wrap: wrap; 115 + gap: calc(1.5rem * var(--spacing-scale, 1.5)); 116 + font-size: 0.95rem; 117 + color: var(--color-subtle); 118 + } 119 + 120 + .entry-authors, 121 + .entry-date, 122 + .entry-tags { 123 + display: flex; 124 + align-items: center; 125 + gap: 0.5rem; 126 + } 127 + 128 + .entry-date { 129 + margin-left: auto; 130 + font-weight: 400; 131 + color: var(--color-muted); 132 + } 133 + 134 + .meta-label { 135 + font-weight: 400; 136 + color: var(--color-muted); 137 + } 138 + 139 + .author-name { 140 + color: var(--color-link); 141 + font-weight: 450; 142 + text-decoration: none; 143 + transition: color 0.2s ease; 144 + } 145 + 146 + .author-name:hover { 147 + color: var(--color-emphasis); 148 + text-decoration: underline; 149 + } 150 + 151 + .entry-tags { 152 + display: flex; 153 + gap: 0.5rem; 154 + flex-wrap: wrap; 155 + } 156 + 157 + .entry-tag { 158 + padding: 0.25rem 0.75rem; 159 + background: var(--color-surface); 160 + border: 1px solid var(--color-border); 161 + border-radius: 3px; 162 + font-size: 0.85rem; 163 + color: var(--color-subtle); 164 + text-decoration: none; 165 + transition: all 0.2s ease; 166 + } 167 + 168 + .entry-tag:hover { 169 + background: var(--color-overlay); 170 + border-color: var(--color-tertiary); 171 + color: var(--color-text); 172 + } 173 + 174 + /* Content styling */ 175 + .entry-content-main ::selection { 176 + background: var(--color-highlight); 177 + color: var(--color-text); 178 + } 179 + 180 + .entry-content-main code { 181 + background: var(--color-surface); 182 + } 183 + 184 + .entry-content-main blockquote { 185 + border-left-color: var(--color-secondary); 186 + background: var(--color-surface); 187 + } 188 + 189 + .entry-content-main table th { 190 + background: var(--color-surface); 191 + border-color: var(--color-border); 192 + } 193 + 194 + .entry-content-main table td { 195 + border-color: var(--color-border); 196 + } 197 + 198 + .entry-content-main table tr:hover { 199 + background: var(--color-surface); 200 + } 201 + 202 + /* Responsive layout */ 203 + @media (max-width: 1200px) { 204 + .entry-page-layout { 205 + grid-template-columns: 1fr; 206 + gap: 0; 207 + } 208 + 209 + .entry-content-main { 210 + grid-column: 1; 211 + padding: 2rem 1rem; 212 + } 213 + 214 + .nav-gutter { 215 + position: relative; 216 + bottom: auto; 217 + grid-column: 1; 218 + padding: 0 1rem; 219 + } 220 + 221 + .nav-prev { 222 + order: 2; 223 + margin-top: 2rem; 224 + } 225 + 226 + .nav-next { 227 + order: 3; 228 + margin-top: 1rem; 229 + } 230 + 231 + .entry-content-main { 232 + order: 1; 233 + } 234 + }
+9 -7
crates/weaver-server/assets/styling/main.css
··· 1 body { 2 - background-color: #0f1116; 3 - color: #ffffff; 4 - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 5 margin: 20px; 6 } 7 ··· 17 width: 400px; 18 text-align: left; 19 font-size: x-large; 20 - color: white; 21 display: flex; 22 flex-direction: column; 23 } 24 25 #links a { 26 - color: white; 27 text-decoration: none; 28 margin-top: 20px; 29 margin: 10px 0px; 30 - border: white 1px solid; 31 border-radius: 5px; 32 padding: 10px; 33 } 34 35 #links a:hover { 36 - background-color: #1f1f1f; 37 cursor: pointer; 38 } 39
··· 1 body { 2 + background-color: var(--color-base); 3 + color: var(--color-text); 4 + font-family: var(--font-body); 5 margin: 20px; 6 } 7 ··· 17 width: 400px; 18 text-align: left; 19 font-size: x-large; 20 + color: var(--color-text); 21 display: flex; 22 flex-direction: column; 23 } 24 25 #links a { 26 + color: var(--color-link); 27 text-decoration: none; 28 margin-top: 20px; 29 margin: 10px 0px; 30 + border: 1px solid var(--color-border); 31 border-radius: 5px; 32 padding: 10px; 33 + transition: all 0.2s ease; 34 } 35 36 #links a:hover { 37 + background-color: var(--color-surface); 38 + border-color: var(--color-primary); 39 cursor: pointer; 40 } 41
+2 -2
crates/weaver-server/assets/styling/navbar.css
··· 4 } 5 6 #navbar a { 7 - color: #ffffff; 8 margin-right: 20px; 9 text-decoration: none; 10 transition: color 0.2s ease; ··· 12 13 #navbar a:hover { 14 cursor: pointer; 15 - color: #91a4d2; 16 }
··· 4 } 5 6 #navbar a { 7 + color: var(--color-text); 8 margin-right: 20px; 9 text-decoration: none; 10 transition: color 0.2s ease; ··· 12 13 #navbar a:hover { 14 cursor: pointer; 15 + color: var(--color-primary); 16 }
+52
crates/weaver-server/assets/styling/theme-defaults.css
···
··· 1 + /* Default theme variables for non-notebook pages */ 2 + /* These match the Rose Pine light/dark defaults */ 3 + 4 + /* CSS Variables - Light Mode (default) */ 5 + :root { 6 + --color-base: #faf4ed; 7 + --color-surface: #fffaf3; 8 + --color-overlay: #f2e9e1; 9 + --color-text: #575279; 10 + --color-muted: #9893a5; 11 + --color-subtle: #797593; 12 + --color-emphasis: #575279; 13 + --color-primary: #907aa9; 14 + --color-secondary: #56949f; 15 + --color-tertiary: #286983; 16 + --color-error: #b4637a; 17 + --color-warning: #ea9d34; 18 + --color-success: #286983; 19 + --color-border: #dfdad9; 20 + --color-link: #d7827e; 21 + --color-highlight: #cecacd; 22 + 23 + --font-body: IBM Plex, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 24 + --font-heading: IBM Plex Sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; 25 + --font-mono: 'IBM Plex Mono', 'Berkeley Mono', 'Cascadia Code', 'Roboto Mono', Consolas, monospace; 26 + 27 + --spacing-base: 16px; 28 + --spacing-line-height: 1.6; 29 + --spacing-scale: 1.25; 30 + } 31 + 32 + /* CSS Variables - Dark Mode */ 33 + @media (prefers-color-scheme: dark) { 34 + :root { 35 + --color-base: #191724; 36 + --color-surface: #1f1d2e; 37 + --color-overlay: #26233a; 38 + --color-text: #e0def4; 39 + --color-muted: #6e6a86; 40 + --color-subtle: #908caa; 41 + --color-emphasis: #e0def4; 42 + --color-primary: #c4a7e7; 43 + --color-secondary: #3e8fb0; 44 + --color-tertiary: #9ccfd8; 45 + --color-error: #eb6f92; 46 + --color-warning: #f6c177; 47 + --color-success: #31748f; 48 + --color-border: #403d52; 49 + --color-link: #ebbcba; 50 + --color-highlight: #524f67; 51 + } 52 + }
+8 -8
crates/weaver-server/src/components/avatar/style.css
··· 7 8 .avatar-label { 9 margin: 0; 10 - color: var(--secondary-color-4); 11 font-size: 0.875rem; 12 } 13 ··· 21 flex-shrink: 0; 22 align-items: center; 23 justify-content: center; 24 - border-radius: 3.40282e+38px; 25 - color: var(--secondary-color-4); 26 cursor: pointer; 27 font-weight: 500; 28 } ··· 58 } 59 60 .avatar[data-state="empty"] { 61 - background: var(--primary-color-2); 62 } 63 64 @keyframes pulse { ··· 81 height: 100%; 82 align-items: center; 83 justify-content: center; 84 - background: var(--primary-color); 85 - color: var(--secondary-color-4); 86 font-size: 1.5rem; 87 } 88 89 .avatar[data-state="error"] .avatar-fallback { 90 - background: var(--primary-color-3); 91 - color: var(--secondary-color-4); 92 }
··· 7 8 .avatar-label { 9 margin: 0; 10 + color: var(--color-secondary); 11 font-size: 0.875rem; 12 } 13 ··· 21 flex-shrink: 0; 22 align-items: center; 23 justify-content: center; 24 + border-radius: 3.40282e38px; 25 + color: var(--color-secondary); 26 cursor: pointer; 27 font-weight: 500; 28 } ··· 58 } 59 60 .avatar[data-state="empty"] { 61 + background: var(--color-surface); 62 } 63 64 @keyframes pulse { ··· 81 height: 100%; 82 align-items: center; 83 justify-content: center; 84 + background: var(--color-surface); 85 + color: var(--color-primary); 86 font-size: 1.5rem; 87 } 88 89 .avatar[data-state="error"] .avatar-fallback { 90 + background: var(--color-surface); 91 + color: var(--color-subtle); 92 }
+20 -14
crates/weaver-server/src/components/css.rs
··· 15 #[allow(unused_imports)] 16 use std::sync::Arc; 17 #[allow(unused_imports)] 18 - use weaver_renderer::theme::Theme; 19 20 #[cfg(feature = "server")] 21 use axum::{extract::Extension, response::IntoResponse}; ··· 37 38 use weaver_api::sh_weaver::notebook::book::Book; 39 use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 40 - use weaver_renderer::theme::default_theme; 41 42 let ident = AtIdentifier::new_owned(ident)?; 43 - let theme = if let Some(notebook) = fetcher.get_notebook(ident, notebook).await? { 44 let book: Book = from_data(&notebook.0.record).unwrap(); 45 - if let Some(theme) = book.theme { 46 - if let Ok(theme) = fetcher.client.get_record::<Theme>(&theme.uri).await { 47 - theme 48 - .into_output() 49 - .map(|t| t.value) 50 - .unwrap_or(default_theme()) 51 } else { 52 - default_theme() 53 } 54 } else { 55 - default_theme() 56 } 57 } else { 58 - default_theme() 59 }; 60 - let mut css = generate_base_css(&theme); 61 css.push_str( 62 - &generate_syntax_css(&theme) 63 .await 64 .map_err(|e| CapturedError::from_display(e)) 65 .unwrap_or_default(),
··· 15 #[allow(unused_imports)] 16 use std::sync::Arc; 17 #[allow(unused_imports)] 18 + use weaver_renderer::theme::{Theme, ResolvedTheme}; 19 20 #[cfg(feature = "server")] 21 use axum::{extract::Extension, response::IntoResponse}; ··· 37 38 use weaver_api::sh_weaver::notebook::book::Book; 39 use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 40 + use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; 41 42 let ident = AtIdentifier::new_owned(ident)?; 43 + let resolved_theme = if let Some(notebook) = fetcher.get_notebook(ident, notebook).await? { 44 let book: Book = from_data(&notebook.0.record).unwrap(); 45 + if let Some(theme_ref) = book.theme { 46 + if let Ok(theme_response) = fetcher.client.get_record::<Theme>(&theme_ref.uri).await { 47 + if let Ok(theme_output) = theme_response.into_output() { 48 + let theme: Theme = theme_output.into(); 49 + // Try to resolve the theme (fetch colour schemes from PDS) 50 + resolve_theme(fetcher.client.as_ref(), &theme) 51 + .await 52 + .unwrap_or_else(|_| default_resolved_theme()) 53 + } else { 54 + default_resolved_theme() 55 + } 56 } else { 57 + default_resolved_theme() 58 } 59 } else { 60 + default_resolved_theme() 61 } 62 } else { 63 + default_resolved_theme() 64 }; 65 + 66 + let mut css = generate_base_css(&resolved_theme); 67 css.push_str( 68 + &generate_syntax_css(&resolved_theme) 69 .await 70 .map_err(|e| CapturedError::from_display(e)) 71 .unwrap_or_default(),
+208 -6
crates/weaver-server/src/components/entry.rs
··· 2 3 #[cfg(feature = "server")] 4 use crate::blobcache::BlobCache; 5 - use crate::fetch; 6 use dioxus::prelude::*; 7 #[allow(unused_imports)] 8 use dioxus::{fullstack::extract::Extension, CapturedError}; 9 - use jacquard::{prelude::IdentityResolver, smol_str::ToSmolStr}; 10 #[allow(unused_imports)] 11 use jacquard::{ 12 smol_str::SmolStr, ··· 19 #[component] 20 pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) -> Element { 21 let ident_clone = ident.clone(); 22 let entry = use_resource(use_reactive!(|(ident, book_title, title)| async move { 23 let fetcher = use_context::<fetch::CachedFetcher>(); 24 let entry = fetcher ··· 48 49 match &*entry.read_unchecked() { 50 Some(Some(entry_data)) => { 51 - rsx! { EntryMarkdownDirect { 52 - content: entry_data.1.clone(), 53 - ident: ident_clone 54 } } 55 } 56 Some(None) => { ··· 60 } 61 } 62 63 #[component] 64 pub fn EntryCard(entry: BookEntryView<'static>) -> Element { 65 rsx! {} 66 } 67 68 #[derive(Props, Clone, PartialEq)] 69 pub struct EntryMarkdownProps { 70 #[props(default)] ··· 124 125 // Render to HTML 126 let mut html_buf = String::new(); 127 - let _ = ClientWriter::<_, ()>::new(&mut html_buf).run(events.into_iter()); 128 Some(html_buf) 129 })); 130
··· 2 3 #[cfg(feature = "server")] 4 use crate::blobcache::BlobCache; 5 + use crate::{ 6 + components::avatar::{Avatar, AvatarFallback, AvatarImage}, 7 + fetch, 8 + }; 9 use dioxus::prelude::*; 10 + 11 + const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); 12 #[allow(unused_imports)] 13 use dioxus::{fullstack::extract::Extension, CapturedError}; 14 + use jacquard::{ 15 + from_data, prelude::IdentityResolver, smol_str::ToSmolStr, types::string::Datetime, 16 + }; 17 #[allow(unused_imports)] 18 use jacquard::{ 19 smol_str::SmolStr, ··· 26 #[component] 27 pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) -> Element { 28 let ident_clone = ident.clone(); 29 + let book_title_clone = book_title.clone(); 30 let entry = use_resource(use_reactive!(|(ident, book_title, title)| async move { 31 let fetcher = use_context::<fetch::CachedFetcher>(); 32 let entry = fetcher ··· 56 57 match &*entry.read_unchecked() { 58 Some(Some(entry_data)) => { 59 + rsx! { EntryPage { 60 + book_entry_view: entry_data.0.clone(), 61 + entry_record: entry_data.1.clone(), 62 + ident: ident_clone, 63 + book_title: book_title_clone 64 } } 65 } 66 Some(None) => { ··· 70 } 71 } 72 73 + /// Full entry page with metadata, content, and navigation 74 + #[component] 75 + fn EntryPage( 76 + book_entry_view: BookEntryView<'static>, 77 + entry_record: entry::Entry<'static>, 78 + ident: AtIdentifier<'static>, 79 + book_title: SmolStr, 80 + ) -> Element { 81 + // Extract metadata 82 + let entry_view = &book_entry_view.entry; 83 + let title = entry_view 84 + .title 85 + .as_ref() 86 + .map(|t| t.as_ref()) 87 + .unwrap_or("Untitled"); 88 + 89 + rsx! { 90 + // Set page title 91 + document::Title { "{title}" } 92 + document::Link { rel: "stylesheet", href: ENTRY_CSS } 93 + 94 + div { class: "entry-page-layout", 95 + // Left gutter with prev button 96 + if let Some(ref prev) = book_entry_view.prev { 97 + div { class: "nav-gutter nav-prev", 98 + NavButton { 99 + direction: "prev", 100 + entry: prev.entry.clone(), 101 + ident: ident.clone(), 102 + book_title: book_title.clone() 103 + } 104 + } 105 + } 106 + 107 + // Main content area 108 + div { class: "entry-content-main", 109 + // Metadata header 110 + EntryMetadata { 111 + entry_view: entry_view.clone(), 112 + ident: ident.clone(), 113 + created_at: entry_record.created_at.clone() 114 + } 115 + 116 + // Rendered markdown 117 + EntryMarkdownDirect { 118 + content: entry_record, 119 + ident: ident.clone() 120 + } 121 + } 122 + 123 + // Right gutter with next button 124 + if let Some(ref next) = book_entry_view.next { 125 + div { class: "nav-gutter nav-next", 126 + NavButton { 127 + direction: "next", 128 + entry: next.entry.clone(), 129 + ident: ident.clone(), 130 + book_title: book_title.clone() 131 + } 132 + } 133 + } 134 + } 135 + } 136 + } 137 + 138 #[component] 139 pub fn EntryCard(entry: BookEntryView<'static>) -> Element { 140 rsx! {} 141 } 142 143 + /// Metadata header showing title, authors, date, tags 144 + #[component] 145 + fn EntryMetadata( 146 + entry_view: weaver_api::sh_weaver::notebook::EntryView<'static>, 147 + ident: AtIdentifier<'static>, 148 + created_at: Datetime, 149 + ) -> Element { 150 + use weaver_api::app_bsky::actor::profile::Profile; 151 + 152 + let title = entry_view 153 + .title 154 + .as_ref() 155 + .map(|t| t.as_ref()) 156 + .unwrap_or("Untitled"); 157 + 158 + let indexed_at_chrono = entry_view.indexed_at.as_ref(); 159 + 160 + rsx! { 161 + header { class: "entry-metadata", 162 + h1 { class: "entry-title", "{title}" } 163 + 164 + div { class: "entry-meta-info", 165 + // Authors 166 + if !entry_view.authors.is_empty() { 167 + div { class: "entry-authors", 168 + for (i, author) in entry_view.authors.iter().enumerate() { 169 + if i > 0 { span { ", " } } 170 + { 171 + // Parse author profile from the nested value field 172 + match from_data::<Profile>(author.record.get_at_path(".value").unwrap()) { 173 + Ok(profile) => { 174 + let avatar = profile.avatar 175 + .map(|avatar| { 176 + let cid = avatar.blob().cid(); 177 + let did = entry_view.uri.authority(); 178 + format!("https://cdn.bsky.app/img/avatar/plain/{did}/{cid}@jpeg") 179 + }); 180 + let display_name = profile.display_name 181 + .as_ref() 182 + .map(|n| n.as_ref()) 183 + .unwrap_or("Unknown"); 184 + rsx! { 185 + if let Some(avatar) = avatar { 186 + Avatar { 187 + AvatarImage { 188 + src: avatar 189 + } 190 + } 191 + } 192 + span { class: "author-name", "{display_name}" } 193 + span { class: "meta-label", "@{ident}" } 194 + } 195 + } 196 + Err(_) => { 197 + rsx! { 198 + span { class: "author-name", "Author {author.index}" } 199 + } 200 + } 201 + } 202 + } 203 + } 204 + } 205 + } 206 + 207 + // Date 208 + div { class: "entry-date", 209 + { 210 + let formatted_date = created_at.as_ref().format("%B %d, %Y").to_string(); 211 + 212 + rsx! { 213 + time { datetime: "{entry_view.indexed_at.as_str()}", "{formatted_date}" } 214 + 215 + } 216 + } 217 + } 218 + 219 + // Tags 220 + if let Some(ref tags) = entry_view.tags { 221 + div { class: "entry-tags", 222 + // TODO: Parse tags structure 223 + span { class: "meta-label", "Tags: " } 224 + span { "[tags]" } 225 + } 226 + } 227 + } 228 + } 229 + } 230 + } 231 + 232 + /// Navigation button for prev/next entries 233 + #[component] 234 + fn NavButton( 235 + direction: &'static str, 236 + entry: weaver_api::sh_weaver::notebook::EntryView<'static>, 237 + ident: AtIdentifier<'static>, 238 + book_title: SmolStr, 239 + ) -> Element { 240 + use crate::Route; 241 + 242 + let entry_title = entry 243 + .title 244 + .as_ref() 245 + .map(|t| t.as_ref()) 246 + .unwrap_or("Untitled"); 247 + 248 + let label = if direction == "prev" { 249 + "← Previous" 250 + } else { 251 + "Next →" 252 + }; 253 + let arrow = if direction == "prev" { "←" } else { "→" }; 254 + 255 + rsx! { 256 + Link { 257 + to: Route::Entry { 258 + ident: ident.clone(), 259 + book_title: book_title.clone(), 260 + title: entry_title.to_string().into() 261 + }, 262 + class: "nav-button nav-button-{direction}", 263 + div { class: "nav-arrow", "{arrow}" } 264 + div { class: "nav-label", "{label}" } 265 + div { class: "nav-title", "{entry_title}" } 266 + } 267 + } 268 + } 269 + 270 #[derive(Props, Clone, PartialEq)] 271 pub struct EntryMarkdownProps { 272 #[props(default)] ··· 326 327 // Render to HTML 328 let mut html_buf = String::new(); 329 + let _ = ClientWriter::<_, _, ()>::new(events.into_iter(), &mut html_buf).run(); 330 Some(html_buf) 331 })); 332
+1 -1
crates/weaver-server/src/components/mod.rs
··· 10 11 mod identity; 12 pub use identity::{Repository, RepositoryIndex}; 13 - //pub mod avatar;
··· 10 11 mod identity; 12 pub use identity::{Repository, RepositoryIndex}; 13 + pub mod avatar;
+51 -2
crates/weaver-server/src/main.rs
··· 1 // The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you 2 // need dioxus 3 use components::{Entry, Repository, RepositoryIndex}; 4 - use dioxus::{fullstack::FullstackContext, prelude::*}; 5 - use jacquard::{client::BasicClient, smol_str::SmolStr, types::string::AtIdentifier}; 6 use std::sync::Arc; 7 use views::{Home, Navbar, Notebook, NotebookIndex, NotebookPage}; 8 9 #[cfg(feature = "server")] ··· 135 } 136 } 137 }
··· 1 // The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you 2 // need dioxus 3 use components::{Entry, Repository, RepositoryIndex}; 4 + #[allow(unused)] 5 + use dioxus::{ 6 + fullstack::{response::Extension, FullstackContext}, 7 + prelude::*, 8 + CapturedError, 9 + }; 10 + #[allow(unused)] 11 + use jacquard::{ 12 + client::BasicClient, 13 + smol_str::SmolStr, 14 + types::{cid::Cid, string::AtIdentifier}, 15 + }; 16 + 17 use std::sync::Arc; 18 + #[allow(unused)] 19 use views::{Home, Navbar, Notebook, NotebookIndex, NotebookPage}; 20 21 #[cfg(feature = "server")] ··· 147 } 148 } 149 } 150 + 151 + #[get("/{notebook}/image/{name}", blob_cache: Extension<Arc<crate::blobcache::BlobCache>>)] 152 + pub async fn image_named( 153 + notebook: SmolStr, 154 + name: SmolStr, 155 + ) -> Result<dioxus_fullstack::response::Response> { 156 + use axum::response::IntoResponse; 157 + use mime_sniffer::MimeTypeSniffer; 158 + if let Some(bytes) = blob_cache.get_named(&name) { 159 + let blob = bytes.clone(); 160 + let mime = blob.sniff_mime_type().unwrap_or("application/octet-stream"); 161 + Ok(( 162 + [(dioxus_fullstack::http::header::CONTENT_TYPE, mime)], 163 + bytes, 164 + ) 165 + .into_response()) 166 + } else { 167 + Err(CapturedError::from_display("no image")) 168 + } 169 + } 170 + 171 + #[get("/{notebook}/blob/{cid}", blob_cache: Extension<Arc<crate::blobcache::BlobCache>>)] 172 + pub async fn blob(notebook: SmolStr, cid: SmolStr) -> Result<dioxus_fullstack::response::Response> { 173 + use axum::response::IntoResponse; 174 + use mime_sniffer::MimeTypeSniffer; 175 + if let Some(bytes) = blob_cache.get_cid(&Cid::new_owned(cid.as_bytes())?) { 176 + let blob = bytes.clone(); 177 + let mime = blob.sniff_mime_type().unwrap_or("application/octet-stream"); 178 + Ok(( 179 + [(dioxus_fullstack::http::header::CONTENT_TYPE, mime)], 180 + bytes, 181 + ) 182 + .into_response()) 183 + } else { 184 + Err(CapturedError::from_display("no blob")) 185 + } 186 + }
+2
crates/weaver-server/src/views/navbar.rs
··· 1 use crate::Route; 2 use dioxus::prelude::*; 3 4 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); 5 6 /// The Navbar component that will be rendered on all pages of our app since every page is under the layout. ··· 11 #[component] 12 pub fn Navbar() -> Element { 13 rsx! { 14 document::Link { rel: "stylesheet", href: NAVBAR_CSS } 15 16 div {
··· 1 use crate::Route; 2 use dioxus::prelude::*; 3 4 + const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); 5 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); 6 7 /// The Navbar component that will be rendered on all pages of our app since every page is under the layout. ··· 12 #[component] 13 pub fn Navbar() -> Element { 14 rsx! { 15 + document::Link { rel: "stylesheet", href: THEME_DEFAULTS_CSS } 16 document::Link { rel: "stylesheet", href: NAVBAR_CSS } 17 18 div {
+99
docs/plans/2025-11-03-theme-colour-scheme-redesign.md
···
··· 1 + # Theme and Colour Scheme Redesign 2 + 3 + **Date:** 2025-11-03 4 + **Status:** Design approved, ready for implementation 5 + 6 + ## Overview 7 + 8 + Redesign the theme lexicon to support expressive colour schemes comparable to base16 and Rose Pine, while maintaining flexibility for both preset themes and power user customization. 9 + 10 + ## Requirements 11 + 12 + - Support 16+ semantic colour slots (comparable to base16 expressiveness) 13 + - Enable importing/adapting popular themes (base16, Rose Pine, etc.) 14 + - Separate light/dark modes as distinct themes (users can mix and match) 15 + - Semantic naming over positional identifiers (readable, consistent across themes) 16 + - Support preset picker UI + power user manual customization 17 + 18 + ## Design 19 + 20 + ### Two-Lexicon Structure 21 + 22 + Split colour schemes from themes to enable reusability and mixing: 23 + 24 + **1. `sh.weaver.notebook.colourScheme`** - Standalone colour palette record 25 + - Standalone AT Protocol record 26 + - Contains name, variant (dark/light), and 16 colour slots 27 + - Can be published and referenced by multiple themes 28 + - Enables sharing palettes between users 29 + 30 + **2. `sh.weaver.notebook.theme`** - Complete theme with colour references 31 + - References two colourScheme records (dark and light) via strongRefs 32 + - Contains fonts, spacing, codeTheme (unchanged from previous design) 33 + - Users can point to any published colour schemes, including others' palettes 34 + 35 + ### 16 Semantic Colour Slots 36 + 37 + Organized into 5 semantic categories with consistent naming: 38 + 39 + **Backgrounds (3):** 40 + - `base` - Primary background for page/frame 41 + - `surface` - Secondary background for panels/cards 42 + - `overlay` - Tertiary background for popovers/dialogs 43 + 44 + **Text (1):** 45 + - `text` - Primary readable text colour (baseline) 46 + 47 + **Foreground variations (3):** 48 + - `muted` - De-emphasized text (disabled, metadata) 49 + - `subtle` - Medium emphasis text (comments, labels) 50 + - `emphasis` - Emphasized text (bold, important) 51 + 52 + **Accents (3):** 53 + - `primary` - Primary brand/accent colour 54 + - `secondary` - Secondary accent colour 55 + - `tertiary` - Tertiary accent colour 56 + 57 + **Status (3):** 58 + - `error` - Error state colour 59 + - `warning` - Warning state colour 60 + - `success` - Success state colour 61 + 62 + **Role (3):** 63 + - `border` - Border/divider colour 64 + - `link` - Hyperlink colour 65 + - `highlight` - Selection/highlight colour 66 + 67 + ### Mapping to Existing Schemes 68 + 69 + **Base16 compatibility:** 70 + - Backgrounds map to base00-base02 71 + - Foregrounds map to base03-base07 72 + - Accents/status/roles map to base08-base0F 73 + 74 + **Rose Pine compatibility:** 75 + - Direct semantic mapping (base→base, surface→surface, overlay→overlay) 76 + - text/muted/subtle map to text/muted/subtle 77 + - Accent colours map to love/gold/rose/pine/foam/iris 78 + 79 + ## Implementation Impact 80 + 81 + ### Files to Create 82 + - `lexicons/notebook/colourScheme.json` ✓ (created) 83 + 84 + ### Files to Modify 85 + - `lexicons/notebook/theme.json` ✓ (modified) 86 + - Run `./lexicon-codegen.sh` to regenerate Rust types 87 + - Update `crates/weaver-common/src/lexicons/mod.rs` (remove duplicate `mod sh;`) 88 + 89 + ### Code Changes Needed 90 + - Update theme rendering code to use strongRef lookups 91 + - Update CSS generation to use new 16-slot colour names 92 + - Create default colour schemes (at least one dark, one light) 93 + - Update any existing theme records/configs to new format 94 + 95 + ### Future Enhancements 96 + - Theme picker UI with preset browser 97 + - Colour scheme validation/linting 98 + - Auto-generate light from dark (and vice versa) where appropriate 99 + - Import converters for base16 YAML, Rose Pine JSON
-20
lexicon-codegen.sh
··· 1 - #!/usr/bin/env bash 2 - 3 - 4 - #cargo install esquema-cli --locked --git https://github.com/fatfingers23/esquema.git 5 - 6 - rm -rf ./target/lexicons 7 - mkdir -p ./target/lexicons 8 - cd target 9 - git clone -n --depth=1 --filter=tree:0 \ 10 - https://github.com/bluesky-social/atproto.git 11 - cd atproto 12 - git sparse-checkout set --no-cone /lexicons 13 - git checkout 14 - cd .. 15 - 16 - cp -r ../lexicons ./ 17 - cp -r ./atproto/lexicons ./ 18 - 19 - 20 - ~/.cargo/bin/esquema-cli generate local --lexdir ./lexicons/ --outdir ../crates/weaver-common/src/ --module lexicons
···
+102
lexicons/notebook/colourScheme.json
···
··· 1 + { 2 + "lexicon": 1, 3 + "id": "sh.weaver.notebook.colourScheme", 4 + "defs": { 5 + "main": { 6 + "type": "record", 7 + "description": "A colour palette for notebook theming", 8 + "key": "tid", 9 + "record": { 10 + "type": "object", 11 + "required": ["name", "variant", "colours"], 12 + "properties": { 13 + "name": { 14 + "type": "string", 15 + "description": "Human-readable name for the colour scheme" 16 + }, 17 + "variant": { 18 + "type": "string", 19 + "enum": ["dark", "light"], 20 + "description": "Whether this is a dark or light colour scheme" 21 + }, 22 + "colours": { 23 + "type": "object", 24 + "required": [ 25 + "base", "surface", "overlay", 26 + "text", "muted", "subtle", "emphasis", 27 + "primary", "secondary", "tertiary", 28 + "error", "warning", "success", 29 + "border", "link", "highlight" 30 + ], 31 + "properties": { 32 + "base": { 33 + "type": "string", 34 + "description": "Primary background for page/frame" 35 + }, 36 + "surface": { 37 + "type": "string", 38 + "description": "Secondary background for panels/cards" 39 + }, 40 + "overlay": { 41 + "type": "string", 42 + "description": "Tertiary background for popovers/dialogs" 43 + }, 44 + "text": { 45 + "type": "string", 46 + "description": "Primary readable text colour" 47 + }, 48 + "muted": { 49 + "type": "string", 50 + "description": "De-emphasized text (disabled, metadata)" 51 + }, 52 + "subtle": { 53 + "type": "string", 54 + "description": "Medium emphasis text (comments, labels)" 55 + }, 56 + "emphasis": { 57 + "type": "string", 58 + "description": "Emphasized text (bold, important)" 59 + }, 60 + "primary": { 61 + "type": "string", 62 + "description": "Primary brand/accent colour" 63 + }, 64 + "secondary": { 65 + "type": "string", 66 + "description": "Secondary accent colour" 67 + }, 68 + "tertiary": { 69 + "type": "string", 70 + "description": "Tertiary accent colour" 71 + }, 72 + "error": { 73 + "type": "string", 74 + "description": "Error state colour" 75 + }, 76 + "warning": { 77 + "type": "string", 78 + "description": "Warning state colour" 79 + }, 80 + "success": { 81 + "type": "string", 82 + "description": "Success state colour" 83 + }, 84 + "border": { 85 + "type": "string", 86 + "description": "Border/divider colour" 87 + }, 88 + "link": { 89 + "type": "string", 90 + "description": "Hyperlink colour" 91 + }, 92 + "highlight": { 93 + "type": "string", 94 + "description": "Selection/highlight colour" 95 + } 96 + } 97 + } 98 + } 99 + } 100 + } 101 + } 102 + }
+18 -26
lexicons/notebook/theme.json
··· 8 "key": "tid", 9 "record": { 10 "type": "object", 11 - "required": ["colours", "fonts", "spacing", "codeTheme"], 12 "properties": { 13 - "colours": { 14 - "type": "object", 15 - "required": ["background", "foreground", "primary", "secondary", "link", "link_hover"], 16 - "properties": { 17 - "background": { 18 - "type": "string" 19 - }, 20 - "foreground": { 21 - "type": "string" 22 - }, 23 - "primary": { 24 - "type": "string" 25 - }, 26 - "secondary": { 27 - "type": "string" 28 - }, 29 - "link": { 30 - "type": "string" 31 - }, 32 - "link_hover": { 33 - "type": "string" 34 - } 35 - } 36 }, 37 "fonts": { 38 "type": "object", ··· 64 } 65 } 66 }, 67 - "codeTheme": { 68 "type": "union", 69 - "refs": ["#codeThemeName", "#codeThemeFile"] 70 } 71 } 72 }
··· 8 "key": "tid", 9 "record": { 10 "type": "object", 11 + "required": ["darkScheme", "lightScheme", "fonts", "spacing", "darkCodeTheme", "lightCodeTheme"], 12 "properties": { 13 + "darkScheme": { 14 + "type": "ref", 15 + "ref": "com.atproto.repo.strongRef", 16 + "description": "Reference to a dark colour scheme" 17 + }, 18 + "lightScheme": { 19 + "type": "ref", 20 + "ref": "com.atproto.repo.strongRef", 21 + "description": "Reference to a light colour scheme" 22 }, 23 "fonts": { 24 "type": "object", ··· 50 } 51 } 52 }, 53 + "darkCodeTheme": { 54 "type": "union", 55 + "refs": ["#codeThemeName", "#codeThemeFile"], 56 + "description": "Syntax highlighting theme for dark mode" 57 + }, 58 + "lightCodeTheme": { 59 + "type": "union", 60 + "refs": ["#codeThemeName", "#codeThemeFile"], 61 + "description": "Syntax highlighting theme for light mode" 62 } 63 } 64 }
+1
weaver_notes/.obsidian/app.json
···
··· 1 + {}
+1
weaver_notes/.obsidian/appearance.json
···
··· 1 + {}
+33
weaver_notes/.obsidian/core-plugins.json
···
··· 1 + { 2 + "file-explorer": true, 3 + "global-search": true, 4 + "switcher": true, 5 + "graph": true, 6 + "backlink": true, 7 + "canvas": true, 8 + "outgoing-link": true, 9 + "tag-pane": true, 10 + "footnotes": false, 11 + "properties": false, 12 + "page-preview": true, 13 + "daily-notes": true, 14 + "templates": true, 15 + "note-composer": true, 16 + "command-palette": true, 17 + "slash-command": false, 18 + "editor-status": true, 19 + "bookmarks": true, 20 + "markdown-importer": false, 21 + "zk-prefixer": false, 22 + "random-note": false, 23 + "outline": true, 24 + "word-count": true, 25 + "slides": false, 26 + "audio-recorder": false, 27 + "workspaces": false, 28 + "file-recovery": true, 29 + "publish": false, 30 + "sync": true, 31 + "bases": true, 32 + "webviewer": false 33 + }
+176
weaver_notes/.obsidian/workspace.json
···
··· 1 + { 2 + "main": { 3 + "id": "9e811362058e2cfe", 4 + "type": "split", 5 + "children": [ 6 + { 7 + "id": "38872499ca515395", 8 + "type": "tabs", 9 + "children": [ 10 + { 11 + "id": "f41bdf1f327c8668", 12 + "type": "leaf", 13 + "state": { 14 + "type": "markdown", 15 + "state": { 16 + "file": "Long-form writing.md", 17 + "mode": "source", 18 + "source": false 19 + }, 20 + "icon": "lucide-file", 21 + "title": "Long-form writing" 22 + } 23 + } 24 + ] 25 + } 26 + ], 27 + "direction": "vertical" 28 + }, 29 + "left": { 30 + "id": "d19acb107ca6c5a8", 31 + "type": "split", 32 + "children": [ 33 + { 34 + "id": "deab36e0c8ac1697", 35 + "type": "tabs", 36 + "children": [ 37 + { 38 + "id": "2f6bdb6d2dce13ed", 39 + "type": "leaf", 40 + "state": { 41 + "type": "file-explorer", 42 + "state": { 43 + "sortOrder": "alphabetical", 44 + "autoReveal": false 45 + }, 46 + "icon": "lucide-folder-closed", 47 + "title": "Files" 48 + } 49 + }, 50 + { 51 + "id": "b2bacecc9ad2b76f", 52 + "type": "leaf", 53 + "state": { 54 + "type": "search", 55 + "state": { 56 + "query": "", 57 + "matchingCase": false, 58 + "explainSearch": false, 59 + "collapseAll": false, 60 + "extraContext": false, 61 + "sortOrder": "alphabetical" 62 + }, 63 + "icon": "lucide-search", 64 + "title": "Search" 65 + } 66 + }, 67 + { 68 + "id": "bfa8d6752e6e8a65", 69 + "type": "leaf", 70 + "state": { 71 + "type": "bookmarks", 72 + "state": {}, 73 + "icon": "lucide-bookmark", 74 + "title": "Bookmarks" 75 + } 76 + } 77 + ] 78 + } 79 + ], 80 + "direction": "horizontal", 81 + "width": 300 82 + }, 83 + "right": { 84 + "id": "e2be0bd6d264a319", 85 + "type": "split", 86 + "children": [ 87 + { 88 + "id": "9dbec3dc54c2e087", 89 + "type": "tabs", 90 + "children": [ 91 + { 92 + "id": "c090edea1d5304fa", 93 + "type": "leaf", 94 + "state": { 95 + "type": "backlink", 96 + "state": { 97 + "file": "Long-form writing.md", 98 + "collapseAll": false, 99 + "extraContext": false, 100 + "sortOrder": "alphabetical", 101 + "showSearch": false, 102 + "searchQuery": "", 103 + "backlinkCollapsed": false, 104 + "unlinkedCollapsed": true 105 + }, 106 + "icon": "links-coming-in", 107 + "title": "Backlinks for Long-form writing" 108 + } 109 + }, 110 + { 111 + "id": "b6d0494de7d9f798", 112 + "type": "leaf", 113 + "state": { 114 + "type": "outgoing-link", 115 + "state": { 116 + "file": "Long-form writing.md", 117 + "linksCollapsed": false, 118 + "unlinkedCollapsed": true 119 + }, 120 + "icon": "links-going-out", 121 + "title": "Outgoing links from Long-form writing" 122 + } 123 + }, 124 + { 125 + "id": "634ad4a6ff1cb685", 126 + "type": "leaf", 127 + "state": { 128 + "type": "tag", 129 + "state": { 130 + "sortOrder": "frequency", 131 + "useHierarchy": true, 132 + "showSearch": false, 133 + "searchQuery": "" 134 + }, 135 + "icon": "lucide-tags", 136 + "title": "Tags" 137 + } 138 + }, 139 + { 140 + "id": "16dc52d50e7d2218", 141 + "type": "leaf", 142 + "state": { 143 + "type": "outline", 144 + "state": { 145 + "file": "Long-form writing.md", 146 + "followCursor": false, 147 + "showSearch": false, 148 + "searchQuery": "" 149 + }, 150 + "icon": "lucide-list", 151 + "title": "Outline of Long-form writing" 152 + } 153 + } 154 + ] 155 + } 156 + ], 157 + "direction": "horizontal", 158 + "width": 300, 159 + "collapsed": true 160 + }, 161 + "left-ribbon": { 162 + "hiddenItems": { 163 + "switcher:Open quick switcher": false, 164 + "graph:Open graph view": false, 165 + "canvas:Create new canvas": false, 166 + "daily-notes:Open today's daily note": false, 167 + "templates:Insert template": false, 168 + "command-palette:Open command palette": false, 169 + "bases:Create new base": false 170 + } 171 + }, 172 + "active": "f41bdf1f327c8668", 173 + "lastOpenFiles": [ 174 + "Long-form writing.md" 175 + ] 176 + }
+12
weaver_notes/Long-form writing.md
···
··· 1 + I grew up, like a lot of people on Bluesky, in the era of the internet where most of your online social interactions took place via text. I had a MySpace account, MSN messenger and Google Chat, I first got on Facebook back when they required a school email to sign up, I had a Tumblr, though not a LiveJournal. I was super into reddit for a long time. Big fan of Fanfiction.net and later Archive of Our Own. 2 + 3 + Social media in the conventional sense has been in a lot of ways a small part of the story of my time on the internet. Because while I wasn't huge into independent internet forums, the broader independent blogosphere of my teens and early adulthood shaped my worldview, and I was an avid reader and sometime participant there. I am an atheist in large part because of a blog called [Common Sense Atheism](http://commonsenseatheism.com) (which I started reading in part because the author, Luke Muehlhauser, was criticising both Richard Dawkins and some Christian apologetics I was familiar with). Luke's blog was part of cluster of blogs out of which grew the [rationalists](https://www.lesswrong.com/), one of, for better or for worse, the most influential intellectual movements of the 21st century, who are, via people like [Scott ](https://slatestarcodex.com/) [Alexander](https://www.astralcodexten.com/), both downstream and upstream of the tech billionaire ideology. I also read blogs like [boingboing.net](https://boingboing.net/), was a big fan of Cory Doctorow. I figured out I am trans in part because of [Thing of Things](https://thingofthings.wordpress.com/2017/05/05/the-cluster-structure-of-genderspace/), a blog by Ozy Frantz, a transmasc person in the broader rationalist and Effective Altruist blogosphere. One thing these all have in common is length. Part of the reason I only really got onto Twitter in 2020 or so was because the concept of microblogging, of having to fit your thoughts into such a small package, baffled me for ages. Amusingly I now think that being on Twitter and now Bluesky made me a better writer. [Restrictions breed creativity](https://articles.starcitygames.com/articles/restrictions-breed-creativity/), after all. 4 + 5 + But through all of this I was never really satisfied with the options that were out there for long-form writing. Wordpress, even their hosted version, required a lot of setup to really be functional, Tumblr's system for comment/replies was and remains insane, hosting my own seemed like too much money to burn on something nobody might even read at the time, and honestly I felt like I kinda missed the boat on discoverability, as the internet grew larger and more centralised, with platforms like Substack eating what was left of the blogosphere. But at the same time, its success proves that there is very much a desire for long-form writing, enough that people will pay for it, and that investors will back it. There are thoughts and forms of writing that you simply cannot fit into a post or even a thread of posts, and which don't make sense on a topic-based forum, or a place like Archive of our Own. Plus, I'm loathe to enable a centralised platform like Substack where the owners are unfortunately friendly to fascists. 6 + 7 + That's where the `at://` protocol and Weaver comes in. 8 + ### The pitch 9 + Weaver is designed to be a highly flexible platform for medium and long-form writing on atproto. I was inspired by how weaver birds build their own homes, and by the notebooks, physical and virtual, that I create in the course of my work, to ideate, to document, and to inform. The initial proof-of-concept is essentially a static site generator, able to turn a Markdown text file or a folder of Markdown files, such as an Obsidian vault or git repository documentation, into a static "notebook" site. The file is uploaded to your PDS, where it can be accessed, either directly, via a minimal app server layer that provides a friendlier web address than an XRPC request or CDN link, or hosted on a platform of your choice, be that your own server or any other static site hosting service. The intermediate goal is an elegant and intuitive writing platform with collaborative editing and straightforward, immediate publishing via a web-app. The ultimate goal is to build a platform suitable for professional writers and journalists, an open alternative to platforms like Substack, with ways for readers to support writers, all on the AT protocol. 10 + 11 + ### So what about... 12 + When I started working on Weaver back in the spring, the only real games in town for long-form blogging based on atproto, aside from rolling your own, [piss.beauty](https://piss.beauty) style, were [whtwnd.com](https://whtwnd.com/) and [leaflet.pub](https://leaflet.pub/home). Leaflet's good, and it's gotten a lot better in the last year, but it doesn't offer quite what I'm looking for either. For one, I am a Markdown fangirl, for better or for worse, and while Leaflet allows you to use Markdown for formatting, it doesn't speak it natively. There are [more alternatives now](https://connectedplaces.leaflet.pub/3m4qgpc7h3223), which makes sense as this space definitely feels like one that has gaps to fill. And the `at://` protocol, while it was developed in concert with a microblogging app, is actually pretty damn good for "macro"blogging, too. The interoperability the protocol allows is incredible. Weaver's app server can display Whitewind posts very easily. With some effort on my part, it can faithfully render Leaflet posts. It doesn't care what app your profile is on, it uses the [partial understanding](https://sdr-podcast.com/episodes/partial-understanding/) [capabilities](https://bsky.app/profile/nonbinary.computer/post/3m44ooo42xc2j) of the [jacquard](https://tangled.org/@nonbinary.computer/jacquard/) library I created to pull useful data out of it.