new theming scheme, better css, blob cdn endpoints

Orual f219fec8 dd28dcc0

+2928 -505
+38 -5
Cargo.lock
··· 1724 1724 "global-hotkey", 1725 1725 "infer", 1726 1726 "jni", 1727 - "lazy-js-bundle", 1727 + "lazy-js-bundle 0.7.0", 1728 1728 "libc", 1729 1729 "muda", 1730 1730 "ndk", ··· 1796 1796 "futures-channel", 1797 1797 "futures-util", 1798 1798 "generational-box", 1799 - "lazy-js-bundle", 1799 + "lazy-js-bundle 0.7.0", 1800 1800 "serde", 1801 1801 "serde_json", 1802 1802 "tracing", ··· 1955 1955 "futures-util", 1956 1956 "generational-box", 1957 1957 "keyboard-types", 1958 - "lazy-js-bundle", 1958 + "lazy-js-bundle 0.7.0", 1959 1959 "rustversion", 1960 1960 "serde", 1961 1961 "serde_json", ··· 1985 1985 "dioxus-core-types", 1986 1986 "dioxus-html", 1987 1987 "js-sys", 1988 - "lazy-js-bundle", 1988 + "lazy-js-bundle 0.7.0", 1989 1989 "rustc-hash 2.1.1", 1990 1990 "serde", 1991 1991 "sledgehammer_bindgen", ··· 2036 2036 ] 2037 2037 2038 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]] 2039 2052 name = "dioxus-router" 2040 2053 version = "0.7.0" 2041 2054 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2193 2206 ] 2194 2207 2195 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]] 2196 2220 name = "dioxus-web" 2197 2221 version = "0.7.0" 2198 2222 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2213 2237 "generational-box", 2214 2238 "gloo-timers", 2215 2239 "js-sys", 2216 - "lazy-js-bundle", 2240 + "lazy-js-bundle 0.7.0", 2217 2241 "rustc-hash 2.1.1", 2218 2242 "send_wrapper", 2219 2243 "serde", ··· 4323 4347 "static-regular-grammar", 4324 4348 "thiserror 1.0.69", 4325 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" 4326 4356 4327 4357 [[package]] 4328 4358 name = "lazy-js-bundle" ··· 8620 8650 "console_error_panic_hook", 8621 8651 "dashmap", 8622 8652 "dioxus", 8653 + "dioxus-primitives", 8623 8654 "jacquard", 8624 8655 "jacquard-axum", 8625 8656 "markdown-weaver", 8657 + "mime-sniffer", 8626 8658 "mini-moka", 8627 8659 "n0-future", 8660 + "serde_json", 8628 8661 "time", 8629 8662 "weaver-api", 8630 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 36 "record": { 37 37 "type": "object", 38 38 "required": [ 39 - "colours", 39 + "darkScheme", 40 + "lightScheme", 40 41 "fonts", 41 42 "spacing", 42 - "codeTheme" 43 + "darkCodeTheme", 44 + "lightCodeTheme" 43 45 ], 44 46 "properties": { 45 - "codeTheme": { 47 + "darkCodeTheme": { 46 48 "type": "union", 49 + "description": "Syntax highlighting theme for dark mode", 47 50 "refs": [ 48 51 "#codeThemeName", 49 52 "#codeThemeFile" 50 53 ] 51 54 }, 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 - } 55 + "darkScheme": { 56 + "type": "ref", 57 + "description": "Reference to a dark colour scheme", 58 + "ref": "com.atproto.repo.strongRef" 82 59 }, 83 60 "fonts": { 84 61 "type": "object", ··· 98 75 "type": "string" 99 76 } 100 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" 101 91 }, 102 92 "spacing": { 103 93 "type": "object",
+1
crates/weaver-api/src/com_atproto/lexicon.rs
··· 3 3 // This file was automatically generated from Lexicon schemas. 4 4 // Any manual changes will be overwritten on the next regeneration. 5 5 6 + pub mod resolve_lexicon; 6 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 8 pub mod authors; 9 9 pub mod book; 10 10 pub mod chapter; 11 + pub mod colour_scheme; 11 12 pub mod entry; 12 13 pub mod page; 13 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 297 description: None, 298 298 required: Some( 299 299 vec![ 300 - ::jacquard_common::smol_str::SmolStr::new_static("colours"), 300 + ::jacquard_common::smol_str::SmolStr::new_static("darkScheme"), 301 + ::jacquard_common::smol_str::SmolStr::new_static("lightScheme"), 301 302 ::jacquard_common::smol_str::SmolStr::new_static("fonts"), 302 303 ::jacquard_common::smol_str::SmolStr::new_static("spacing"), 303 - ::jacquard_common::smol_str::SmolStr::new_static("codeTheme") 304 + ::jacquard_common::smol_str::SmolStr::new_static("darkCodeTheme"), 305 + ::jacquard_common::smol_str::SmolStr::new_static("lightCodeTheme") 304 306 ], 305 307 ), 306 308 nullable: None, ··· 309 311 let mut map = ::std::collections::BTreeMap::new(); 310 312 map.insert( 311 313 ::jacquard_common::smol_str::SmolStr::new_static( 312 - "codeTheme", 314 + "darkCodeTheme", 313 315 ), 314 316 ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { 315 - description: None, 317 + description: Some( 318 + ::jacquard_common::CowStr::new_static( 319 + "Syntax highlighting theme for dark mode", 320 + ), 321 + ), 316 322 refs: vec![ 317 323 ::jacquard_common::CowStr::new_static("#codeThemeName"), 318 324 ::jacquard_common::CowStr::new_static("#codeThemeFile") ··· 321 327 }), 322 328 ); 323 329 map.insert( 324 - ::jacquard_common::smol_str::SmolStr::new_static("colours"), 325 - ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { 330 + ::jacquard_common::smol_str::SmolStr::new_static( 331 + "darkScheme", 332 + ), 333 + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { 326 334 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 - ], 335 + r#ref: ::jacquard_common::CowStr::new_static( 336 + "com.atproto.repo.strongRef", 336 337 ), 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 338 }), 442 339 ); 443 340 map.insert( ··· 507 404 }), 508 405 ); 509 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( 510 435 ::jacquard_common::smol_str::SmolStr::new_static("spacing"), 511 436 ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { 512 437 description: None, ··· 615 540 )] 616 541 #[serde(rename_all = "camelCase")] 617 542 pub struct Theme<'a> { 543 + /// Syntax highlighting theme for dark mode 618 544 #[serde(borrow)] 619 - pub code_theme: ThemeCodeTheme<'a>, 545 + pub dark_code_theme: ThemeDarkCodeTheme<'a>, 546 + /// Reference to a dark colour scheme 620 547 #[serde(borrow)] 621 - pub colours: ThemeColours<'a>, 548 + pub dark_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, 622 549 #[serde(borrow)] 623 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>, 624 557 #[serde(borrow)] 625 558 pub spacing: ThemeSpacing<'a>, 626 559 } ··· 635 568 } 636 569 /// State trait tracking which required fields have been set 637 570 pub trait State: sealed::Sealed { 638 - type Colours; 571 + type DarkScheme; 572 + type LightScheme; 639 573 type Fonts; 640 574 type Spacing; 641 - type CodeTheme; 575 + type DarkCodeTheme; 576 + type LightCodeTheme; 642 577 } 643 578 /// Empty state - all required fields are unset 644 579 pub struct Empty(()); 645 580 impl sealed::Sealed for Empty {} 646 581 impl State for Empty { 647 - type Colours = Unset; 582 + type DarkScheme = Unset; 583 + type LightScheme = Unset; 648 584 type Fonts = Unset; 649 585 type Spacing = Unset; 650 - type CodeTheme = Unset; 586 + type DarkCodeTheme = Unset; 587 + type LightCodeTheme = Unset; 651 588 } 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>; 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; 657 595 type Fonts = S::Fonts; 658 596 type Spacing = S::Spacing; 659 - type CodeTheme = S::CodeTheme; 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; 660 610 } 661 611 ///State transition - sets the `fonts` field to Set 662 612 pub struct SetFonts<S: State = Empty>(PhantomData<fn() -> S>); 663 613 impl<S: State> sealed::Sealed for SetFonts<S> {} 664 614 impl<S: State> State for SetFonts<S> { 665 - type Colours = S::Colours; 615 + type DarkScheme = S::DarkScheme; 616 + type LightScheme = S::LightScheme; 666 617 type Fonts = Set<members::fonts>; 667 618 type Spacing = S::Spacing; 668 - type CodeTheme = S::CodeTheme; 619 + type DarkCodeTheme = S::DarkCodeTheme; 620 + type LightCodeTheme = S::LightCodeTheme; 669 621 } 670 622 ///State transition - sets the `spacing` field to Set 671 623 pub struct SetSpacing<S: State = Empty>(PhantomData<fn() -> S>); 672 624 impl<S: State> sealed::Sealed for SetSpacing<S> {} 673 625 impl<S: State> State for SetSpacing<S> { 674 - type Colours = S::Colours; 626 + type DarkScheme = S::DarkScheme; 627 + type LightScheme = S::LightScheme; 675 628 type Fonts = S::Fonts; 676 629 type Spacing = Set<members::spacing>; 677 - type CodeTheme = S::CodeTheme; 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; 678 643 } 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; 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; 684 650 type Fonts = S::Fonts; 685 651 type Spacing = S::Spacing; 686 - type CodeTheme = Set<members::code_theme>; 652 + type DarkCodeTheme = S::DarkCodeTheme; 653 + type LightCodeTheme = Set<members::light_code_theme>; 687 654 } 688 655 /// Marker types for field names 689 656 #[allow(non_camel_case_types)] 690 657 pub mod members { 691 - ///Marker type for the `colours` field 692 - pub struct colours(()); 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(()); 693 662 ///Marker type for the `fonts` field 694 663 pub struct fonts(()); 695 664 ///Marker type for the `spacing` field 696 665 pub struct spacing(()); 697 - ///Marker type for the `code_theme` field 698 - pub struct code_theme(()); 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(()); 699 670 } 700 671 } 701 672 ··· 703 674 pub struct ThemeBuilder<'a, S: theme_state::State> { 704 675 _phantom_state: ::core::marker::PhantomData<fn() -> S>, 705 676 __unsafe_private_named: ( 706 - ::core::option::Option<ThemeCodeTheme<'a>>, 707 - ::core::option::Option<ThemeColours<'a>>, 677 + ::core::option::Option<ThemeDarkCodeTheme<'a>>, 678 + ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 708 679 ::core::option::Option<ThemeFonts<'a>>, 680 + ::core::option::Option<ThemeLightCodeTheme<'a>>, 681 + ::core::option::Option<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 709 682 ::core::option::Option<ThemeSpacing<'a>>, 710 683 ), 711 684 _phantom: ::core::marker::PhantomData<&'a ()>, ··· 723 696 pub fn new() -> Self { 724 697 ThemeBuilder { 725 698 _phantom_state: ::core::marker::PhantomData, 726 - __unsafe_private_named: (None, None, None, None), 699 + __unsafe_private_named: (None, None, None, None, None, None), 727 700 _phantom: ::core::marker::PhantomData, 728 701 } 729 702 } ··· 732 705 impl<'a, S> ThemeBuilder<'a, S> 733 706 where 734 707 S: theme_state::State, 735 - S::CodeTheme: theme_state::IsUnset, 708 + S::DarkCodeTheme: theme_state::IsUnset, 736 709 { 737 - /// Set the `codeTheme` field (required) 738 - pub fn code_theme( 710 + /// Set the `darkCodeTheme` field (required) 711 + pub fn dark_code_theme( 739 712 mut self, 740 - value: impl Into<ThemeCodeTheme<'a>>, 741 - ) -> ThemeBuilder<'a, theme_state::SetCodeTheme<S>> { 713 + value: impl Into<ThemeDarkCodeTheme<'a>>, 714 + ) -> ThemeBuilder<'a, theme_state::SetDarkCodeTheme<S>> { 742 715 self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); 743 716 ThemeBuilder { 744 717 _phantom_state: ::core::marker::PhantomData, ··· 751 724 impl<'a, S> ThemeBuilder<'a, S> 752 725 where 753 726 S: theme_state::State, 754 - S::Colours: theme_state::IsUnset, 727 + S::DarkScheme: theme_state::IsUnset, 755 728 { 756 - /// Set the `colours` field (required) 757 - pub fn colours( 729 + /// Set the `darkScheme` field (required) 730 + pub fn dark_scheme( 758 731 mut self, 759 - value: impl Into<ThemeColours<'a>>, 760 - ) -> ThemeBuilder<'a, theme_state::SetColours<S>> { 732 + value: impl Into<crate::com_atproto::repo::strong_ref::StrongRef<'a>>, 733 + ) -> ThemeBuilder<'a, theme_state::SetDarkScheme<S>> { 761 734 self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); 762 735 ThemeBuilder { 763 736 _phantom_state: ::core::marker::PhantomData, ··· 789 762 impl<'a, S> ThemeBuilder<'a, S> 790 763 where 791 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, 792 803 S::Spacing: theme_state::IsUnset, 793 804 { 794 805 /// Set the `spacing` field (required) ··· 796 807 mut self, 797 808 value: impl Into<ThemeSpacing<'a>>, 798 809 ) -> ThemeBuilder<'a, theme_state::SetSpacing<S>> { 799 - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); 810 + self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); 800 811 ThemeBuilder { 801 812 _phantom_state: ::core::marker::PhantomData, 802 813 __unsafe_private_named: self.__unsafe_private_named, ··· 808 819 impl<'a, S> ThemeBuilder<'a, S> 809 820 where 810 821 S: theme_state::State, 811 - S::Colours: theme_state::IsSet, 822 + S::DarkScheme: theme_state::IsSet, 823 + S::LightScheme: theme_state::IsSet, 812 824 S::Fonts: theme_state::IsSet, 813 825 S::Spacing: theme_state::IsSet, 814 - S::CodeTheme: theme_state::IsSet, 826 + S::DarkCodeTheme: theme_state::IsSet, 827 + S::LightCodeTheme: theme_state::IsSet, 815 828 { 816 829 /// Build the final struct 817 830 pub fn build(self) -> Theme<'a> { 818 831 Theme { 819 - code_theme: self.__unsafe_private_named.0.unwrap(), 820 - colours: self.__unsafe_private_named.1.unwrap(), 832 + dark_code_theme: self.__unsafe_private_named.0.unwrap(), 833 + dark_scheme: self.__unsafe_private_named.1.unwrap(), 821 834 fonts: self.__unsafe_private_named.2.unwrap(), 822 - spacing: self.__unsafe_private_named.3.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(), 823 838 extra_data: Default::default(), 824 839 } 825 840 } ··· 832 847 >, 833 848 ) -> Theme<'a> { 834 849 Theme { 835 - code_theme: self.__unsafe_private_named.0.unwrap(), 836 - colours: self.__unsafe_private_named.1.unwrap(), 850 + dark_code_theme: self.__unsafe_private_named.0.unwrap(), 851 + dark_scheme: self.__unsafe_private_named.1.unwrap(), 837 852 fonts: self.__unsafe_private_named.2.unwrap(), 838 - spacing: self.__unsafe_private_named.3.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(), 839 856 extra_data: Some(extra_data), 840 857 } 841 858 } ··· 866 883 )] 867 884 #[serde(tag = "$type")] 868 885 #[serde(bound(deserialize = "'de: 'a"))] 869 - pub enum ThemeCodeTheme<'a> { 886 + pub enum ThemeDarkCodeTheme<'a> { 870 887 #[serde(rename = "sh.weaver.notebook.theme#codeThemeName")] 871 888 CodeThemeName(Box<crate::sh_weaver::notebook::theme::CodeThemeName<'a>>), 872 889 #[serde(rename = "sh.weaver.notebook.theme#codeThemeFile")] ··· 885 902 Default 886 903 )] 887 904 #[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>, 905 + pub struct ThemeFonts<'a> { 893 906 #[serde(borrow)] 894 - pub link: jacquard_common::CowStr<'a>, 907 + pub body: jacquard_common::CowStr<'a>, 895 908 #[serde(borrow)] 896 - pub link_hover: jacquard_common::CowStr<'a>, 909 + pub heading: jacquard_common::CowStr<'a>, 897 910 #[serde(borrow)] 898 - pub primary: jacquard_common::CowStr<'a>, 899 - #[serde(borrow)] 900 - pub secondary: jacquard_common::CowStr<'a>, 911 + pub monospace: jacquard_common::CowStr<'a>, 901 912 } 902 913 903 - impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeColours<'a> { 914 + impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeFonts<'a> { 904 915 fn nsid() -> &'static str { 905 916 "sh.weaver.notebook.theme" 906 917 } 907 918 fn def_name() -> &'static str { 908 - "ThemeColours" 919 + "ThemeFonts" 909 920 } 910 921 fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { 911 922 lexicon_doc_sh_weaver_notebook_theme() ··· 917 928 } 918 929 } 919 930 920 - #[jacquard_derive::lexicon] 931 + #[jacquard_derive::open_union] 921 932 #[derive( 922 933 serde::Serialize, 923 934 serde::Deserialize, ··· 925 936 Clone, 926 937 PartialEq, 927 938 Eq, 928 - jacquard_derive::IntoStatic, 929 - Default 939 + jacquard_derive::IntoStatic 930 940 )] 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 - } 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>>), 956 948 } 957 949 958 950 #[jacquard_derive::lexicon]
+46 -27
crates/weaver-common/src/lib.rs
··· 129 129 &self, 130 130 ident: &jacquard::types::ident::AtIdentifier<'_>, 131 131 title: &str, 132 - ) -> impl Future<Output = Result<Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, WeaverError>>; 132 + ) -> impl Future< 133 + Output = Result< 134 + Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, 135 + WeaverError, 136 + >, 137 + >; 133 138 } 134 139 135 140 impl<A: AgentSession + IdentityResolver> WeaverExt for Agent<A> { ··· 144 149 url_path: &'a str, 145 150 prev: Option<Tid>, 146 151 ) -> 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 - ); 152 + let mime_type = 153 + MimeType::new_owned(blob.sniff_mime_type().unwrap_or("application/octet-stream")); 151 154 152 155 let blob = self.upload_blob(blob, mime_type).await?; 153 156 let publish_record = PublishedBlob::new() ··· 195 198 if let Ok(list) = resp.parse() { 196 199 for record in list.records { 197 200 let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { 198 - AgentError::from(ClientError::invalid_request("Failed to parse notebook record")) 201 + AgentError::from(ClientError::invalid_request( 202 + "Failed to parse notebook record", 203 + )) 199 204 })?; 200 205 if let Some(book_title) = notebook.title 201 206 && book_title == title ··· 265 270 266 271 // Add to notebook's entry_list 267 272 use weaver_api::sh_weaver::notebook::book::Book; 268 - let new_ref = StrongRef::new() 269 - .uri(response.uri) 270 - .cid(response.cid) 271 - .build(); 273 + let new_ref = StrongRef::new().uri(response.uri).cid(response.cid).build(); 272 274 273 275 self.update_record::<Book>(&notebook_uri, |book| { 274 276 book.entry_list.push(new_ref); ··· 282 284 &self, 283 285 uri: &AtUri<'_>, 284 286 ) -> Result<(view::NotebookView<'static>, Vec<StrongRef<'static>>), WeaverError> { 287 + use jacquard::to_data; 285 288 use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 289 + use weaver_api::sh_weaver::notebook::AuthorListView; 286 290 use weaver_api::sh_weaver::notebook::book::Book; 287 - use weaver_api::sh_weaver::notebook::AuthorListView; 288 - use jacquard::to_data; 289 291 290 292 let notebook = self 291 293 .get_record::<Book>(uri) ··· 302 304 let mut authors = Vec::new(); 303 305 304 306 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_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 + })?; 307 312 let author_profile = self.fetch_record(&author_uri).await?; 308 313 309 314 authors.push( 310 315 AuthorListView::new() 311 316 .uri(author_uri.as_uri().clone()) 312 317 .record(to_data(&author_profile).map_err(|_| { 313 - AgentError::from(ClientError::invalid_request("Failed to serialize author profile")) 318 + AgentError::from(ClientError::invalid_request( 319 + "Failed to serialize author profile", 320 + )) 314 321 })?) 315 322 .index(index as i64) 316 323 .build(), ··· 347 354 notebook: &view::NotebookView<'a>, 348 355 entry_ref: &StrongRef<'_>, 349 356 ) -> Result<view::EntryView<'a>, WeaverError> { 350 - use weaver_api::sh_weaver::notebook::entry::Entry; 351 357 use jacquard::to_data; 358 + use weaver_api::sh_weaver::notebook::entry::Entry; 352 359 353 360 let entry_uri = Entry::uri(entry_ref.uri.clone()) 354 361 .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid entry URI")))?; ··· 378 385 entries: &[StrongRef<'_>], 379 386 title: &str, 380 387 ) -> Result<Option<(view::BookEntryView<'a>, entry::Entry<'a>)>, WeaverError> { 381 - use weaver_api::sh_weaver::notebook::entry::Entry; 382 388 use weaver_api::sh_weaver::notebook::BookEntryRef; 389 + use weaver_api::sh_weaver::notebook::entry::Entry; 383 390 384 391 for (index, entry_ref) in entries.iter().enumerate() { 385 392 let resp = self ··· 426 433 ident: &jacquard::types::ident::AtIdentifier<'_>, 427 434 title: &str, 428 435 ) -> Result<Option<(view::NotebookView<'static>, Vec<StrongRef<'static>>)>, WeaverError> { 436 + use jacquard::to_data; 429 437 use jacquard::types::collection::Collection; 430 438 use jacquard::types::nsid::Nsid; 431 439 use jacquard::xrpc::XrpcExt; 440 + use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; 432 441 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 442 use weaver_api::sh_weaver::notebook::AuthorListView; 436 - use jacquard::to_data; 443 + use weaver_api::sh_weaver::notebook::book::Book; 437 444 438 445 let (repo_did, pds_url) = match ident { 439 446 jacquard::types::ident::AtIdentifier::Did(did) => { 440 447 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")) 448 + AgentError::from( 449 + ClientError::from(e).with_context("Failed to resolve PDS for DID"), 450 + ) 442 451 })?; 443 452 (did.clone(), pds) 444 453 } 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 - })?, 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 + } 448 459 }; 449 460 450 461 // TODO: use the cursor to search through all records with this NSID for the repo ··· 463 474 if let Ok(list) = resp.parse() { 464 475 for record in list.records { 465 476 let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { 466 - AgentError::from(ClientError::invalid_request("Failed to parse notebook record")) 477 + AgentError::from(ClientError::invalid_request( 478 + "Failed to parse notebook record", 479 + )) 467 480 })?; 468 481 if let Some(book_title) = notebook.title 469 482 && book_title == title ··· 477 490 "at://{}/app.bsky.actor.profile/self", 478 491 author.did 479 492 )) 480 - .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid author profile URI")))?; 493 + .map_err(|_| { 494 + AgentError::from(ClientError::invalid_request( 495 + "Invalid author profile URI", 496 + )) 497 + })?; 481 498 let author_profile = self.fetch_record(&author_uri).await?; 482 499 483 500 authors.push( 484 501 AuthorListView::new() 485 502 .uri(author_uri.as_uri().clone()) 486 503 .record(to_data(&author_profile).map_err(|_| { 487 - AgentError::from(ClientError::invalid_request("Failed to serialize author profile")) 504 + AgentError::from(ClientError::invalid_request( 505 + "Failed to serialize author profile", 506 + )) 488 507 })?) 489 508 .index(index as i64) 490 509 .build(),
+67 -15
crates/weaver-renderer/src/atproto/writer.rs
··· 26 26 /// 27 27 /// This writer is designed for client-side rendering where embeds may have 28 28 /// pre-rendered content in their attributes. 29 - pub struct ClientWriter<W: StrWrite, E = ()> { 29 + pub struct ClientWriter<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E = ()> { 30 + events: I, 30 31 writer: W, 31 32 end_newline: bool, 32 33 in_non_writing_block: bool, ··· 40 41 embed_provider: Option<E>, 41 42 42 43 code_buffer: Option<(Option<String>, String)>, // (lang, content) 44 + _phantom: std::marker::PhantomData<&'a ()>, 43 45 } 44 46 45 47 #[derive(Debug, Clone, Copy)] ··· 48 50 Body, 49 51 } 50 52 51 - impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 52 - pub fn new(writer: W) -> Self { 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 { 53 55 Self { 56 + events, 54 57 writer, 55 58 end_newline: true, 56 59 in_non_writing_block: false, ··· 60 63 numbers: HashMap::new(), 61 64 embed_provider: None, 62 65 code_buffer: None, 66 + _phantom: std::marker::PhantomData, 63 67 } 64 68 } 65 69 66 70 /// Add an embed content provider 67 - pub fn with_embed_provider(self, provider: E) -> ClientWriter<W, E> { 71 + pub fn with_embed_provider(self, provider: E) -> ClientWriter<'a, I, W, E> { 68 72 ClientWriter { 73 + events: self.events, 69 74 writer: self.writer, 70 75 end_newline: self.end_newline, 71 76 in_non_writing_block: self.in_non_writing_block, ··· 75 80 numbers: self.numbers, 76 81 embed_provider: Some(provider), 77 82 code_buffer: self.code_buffer, 83 + _phantom: std::marker::PhantomData, 78 84 } 79 85 } 80 - } 81 - 82 - impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 83 86 #[inline] 84 87 fn write_newline(&mut self) -> Result<(), W::Error> { 85 88 self.end_newline = true; ··· 96 99 } 97 100 98 101 /// 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 { 102 + pub fn run(mut self) -> Result<W, W::Error> { 103 + while let Some(event) = self.events.next() { 101 104 self.process_event(event)?; 102 105 } 103 106 Ok(self.writer) 104 107 } 105 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 + 106 155 fn process_event(&mut self, event: Event<'_>) -> Result<(), W::Error> { 107 156 use Event::*; 108 157 match event { ··· 368 417 self.write("<img src=\"")?; 369 418 escape_href(&mut self.writer, &dest_url)?; 370 419 self.write("\" alt=\"")?; 420 + // Consume text events for alt attribute 421 + self.raw_text()?; 422 + self.write("\"")?; 371 423 if !title.is_empty() { 424 + self.write(" title=\"")?; 372 425 escape_html(&mut self.writer, &title)?; 426 + self.write("\"")?; 373 427 } 374 428 if let Some(attrs) = attrs { 375 429 if !attrs.classes.is_empty() { 376 - self.write("\" class=\"")?; 430 + self.write(" class=\"")?; 377 431 for (i, class) in attrs.classes.iter().enumerate() { 378 432 if i > 0 { 379 433 self.write(" ")?; 380 434 } 381 435 escape_html(&mut self.writer, class)?; 382 436 } 437 + self.write("\"")?; 383 438 } 384 - self.write("\"")?; 385 439 for (attr, value) in &attrs.attrs { 386 440 self.write(" ")?; 387 441 escape_html(&mut self.writer, attr)?; ··· 389 443 escape_html(&mut self.writer, value)?; 390 444 self.write("\"")?; 391 445 } 392 - } else { 393 - self.write("\"")?; 394 446 } 395 447 self.write(" />") 396 448 } ··· 501 553 TagEnd::Strong => self.write("</strong>"), 502 554 TagEnd::Strikethrough => self.write("</del>"), 503 555 TagEnd::Link => self.write("</a>"), 504 - TagEnd::Image => Ok(()), 556 + TagEnd::Image => Ok(()), // No-op: raw_text() already consumed the End(Image) event 505 557 TagEnd::Embed => Ok(()), 506 558 TagEnd::WeaverBlock(_) => { 507 559 self.in_non_writing_block = false; ··· 516 568 } 517 569 } 518 570 519 - impl<W: StrWrite, E: EmbedContentProvider> ClientWriter<W, E> { 571 + impl<'a, I: Iterator<Item = Event<'a>>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { 520 572 fn write_embed( 521 573 &mut self, 522 574 embed_type: EmbedType,
+1 -1
crates/weaver-renderer/src/code_pretty.rs
··· 27 27 .find_syntax_by_first_line(code.as_ref()) 28 28 .unwrap_or_else(|| syn_set.find_syntax_plain_text()) 29 29 }; 30 - writer.write_str("<pre><code class=\"language-")?; 30 + writer.write_str("<pre><code class=\"wvrcode-code language-")?; 31 31 writer.write_str(&lang_syn.name)?; 32 32 writer.write_str("\">")?; 33 33
+224 -47
crates/weaver-renderer/src/css.rs
··· 1 - use crate::theme::Theme; 1 + use crate::theme::{ResolvedTheme, ThemeDarkCodeTheme, ThemeLightCodeTheme}; 2 2 use miette::IntoDiagnostic; 3 3 use std::io::Cursor; 4 4 use syntect::highlighting::ThemeSet; 5 5 use syntect::html::{ClassStyle, css_for_theme_with_class_style}; 6 6 use weaver_api::com_atproto::sync::get_blob::GetBlob; 7 - use weaver_api::sh_weaver::notebook::theme::ThemeCodeTheme; 8 7 use weaver_common::jacquard::client::BasicClient; 9 8 use weaver_common::jacquard::prelude::*; 10 9 use weaver_common::jacquard::xrpc::XrpcExt; ··· 13 12 const ROSE_PINE_THEME: &str = include_str!("../themes/rose-pine.tmTheme"); 14 13 const ROSE_PINE_DAWN_THEME: &str = include_str!("../themes/rose-pine-dawn.tmTheme"); 15 14 16 - pub fn generate_base_css(theme: &Theme) -> String { 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 + 17 21 format!( 18 22 r#"/* CSS Reset */ 19 23 *, *::before, *::after {{ ··· 22 26 padding: 0; 23 27 }} 24 28 25 - /* CSS Variables */ 29 + /* CSS Variables - Light Mode (default) */ 26 30 :root {{ 27 - --color-background: {}; 28 - --color-foreground: {}; 29 - --color-link: {}; 30 - --color-link-hover: {}; 31 + --color-base: {}; 32 + --color-surface: {}; 33 + --color-overlay: {}; 34 + --color-text: {}; 35 + --color-muted: {}; 36 + --color-subtle: {}; 37 + --color-emphasis: {}; 31 38 --color-primary: {}; 32 39 --color-secondary: {}; 40 + --color-tertiary: {}; 41 + --color-error: {}; 42 + --color-warning: {}; 43 + --color-success: {}; 44 + --color-border: {}; 45 + --color-link: {}; 46 + --color-highlight: {}; 33 47 34 48 --font-body: {}; 35 49 --font-heading: {}; ··· 40 54 --spacing-scale: {}; 41 55 }} 42 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 + 43 79 /* Base Styles */ 44 80 html {{ 45 81 font-size: var(--spacing-base); ··· 48 84 49 85 body {{ 50 86 font-family: var(--font-body); 51 - color: var(--color-foreground); 52 - background-color: var(--color-background); 87 + color: var(--color-text); 88 + background-color: var(--color-base); 53 89 max-width: 90ch; 54 90 margin: 0 auto; 55 91 padding: 2rem 1rem; ··· 95 131 }} 96 132 97 133 a:hover {{ 98 - color: var(--color-link-hover); 134 + color: var(--color-emphasis); 99 135 text-decoration: underline; 136 + }} 137 + 138 + /* Selection */ 139 + ::selection {{ 140 + background: var(--color-highlight); 141 + color: var(--color-text); 100 142 }} 101 143 102 144 /* Lists */ ··· 112 154 /* Code */ 113 155 code {{ 114 156 font-family: var(--font-mono); 115 - background-color: rgba(0, 0, 0, 0.05); 157 + background: var(--color-surface); 116 158 padding: 0.125rem 0.25rem; 117 - border-radius: 3px; 159 + border-radius: 4px; 118 160 font-size: 0.9em; 119 161 }} 120 162 121 163 pre {{ 122 164 overflow-x: auto; 123 165 margin-bottom: 1rem; 166 + border-radius: 5px; 167 + border: 1px solid var(--color-border); 168 + box-sizing: border-box; 124 169 }} 125 170 171 + /* Code blocks inside pre are handled by syntax theme */ 126 172 pre code {{ 127 173 display: block; 174 + width: fit-content; 175 + min-width: 100%; 128 176 padding: 1rem; 129 - background-color: rgba(0, 0, 0, 0.03); 130 - border-radius: 5px; 177 + background: var(--color-surface); 131 178 }} 132 179 133 180 /* Math */ ··· 143 190 144 191 /* Blockquotes */ 145 192 blockquote {{ 146 - border-left: 4px solid var(--color-link); 193 + border-left: 2px solid var(--color-secondary); 194 + background: var(--color-surface); 147 195 padding-left: 1rem; 148 196 padding-right: 1rem; 197 + padding-top: 0.5rem; 198 + padding-bottom: 0.04rem; 149 199 margin: 1rem 0; 150 - font-style: italic; 200 + font-size: 0.95em; 201 + border-bottom-right-radius: 5px; 202 + border-top-right-radius: 5px; 203 + }} 151 204 }} 152 205 153 206 /* Tables */ ··· 158 211 }} 159 212 160 213 th, td {{ 161 - border: 1px solid rgba(0, 0, 0, 0.1); 214 + border: 1px solid var(--color-border); 162 215 padding: 0.5rem; 163 216 text-align: left; 164 217 }} 165 218 166 219 th {{ 167 - background-color: rgba(0, 0, 0, 0.05); 220 + background: var(--color-surface); 168 221 font-weight: 600; 169 222 }} 170 223 224 + tr:hover {{ 225 + background: var(--color-surface); 226 + }} 227 + 171 228 /* Footnotes */ 172 229 .footnote-reference {{ 173 230 font-size: 0.8em; 231 + color: var(--color-subtle); 174 232 }} 175 233 176 234 .footnote-definition {{ 177 235 margin-top: 2rem; 178 236 padding-top: 0.5rem; 179 - border-top: 1px solid rgba(0, 0, 0, 0.1); 237 + border-top: 1px solid var(--color-border); 180 238 font-size: 0.9em; 181 239 }} 182 240 183 241 .footnote-definition-label {{ 184 242 font-weight: 600; 185 243 margin-right: 0.5rem; 244 + color: var(--color-primary); 186 245 }} 187 246 188 247 /* Images */ ··· 191 250 height: auto; 192 251 display: block; 193 252 margin: 1rem 0; 253 + border-radius: 4px; 194 254 }} 195 255 196 256 /* Horizontal Rule */ 197 257 hr {{ 198 258 border: none; 199 - border-top: 1px solid rgba(0, 0, 0, 0.1); 259 + border-top: 2px solid var(--color-border); 200 260 margin: 2rem 0; 201 261 }} 202 262 "#, 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, 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, 215 304 ) 216 305 } 217 306 218 - pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result<String> { 219 - let syntect_theme = match &theme.code_theme { 220 - ThemeCodeTheme::CodeThemeName(name) => { 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) => { 221 373 match name.as_str() { 222 374 "rose-pine" => { 223 375 let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); ··· 225 377 .into_diagnostic() 226 378 .map_err(|e| { 227 379 miette::miette!("Failed to load embedded rose-pine theme: {}", e) 228 - })? 380 + }) 229 381 } 230 382 "rose-pine-dawn" => { 231 383 let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes()); ··· 233 385 .into_diagnostic() 234 386 .map_err(|e| { 235 387 miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e) 236 - })? 388 + }) 237 389 } 238 390 _ => { 239 391 // Fall back to syntect's built-in themes ··· 241 393 theme_set 242 394 .themes 243 395 .get(name.as_str()) 244 - .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name))? 245 - .clone() 396 + .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name)) 397 + .cloned() 246 398 } 247 399 } 248 400 } 249 - ThemeCodeTheme::CodeThemeFile(file) => { 401 + ThemeLightCodeTheme::CodeThemeFile(file) => { 250 402 let client = BasicClient::unauthenticated(); 251 403 let pds = client.pds_for_did(&file.did).await?; 252 404 let blob = client ··· 263 415 let mut cursor = Cursor::new(blob); 264 416 ThemeSet::load_from_reader(&mut cursor) 265 417 .into_diagnostic() 266 - .map_err(|e| miette::miette!("Failed to download theme: {}", e))? 418 + .map_err(|e| miette::miette!("Failed to download theme: {}", e)) 267 419 } 268 420 _ => { 269 421 let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes()); 270 422 ThemeSet::load_from_reader(&mut cursor) 271 423 .into_diagnostic() 272 - .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e))? 424 + .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e)) 273 425 } 274 - }; 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()?; 275 442 276 - let css = css_for_theme_with_class_style( 277 - &syntect_theme, 443 + // Generate light mode CSS 444 + let light_css = css_for_theme_with_class_style( 445 + &light_syntect_theme, 278 446 ClassStyle::SpacedPrefixed { 279 447 prefix: crate::code_pretty::CSS_PREFIX, 280 448 }, 281 449 ) 282 450 .into_diagnostic()?; 283 451 284 - Ok(css) 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) 285 462 }
+2 -2
crates/weaver-renderer/src/static_site.rs
··· 17 17 document::{CssMode, write_document_footer, write_document_head}, 18 18 writer::StaticPageWriter, 19 19 }, 20 - theme::default_theme, 20 + theme::default_resolved_theme, 21 21 utils::flatten_dir_to_just_one_parent, 22 22 walker::{WalkOptions, vault_contents}, 23 23 }; ··· 239 239 .await 240 240 .into_diagnostic()?; 241 241 242 - let default_theme = default_theme(); 242 + let default_theme = default_resolved_theme(); 243 243 let theme = self.context.theme.as_deref().unwrap_or(&default_theme); 244 244 245 245 // Write base.css
+6 -4
crates/weaver-renderer/src/static_site/context.rs
··· 1 1 use crate::static_site::{StaticSiteOptions}; 2 - use crate::theme::Theme; 2 + use crate::theme::ResolvedTheme; 3 3 use crate::{Frontmatter, NotebookContext,default_md_options}; 4 4 use dashmap::DashMap; 5 5 use markdown_weaver::{CowStr, EmbedType, Tag, WeaverAttributes}; ··· 39 39 pub client: Option<reqwest::Client>, 40 40 agent: Option<Arc<Agent<A>>>, 41 41 42 - pub theme: Option<Arc<Theme<'static>>>, 42 + pub theme: Option<Arc<ResolvedTheme<'static>>>, 43 43 pub katex_source: Option<KaTeXSource>, 44 44 pub syntax_set: Arc<SyntaxSet>, 45 45 pub index_file: Option<PathBuf>, ··· 122 122 } 123 123 } 124 124 pub fn new(root: PathBuf, destination: PathBuf, session: Option<A>) -> Self { 125 + use crate::theme::default_resolved_theme; 126 + 125 127 Self { 126 128 start_at: root.clone(), 127 129 root, ··· 136 138 position: 0, 137 139 client: Some(reqwest::Client::default()), 138 140 agent: session.map(|session| Arc::new(Agent::new(session))), 139 - theme: None, 141 + theme: Some(Arc::new(default_resolved_theme())), 140 142 katex_source: None, 141 143 syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()), 142 144 index_file: None, 143 145 } 144 146 } 145 147 146 - pub fn with_theme(mut self, theme: Theme<'static>) -> Self { 148 + pub fn with_theme(mut self, theme: ResolvedTheme<'static>) -> Self { 147 149 self.theme = Some(Arc::new(theme)); 148 150 self 149 151 }
+2 -2
crates/weaver-renderer/src/static_site/document.rs
··· 1 1 use crate::css::{generate_base_css, generate_syntax_css}; 2 2 use crate::static_site::context::{KaTeXSource, StaticSiteContext}; 3 - use crate::theme::{Theme, default_theme}; 3 + use crate::theme::default_resolved_theme; 4 4 use miette::IntoDiagnostic; 5 5 use weaver_common::jacquard::client::AgentSession; 6 6 ··· 98 98 .into_diagnostic()?; 99 99 } 100 100 CssMode::Inline => { 101 - let default_theme = default_theme(); 101 + let default_theme = default_resolved_theme(); 102 102 let theme = context.theme.as_deref().unwrap_or(&default_theme); 103 103 104 104 writer.write_all(b" <style>\n").await.into_diagnostic()?;
+128 -31
crates/weaver-renderer/src/theme.rs
··· 1 + use miette::IntoDiagnostic; 2 + pub use weaver_api::sh_weaver::notebook::colour_scheme::{ColourScheme, ColourSchemeColours}; 1 3 pub use weaver_api::sh_weaver::notebook::theme::{ 2 - Theme, ThemeCodeTheme, ThemeColours, ThemeFonts, ThemeSpacing, 4 + Theme, ThemeDarkCodeTheme, ThemeFonts, ThemeLightCodeTheme, ThemeSpacing, 3 5 }; 4 6 use weaver_common::jacquard::CowStr; 7 + use weaver_common::jacquard::IntoStatic; 8 + use weaver_common::jacquard::client::AgentSession; 5 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 + } 6 44 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"), 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(); 19 126 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() 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 + }) 38 135 }
+3 -2
crates/weaver-server/Cargo.toml
··· 26 26 weaver-renderer = { path = "../weaver-renderer" } 27 27 mini-moka = { git = "https://github.com/moka-rs/mini-moka", rev = "da864e849f5d034f32e02197fee9bb5d5af36d3d" } 28 28 n0-future = { workspace = true } 29 - #dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } 29 + dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } 30 30 axum = {version = "0.8.6", optional = true} 31 - 31 + mime-sniffer = {version = "^0.1"} 32 32 chrono = { version = "0.4" } 33 + serde_json = "1.0" 33 34 34 35 [target.'cfg(target_arch = "wasm32")'.dependencies] 35 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 1 body { 2 - background-color: #0f1116; 3 - color: #ffffff; 4 - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 2 + background-color: var(--color-base); 3 + color: var(--color-text); 4 + font-family: var(--font-body); 5 5 margin: 20px; 6 6 } 7 7 ··· 17 17 width: 400px; 18 18 text-align: left; 19 19 font-size: x-large; 20 - color: white; 20 + color: var(--color-text); 21 21 display: flex; 22 22 flex-direction: column; 23 23 } 24 24 25 25 #links a { 26 - color: white; 26 + color: var(--color-link); 27 27 text-decoration: none; 28 28 margin-top: 20px; 29 29 margin: 10px 0px; 30 - border: white 1px solid; 30 + border: 1px solid var(--color-border); 31 31 border-radius: 5px; 32 32 padding: 10px; 33 + transition: all 0.2s ease; 33 34 } 34 35 35 36 #links a:hover { 36 - background-color: #1f1f1f; 37 + background-color: var(--color-surface); 38 + border-color: var(--color-primary); 37 39 cursor: pointer; 38 40 } 39 41
+2 -2
crates/weaver-server/assets/styling/navbar.css
··· 4 4 } 5 5 6 6 #navbar a { 7 - color: #ffffff; 7 + color: var(--color-text); 8 8 margin-right: 20px; 9 9 text-decoration: none; 10 10 transition: color 0.2s ease; ··· 12 12 13 13 #navbar a:hover { 14 14 cursor: pointer; 15 - color: #91a4d2; 15 + color: var(--color-primary); 16 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 7 8 8 .avatar-label { 9 9 margin: 0; 10 - color: var(--secondary-color-4); 10 + color: var(--color-secondary); 11 11 font-size: 0.875rem; 12 12 } 13 13 ··· 21 21 flex-shrink: 0; 22 22 align-items: center; 23 23 justify-content: center; 24 - border-radius: 3.40282e+38px; 25 - color: var(--secondary-color-4); 24 + border-radius: 3.40282e38px; 25 + color: var(--color-secondary); 26 26 cursor: pointer; 27 27 font-weight: 500; 28 28 } ··· 58 58 } 59 59 60 60 .avatar[data-state="empty"] { 61 - background: var(--primary-color-2); 61 + background: var(--color-surface); 62 62 } 63 63 64 64 @keyframes pulse { ··· 81 81 height: 100%; 82 82 align-items: center; 83 83 justify-content: center; 84 - background: var(--primary-color); 85 - color: var(--secondary-color-4); 84 + background: var(--color-surface); 85 + color: var(--color-primary); 86 86 font-size: 1.5rem; 87 87 } 88 88 89 89 .avatar[data-state="error"] .avatar-fallback { 90 - background: var(--primary-color-3); 91 - color: var(--secondary-color-4); 90 + background: var(--color-surface); 91 + color: var(--color-subtle); 92 92 }
+20 -14
crates/weaver-server/src/components/css.rs
··· 15 15 #[allow(unused_imports)] 16 16 use std::sync::Arc; 17 17 #[allow(unused_imports)] 18 - use weaver_renderer::theme::Theme; 18 + use weaver_renderer::theme::{Theme, ResolvedTheme}; 19 19 20 20 #[cfg(feature = "server")] 21 21 use axum::{extract::Extension, response::IntoResponse}; ··· 37 37 38 38 use weaver_api::sh_weaver::notebook::book::Book; 39 39 use weaver_renderer::css::{generate_base_css, generate_syntax_css}; 40 - use weaver_renderer::theme::default_theme; 40 + use weaver_renderer::theme::{default_resolved_theme, resolve_theme}; 41 41 42 42 let ident = AtIdentifier::new_owned(ident)?; 43 - let theme = if let Some(notebook) = fetcher.get_notebook(ident, notebook).await? { 43 + let resolved_theme = if let Some(notebook) = fetcher.get_notebook(ident, notebook).await? { 44 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()) 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 + } 51 56 } else { 52 - default_theme() 57 + default_resolved_theme() 53 58 } 54 59 } else { 55 - default_theme() 60 + default_resolved_theme() 56 61 } 57 62 } else { 58 - default_theme() 63 + default_resolved_theme() 59 64 }; 60 - let mut css = generate_base_css(&theme); 65 + 66 + let mut css = generate_base_css(&resolved_theme); 61 67 css.push_str( 62 - &generate_syntax_css(&theme) 68 + &generate_syntax_css(&resolved_theme) 63 69 .await 64 70 .map_err(|e| CapturedError::from_display(e)) 65 71 .unwrap_or_default(),
+208 -6
crates/weaver-server/src/components/entry.rs
··· 2 2 3 3 #[cfg(feature = "server")] 4 4 use crate::blobcache::BlobCache; 5 - use crate::fetch; 5 + use crate::{ 6 + components::avatar::{Avatar, AvatarFallback, AvatarImage}, 7 + fetch, 8 + }; 6 9 use dioxus::prelude::*; 10 + 11 + const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); 7 12 #[allow(unused_imports)] 8 13 use dioxus::{fullstack::extract::Extension, CapturedError}; 9 - use jacquard::{prelude::IdentityResolver, smol_str::ToSmolStr}; 14 + use jacquard::{ 15 + from_data, prelude::IdentityResolver, smol_str::ToSmolStr, types::string::Datetime, 16 + }; 10 17 #[allow(unused_imports)] 11 18 use jacquard::{ 12 19 smol_str::SmolStr, ··· 19 26 #[component] 20 27 pub fn Entry(ident: AtIdentifier<'static>, book_title: SmolStr, title: SmolStr) -> Element { 21 28 let ident_clone = ident.clone(); 29 + let book_title_clone = book_title.clone(); 22 30 let entry = use_resource(use_reactive!(|(ident, book_title, title)| async move { 23 31 let fetcher = use_context::<fetch::CachedFetcher>(); 24 32 let entry = fetcher ··· 48 56 49 57 match &*entry.read_unchecked() { 50 58 Some(Some(entry_data)) => { 51 - rsx! { EntryMarkdownDirect { 52 - content: entry_data.1.clone(), 53 - ident: ident_clone 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 54 64 } } 55 65 } 56 66 Some(None) => { ··· 60 70 } 61 71 } 62 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 + 63 138 #[component] 64 139 pub fn EntryCard(entry: BookEntryView<'static>) -> Element { 65 140 rsx! {} 66 141 } 67 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 + 68 270 #[derive(Props, Clone, PartialEq)] 69 271 pub struct EntryMarkdownProps { 70 272 #[props(default)] ··· 124 326 125 327 // Render to HTML 126 328 let mut html_buf = String::new(); 127 - let _ = ClientWriter::<_, ()>::new(&mut html_buf).run(events.into_iter()); 329 + let _ = ClientWriter::<_, _, ()>::new(events.into_iter(), &mut html_buf).run(); 128 330 Some(html_buf) 129 331 })); 130 332
+1 -1
crates/weaver-server/src/components/mod.rs
··· 10 10 11 11 mod identity; 12 12 pub use identity::{Repository, RepositoryIndex}; 13 - //pub mod avatar; 13 + pub mod avatar;
+51 -2
crates/weaver-server/src/main.rs
··· 1 1 // The dioxus prelude contains a ton of common items used in dioxus apps. It's a good idea to import wherever you 2 2 // need dioxus 3 3 use components::{Entry, Repository, RepositoryIndex}; 4 - use dioxus::{fullstack::FullstackContext, prelude::*}; 5 - use jacquard::{client::BasicClient, smol_str::SmolStr, types::string::AtIdentifier}; 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 + 6 17 use std::sync::Arc; 18 + #[allow(unused)] 7 19 use views::{Home, Navbar, Notebook, NotebookIndex, NotebookPage}; 8 20 9 21 #[cfg(feature = "server")] ··· 135 147 } 136 148 } 137 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 1 use crate::Route; 2 2 use dioxus::prelude::*; 3 3 4 + const THEME_DEFAULTS_CSS: Asset = asset!("/assets/styling/theme-defaults.css"); 4 5 const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); 5 6 6 7 /// The Navbar component that will be rendered on all pages of our app since every page is under the layout. ··· 11 12 #[component] 12 13 pub fn Navbar() -> Element { 13 14 rsx! { 15 + document::Link { rel: "stylesheet", href: THEME_DEFAULTS_CSS } 14 16 document::Link { rel: "stylesheet", href: NAVBAR_CSS } 15 17 16 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 8 "key": "tid", 9 9 "record": { 10 10 "type": "object", 11 - "required": ["colours", "fonts", "spacing", "codeTheme"], 11 + "required": ["darkScheme", "lightScheme", "fonts", "spacing", "darkCodeTheme", "lightCodeTheme"], 12 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 - } 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" 36 22 }, 37 23 "fonts": { 38 24 "type": "object", ··· 64 50 } 65 51 } 66 52 }, 67 - "codeTheme": { 53 + "darkCodeTheme": { 68 54 "type": "union", 69 - "refs": ["#codeThemeName", "#codeThemeFile"] 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" 70 62 } 71 63 } 72 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.