diff --git a/Cargo.lock b/Cargo.lock index e7aa48b..6672651 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1724,7 +1724,7 @@ dependencies = [ "global-hotkey", "infer", "jni", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0", "libc", "muda", "ndk", @@ -1796,7 +1796,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0", "serde", "serde_json", "tracing", @@ -1955,7 +1955,7 @@ dependencies = [ "futures-util", "generational-box", "keyboard-types", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0", "rustversion", "serde", "serde_json", @@ -1985,7 +1985,7 @@ dependencies = [ "dioxus-core-types", "dioxus-html", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0", "rustc-hash 2.1.1", "serde", "sledgehammer_bindgen", @@ -2035,6 +2035,19 @@ dependencies = [ "tracing-wasm", ] +[[package]] +name = "dioxus-primitives" +version = "0.0.1" +source = "git+https://github.com/DioxusLabs/components#8e25631c7d4234ee070509156ed2abebb7b1d6e9" +dependencies = [ + "dioxus", + "dioxus-time", + "lazy-js-bundle 0.6.2", + "num-integer", + "time", + "tracing", +] + [[package]] name = "dioxus-router" version = "0.7.0" @@ -2192,6 +2205,17 @@ dependencies = [ "syn 2.0.108", ] +[[package]] +name = "dioxus-time" +version = "0.7.0" +source = "git+https://github.com/ealmloff/dioxus-std?branch=0.7#e5a74354cd36be440febdd6b701f584bd552bd62" +dependencies = [ + "dioxus", + "futures", + "gloo-timers", + "tokio", +] + [[package]] name = "dioxus-web" version = "0.7.0" @@ -2213,7 +2237,7 @@ dependencies = [ "generational-box", "gloo-timers", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.0", "rustc-hash 2.1.1", "send_wrapper", "serde", @@ -4324,6 +4348,12 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "lazy-js-bundle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" + [[package]] name = "lazy-js-bundle" version = "0.7.0" @@ -8620,11 +8650,14 @@ dependencies = [ "console_error_panic_hook", "dashmap", "dioxus", + "dioxus-primitives", "jacquard", "jacquard-axum", "markdown-weaver", + "mime-sniffer", "mini-moka", "n0-future", + "serde_json", "time", "weaver-api", "weaver-common", diff --git a/crates/weaver-api/lexicons/com_atproto_lexicon_resolveLexicon.json b/crates/weaver-api/lexicons/com_atproto_lexicon_resolveLexicon.json new file mode 100644 index 0000000..80db537 --- /dev/null +++ b/crates/weaver-api/lexicons/com_atproto_lexicon_resolveLexicon.json @@ -0,0 +1,57 @@ +{ + "lexicon": 1, + "id": "com.atproto.lexicon.resolveLexicon", + "defs": { + "main": { + "type": "query", + "description": "Resolves an atproto lexicon (NSID) to a schema.", + "parameters": { + "type": "params", + "required": [ + "nsid" + ], + "properties": { + "nsid": { + "type": "string", + "description": "The lexicon NSID to resolve.", + "format": "nsid" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [ + "uri", + "cid", + "schema" + ], + "properties": { + "cid": { + "type": "string", + "description": "The CID of the lexicon schema record.", + "format": "cid" + }, + "schema": { + "type": "ref", + "description": "The resolved lexicon schema record.", + "ref": "com.atproto.lexicon.schema#main" + }, + "uri": { + "type": "string", + "description": "The AT-URI of the lexicon schema record.", + "format": "at-uri" + } + } + } + }, + "errors": [ + { + "description": "No lexicon was resolved for the NSID.", + "name": "LexiconNotFound" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_colourScheme.json b/crates/weaver-api/lexicons/sh_weaver_notebook_colourScheme.json new file mode 100644 index 0000000..685c064 --- /dev/null +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_colourScheme.json @@ -0,0 +1,120 @@ +{ + "lexicon": 1, + "id": "sh.weaver.notebook.colourScheme", + "defs": { + "main": { + "type": "record", + "description": "A colour palette for notebook theming", + "key": "tid", + "record": { + "type": "object", + "required": [ + "name", + "variant", + "colours" + ], + "properties": { + "colours": { + "type": "object", + "required": [ + "base", + "surface", + "overlay", + "text", + "muted", + "subtle", + "emphasis", + "primary", + "secondary", + "tertiary", + "error", + "warning", + "success", + "border", + "link", + "highlight" + ], + "properties": { + "base": { + "type": "string", + "description": "Primary background for page/frame" + }, + "border": { + "type": "string", + "description": "Border/divider colour" + }, + "emphasis": { + "type": "string", + "description": "Emphasized text (bold, important)" + }, + "error": { + "type": "string", + "description": "Error state colour" + }, + "highlight": { + "type": "string", + "description": "Selection/highlight colour" + }, + "link": { + "type": "string", + "description": "Hyperlink colour" + }, + "muted": { + "type": "string", + "description": "De-emphasized text (disabled, metadata)" + }, + "overlay": { + "type": "string", + "description": "Tertiary background for popovers/dialogs" + }, + "primary": { + "type": "string", + "description": "Primary brand/accent colour" + }, + "secondary": { + "type": "string", + "description": "Secondary accent colour" + }, + "subtle": { + "type": "string", + "description": "Medium emphasis text (comments, labels)" + }, + "success": { + "type": "string", + "description": "Success state colour" + }, + "surface": { + "type": "string", + "description": "Secondary background for panels/cards" + }, + "tertiary": { + "type": "string", + "description": "Tertiary accent colour" + }, + "text": { + "type": "string", + "description": "Primary readable text colour" + }, + "warning": { + "type": "string", + "description": "Warning state colour" + } + } + }, + "name": { + "type": "string", + "description": "Human-readable name for the colour scheme" + }, + "variant": { + "type": "string", + "description": "Whether this is a dark or light colour scheme", + "enum": [ + "dark", + "light" + ] + } + } + } + } + } +} \ No newline at end of file diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json b/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json index c7fea76..dd5a7f7 100644 --- a/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json @@ -36,49 +36,26 @@ "record": { "type": "object", "required": [ - "colours", + "darkScheme", + "lightScheme", "fonts", "spacing", - "codeTheme" + "darkCodeTheme", + "lightCodeTheme" ], "properties": { - "codeTheme": { + "darkCodeTheme": { "type": "union", + "description": "Syntax highlighting theme for dark mode", "refs": [ "#codeThemeName", "#codeThemeFile" ] }, - "colours": { - "type": "object", - "required": [ - "background", - "foreground", - "primary", - "secondary", - "link", - "link_hover" - ], - "properties": { - "background": { - "type": "string" - }, - "foreground": { - "type": "string" - }, - "link": { - "type": "string" - }, - "link_hover": { - "type": "string" - }, - "primary": { - "type": "string" - }, - "secondary": { - "type": "string" - } - } + "darkScheme": { + "type": "ref", + "description": "Reference to a dark colour scheme", + "ref": "com.atproto.repo.strongRef" }, "fonts": { "type": "object", @@ -99,6 +76,19 @@ } } }, + "lightCodeTheme": { + "type": "union", + "description": "Syntax highlighting theme for light mode", + "refs": [ + "#codeThemeName", + "#codeThemeFile" + ] + }, + "lightScheme": { + "type": "ref", + "description": "Reference to a light colour scheme", + "ref": "com.atproto.repo.strongRef" + }, "spacing": { "type": "object", "required": [ diff --git a/crates/weaver-api/src/com_atproto/lexicon.rs b/crates/weaver-api/src/com_atproto/lexicon.rs index afe0ab6..409f66b 100644 --- a/crates/weaver-api/src/com_atproto/lexicon.rs +++ b/crates/weaver-api/src/com_atproto/lexicon.rs @@ -3,4 +3,5 @@ // This file was automatically generated from Lexicon schemas. // Any manual changes will be overwritten on the next regeneration. +pub mod resolve_lexicon; pub mod schema; \ No newline at end of file diff --git a/crates/weaver-api/src/com_atproto/lexicon/resolve_lexicon.rs b/crates/weaver-api/src/com_atproto/lexicon/resolve_lexicon.rs new file mode 100644 index 0000000..9174b35 --- /dev/null +++ b/crates/weaver-api/src/com_atproto/lexicon/resolve_lexicon.rs @@ -0,0 +1,196 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: com.atproto.lexicon.resolveLexicon +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ResolveLexicon<'a> { + #[serde(borrow)] + pub nsid: jacquard_common::types::string::Nsid<'a>, +} + +pub mod resolve_lexicon_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Nsid; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Nsid = Unset; + } + ///State transition - sets the `nsid` field to Set + pub struct SetNsid(PhantomData S>); + impl sealed::Sealed for SetNsid {} + impl State for SetNsid { + type Nsid = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `nsid` field + pub struct nsid(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ResolveLexiconBuilder<'a, S: resolve_lexicon_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ResolveLexicon<'a> { + /// Create a new builder for this type + pub fn new() -> ResolveLexiconBuilder<'a, resolve_lexicon_state::Empty> { + ResolveLexiconBuilder::new() + } +} + +impl<'a> ResolveLexiconBuilder<'a, resolve_lexicon_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ResolveLexiconBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None,), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ResolveLexiconBuilder<'a, S> +where + S: resolve_lexicon_state::State, + S::Nsid: resolve_lexicon_state::IsUnset, +{ + /// Set the `nsid` field (required) + pub fn nsid( + mut self, + value: impl Into>, + ) -> ResolveLexiconBuilder<'a, resolve_lexicon_state::SetNsid> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ResolveLexiconBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ResolveLexiconBuilder<'a, S> +where + S: resolve_lexicon_state::State, + S::Nsid: resolve_lexicon_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ResolveLexicon<'a> { + ResolveLexicon { + nsid: self.__unsafe_private_named.0.unwrap(), + } + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ResolveLexiconOutput<'a> { + /// The CID of the lexicon schema record. + #[serde(borrow)] + pub cid: jacquard_common::types::string::Cid<'a>, + /// The resolved lexicon schema record. + #[serde(borrow)] + pub schema: crate::com_atproto::lexicon::schema::Schema<'a>, + /// The AT-URI of the lexicon schema record. + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, +} + +#[jacquard_derive::open_union] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + thiserror::Error, + miette::Diagnostic, + jacquard_derive::IntoStatic +)] +#[serde(tag = "error", content = "message")] +#[serde(bound(deserialize = "'de: 'a"))] +pub enum ResolveLexiconError<'a> { + /// No lexicon was resolved for the NSID. + #[serde(rename = "LexiconNotFound")] + LexiconNotFound(std::option::Option), +} + +impl std::fmt::Display for ResolveLexiconError<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::LexiconNotFound(msg) => { + write!(f, "LexiconNotFound")?; + if let Some(msg) = msg { + write!(f, ": {}", msg)?; + } + Ok(()) + } + Self::Unknown(err) => write!(f, "Unknown error: {:?}", err), + } + } +} + +/// Response type for +///com.atproto.lexicon.resolveLexicon +pub struct ResolveLexiconResponse; +impl jacquard_common::xrpc::XrpcResp for ResolveLexiconResponse { + const NSID: &'static str = "com.atproto.lexicon.resolveLexicon"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = ResolveLexiconOutput<'de>; + type Err<'de> = ResolveLexiconError<'de>; +} + +impl<'a> jacquard_common::xrpc::XrpcRequest for ResolveLexicon<'a> { + const NSID: &'static str = "com.atproto.lexicon.resolveLexicon"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Response = ResolveLexiconResponse; +} + +/// Endpoint type for +///com.atproto.lexicon.resolveLexicon +pub struct ResolveLexiconRequest; +impl jacquard_common::xrpc::XrpcEndpoint for ResolveLexiconRequest { + const PATH: &'static str = "/xrpc/com.atproto.lexicon.resolveLexicon"; + const METHOD: jacquard_common::xrpc::XrpcMethod = jacquard_common::xrpc::XrpcMethod::Query; + type Request<'de> = ResolveLexicon<'de>; + type Response = ResolveLexiconResponse; +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/notebook.rs b/crates/weaver-api/src/sh_weaver/notebook.rs index 9e0e32f..cd23db0 100644 --- a/crates/weaver-api/src/sh_weaver/notebook.rs +++ b/crates/weaver-api/src/sh_weaver/notebook.rs @@ -8,6 +8,7 @@ pub mod authors; pub mod book; pub mod chapter; +pub mod colour_scheme; pub mod entry; pub mod page; pub mod theme; diff --git a/crates/weaver-api/src/sh_weaver/notebook/colour_scheme.rs b/crates/weaver-api/src/sh_weaver/notebook/colour_scheme.rs new file mode 100644 index 0000000..bdd7df2 --- /dev/null +++ b/crates/weaver-api/src/sh_weaver/notebook/colour_scheme.rs @@ -0,0 +1,784 @@ +// @generated by jacquard-lexicon. DO NOT EDIT. +// +// Lexicon: sh.weaver.notebook.colourScheme +// +// This file was automatically generated from Lexicon schemas. +// Any manual changes will be overwritten on the next regeneration. + +/// A colour palette for notebook theming +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ColourScheme<'a> { + #[serde(borrow)] + pub colours: ColourSchemeColours<'a>, + /// Human-readable name for the colour scheme + #[serde(borrow)] + pub name: jacquard_common::CowStr<'a>, + /// Whether this is a dark or light colour scheme + #[serde(borrow)] + pub variant: jacquard_common::CowStr<'a>, +} + +pub mod colour_scheme_state { + + pub use crate::builder_types::{Set, Unset, IsSet, IsUnset}; + #[allow(unused)] + use ::core::marker::PhantomData; + mod sealed { + pub trait Sealed {} + } + /// State trait tracking which required fields have been set + pub trait State: sealed::Sealed { + type Name; + type Variant; + type Colours; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Name = Unset; + type Variant = Unset; + type Colours = Unset; + } + ///State transition - sets the `name` field to Set + pub struct SetName(PhantomData S>); + impl sealed::Sealed for SetName {} + impl State for SetName { + type Name = Set; + type Variant = S::Variant; + type Colours = S::Colours; + } + ///State transition - sets the `variant` field to Set + pub struct SetVariant(PhantomData S>); + impl sealed::Sealed for SetVariant {} + impl State for SetVariant { + type Name = S::Name; + type Variant = Set; + type Colours = S::Colours; + } + ///State transition - sets the `colours` field to Set + pub struct SetColours(PhantomData S>); + impl sealed::Sealed for SetColours {} + impl State for SetColours { + type Name = S::Name; + type Variant = S::Variant; + type Colours = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `name` field + pub struct name(()); + ///Marker type for the `variant` field + pub struct variant(()); + ///Marker type for the `colours` field + pub struct colours(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ColourSchemeBuilder<'a, S: colour_scheme_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: ( + ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, + ), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> ColourScheme<'a> { + /// Create a new builder for this type + pub fn new() -> ColourSchemeBuilder<'a, colour_scheme_state::Empty> { + ColourSchemeBuilder::new() + } +} + +impl<'a> ColourSchemeBuilder<'a, colour_scheme_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ColourSchemeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ColourSchemeBuilder<'a, S> +where + S: colour_scheme_state::State, + S::Colours: colour_scheme_state::IsUnset, +{ + /// Set the `colours` field (required) + pub fn colours( + mut self, + value: impl Into>, + ) -> ColourSchemeBuilder<'a, colour_scheme_state::SetColours> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ColourSchemeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ColourSchemeBuilder<'a, S> +where + S: colour_scheme_state::State, + S::Name: colour_scheme_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into>, + ) -> ColourSchemeBuilder<'a, colour_scheme_state::SetName> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ColourSchemeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ColourSchemeBuilder<'a, S> +where + S: colour_scheme_state::State, + S::Variant: colour_scheme_state::IsUnset, +{ + /// Set the `variant` field (required) + pub fn variant( + mut self, + value: impl Into>, + ) -> ColourSchemeBuilder<'a, colour_scheme_state::SetVariant> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + ColourSchemeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ColourSchemeBuilder<'a, S> +where + S: colour_scheme_state::State, + S::Name: colour_scheme_state::IsSet, + S::Variant: colour_scheme_state::IsSet, + S::Colours: colour_scheme_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ColourScheme<'a> { + ColourScheme { + colours: self.__unsafe_private_named.0.unwrap(), + name: self.__unsafe_private_named.1.unwrap(), + variant: self.__unsafe_private_named.2.unwrap(), + extra_data: Default::default(), + } + } + /// Build the final struct with custom extra_data + pub fn build_with_data( + self, + extra_data: std::collections::BTreeMap< + jacquard_common::smol_str::SmolStr, + jacquard_common::types::value::Data<'a>, + >, + ) -> ColourScheme<'a> { + ColourScheme { + colours: self.__unsafe_private_named.0.unwrap(), + name: self.__unsafe_private_named.1.unwrap(), + variant: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ColourScheme<'a> { + pub fn uri( + uri: impl Into>, + ) -> Result< + jacquard_common::types::uri::RecordUri<'a, ColourSchemeRecord>, + jacquard_common::types::uri::UriError, + > { + jacquard_common::types::uri::RecordUri::try_from_uri( + jacquard_common::types::string::AtUri::new_cow(uri.into())?, + ) + } +} + +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic, + Default +)] +#[serde(rename_all = "camelCase")] +pub struct ColourSchemeColours<'a> { + /// Primary background for page/frame + #[serde(borrow)] + pub base: jacquard_common::CowStr<'a>, + /// Border/divider colour + #[serde(borrow)] + pub border: jacquard_common::CowStr<'a>, + /// Emphasized text (bold, important) + #[serde(borrow)] + pub emphasis: jacquard_common::CowStr<'a>, + /// Error state colour + #[serde(borrow)] + pub error: jacquard_common::CowStr<'a>, + /// Selection/highlight colour + #[serde(borrow)] + pub highlight: jacquard_common::CowStr<'a>, + /// Hyperlink colour + #[serde(borrow)] + pub link: jacquard_common::CowStr<'a>, + /// De-emphasized text (disabled, metadata) + #[serde(borrow)] + pub muted: jacquard_common::CowStr<'a>, + /// Tertiary background for popovers/dialogs + #[serde(borrow)] + pub overlay: jacquard_common::CowStr<'a>, + /// Primary brand/accent colour + #[serde(borrow)] + pub primary: jacquard_common::CowStr<'a>, + /// Secondary accent colour + #[serde(borrow)] + pub secondary: jacquard_common::CowStr<'a>, + /// Medium emphasis text (comments, labels) + #[serde(borrow)] + pub subtle: jacquard_common::CowStr<'a>, + /// Success state colour + #[serde(borrow)] + pub success: jacquard_common::CowStr<'a>, + /// Secondary background for panels/cards + #[serde(borrow)] + pub surface: jacquard_common::CowStr<'a>, + /// Tertiary accent colour + #[serde(borrow)] + pub tertiary: jacquard_common::CowStr<'a>, + /// Primary readable text colour + #[serde(borrow)] + pub text: jacquard_common::CowStr<'a>, + /// Warning state colour + #[serde(borrow)] + pub warning: jacquard_common::CowStr<'a>, +} + +fn lexicon_doc_sh_weaver_notebook_colourScheme() -> ::jacquard_lexicon::lexicon::LexiconDoc< + 'static, +> { + ::jacquard_lexicon::lexicon::LexiconDoc { + lexicon: ::jacquard_lexicon::lexicon::Lexicon::Lexicon1, + id: ::jacquard_common::CowStr::new_static("sh.weaver.notebook.colourScheme"), + revision: None, + description: None, + defs: { + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("main"), + ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { + description: Some( + ::jacquard_common::CowStr::new_static( + "A colour palette for notebook theming", + ), + ), + key: Some(::jacquard_common::CowStr::new_static("tid")), + record: ::jacquard_lexicon::lexicon::LexRecordRecord::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("name"), + ::jacquard_common::smol_str::SmolStr::new_static("variant"), + ::jacquard_common::smol_str::SmolStr::new_static("colours") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("colours"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("base"), + ::jacquard_common::smol_str::SmolStr::new_static("surface"), + ::jacquard_common::smol_str::SmolStr::new_static("overlay"), + ::jacquard_common::smol_str::SmolStr::new_static("text"), + ::jacquard_common::smol_str::SmolStr::new_static("muted"), + ::jacquard_common::smol_str::SmolStr::new_static("subtle"), + ::jacquard_common::smol_str::SmolStr::new_static("emphasis"), + ::jacquard_common::smol_str::SmolStr::new_static("primary"), + ::jacquard_common::smol_str::SmolStr::new_static("secondary"), + ::jacquard_common::smol_str::SmolStr::new_static("tertiary"), + ::jacquard_common::smol_str::SmolStr::new_static("error"), + ::jacquard_common::smol_str::SmolStr::new_static("warning"), + ::jacquard_common::smol_str::SmolStr::new_static("success"), + ::jacquard_common::smol_str::SmolStr::new_static("border"), + ::jacquard_common::smol_str::SmolStr::new_static("link"), + ::jacquard_common::smol_str::SmolStr::new_static("highlight") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("base"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Primary background for page/frame", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("border"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Border/divider colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "emphasis", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Emphasized text (bold, important)", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("error"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static("Error state colour"), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "highlight", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Selection/highlight colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("link"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static("Hyperlink colour"), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("muted"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "De-emphasized text (disabled, metadata)", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("overlay"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Tertiary background for popovers/dialogs", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("primary"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Primary brand/accent colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "secondary", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Secondary accent colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("subtle"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Medium emphasis text (comments, labels)", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("success"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Success state colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("surface"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Secondary background for panels/cards", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "tertiary", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Tertiary accent colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("text"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Primary readable text colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("warning"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Warning state colour", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("name"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Human-readable name for the colour scheme", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("variant"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: Some( + ::jacquard_common::CowStr::new_static( + "Whether this is a dark or light colour scheme", + ), + ), + format: None, + default: None, + min_length: None, + max_length: None, + min_graphemes: None, + max_graphemes: None, + r#enum: None, + r#const: None, + known_values: None, + }), + ); + map + }, + }), + }), + ); + map + }, + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ColourSchemeColours<'a> { + fn nsid() -> &'static str { + "sh.weaver.notebook.colourScheme" + } + fn def_name() -> &'static str { + "ColourSchemeColours" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_notebook_colourScheme() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} + +/// Typed wrapper for GetRecord response with this collection's record type. +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct ColourSchemeGetRecordOutput<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub cid: std::option::Option>, + #[serde(borrow)] + pub uri: jacquard_common::types::string::AtUri<'a>, + #[serde(borrow)] + pub value: ColourScheme<'a>, +} + +impl From> for ColourScheme<'_> { + fn from(output: ColourSchemeGetRecordOutput<'_>) -> Self { + use jacquard_common::IntoStatic; + output.value.into_static() + } +} + +impl jacquard_common::types::collection::Collection for ColourScheme<'_> { + const NSID: &'static str = "sh.weaver.notebook.colourScheme"; + type Record = ColourSchemeRecord; +} + +/// Marker type for deserializing records from this collection. +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct ColourSchemeRecord; +impl jacquard_common::xrpc::XrpcResp for ColourSchemeRecord { + const NSID: &'static str = "sh.weaver.notebook.colourScheme"; + const ENCODING: &'static str = "application/json"; + type Output<'de> = ColourSchemeGetRecordOutput<'de>; + type Err<'de> = jacquard_common::types::collection::RecordError<'de>; +} + +impl jacquard_common::types::collection::Collection for ColourSchemeRecord { + const NSID: &'static str = "sh.weaver.notebook.colourScheme"; + type Record = ColourSchemeRecord; +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ColourScheme<'a> { + fn nsid() -> &'static str { + "sh.weaver.notebook.colourScheme" + } + fn def_name() -> &'static str { + "main" + } + fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { + lexicon_doc_sh_weaver_notebook_colourScheme() + } + fn validate( + &self, + ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { + Ok(()) + } +} \ No newline at end of file diff --git a/crates/weaver-api/src/sh_weaver/notebook/theme.rs b/crates/weaver-api/src/sh_weaver/notebook/theme.rs index ed54c2e..1cc5b05 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/theme.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/theme.rs @@ -297,10 +297,12 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico description: None, required: Some( vec![ - ::jacquard_common::smol_str::SmolStr::new_static("colours"), + ::jacquard_common::smol_str::SmolStr::new_static("darkScheme"), + ::jacquard_common::smol_str::SmolStr::new_static("lightScheme"), ::jacquard_common::smol_str::SmolStr::new_static("fonts"), ::jacquard_common::smol_str::SmolStr::new_static("spacing"), - ::jacquard_common::smol_str::SmolStr::new_static("codeTheme") + ::jacquard_common::smol_str::SmolStr::new_static("darkCodeTheme"), + ::jacquard_common::smol_str::SmolStr::new_static("lightCodeTheme") ], ), nullable: None, @@ -309,10 +311,14 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico let mut map = ::std::collections::BTreeMap::new(); map.insert( ::jacquard_common::smol_str::SmolStr::new_static( - "codeTheme", + "darkCodeTheme", ), ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { - description: None, + description: Some( + ::jacquard_common::CowStr::new_static( + "Syntax highlighting theme for dark mode", + ), + ), refs: vec![ ::jacquard_common::CowStr::new_static("#codeThemeName"), ::jacquard_common::CowStr::new_static("#codeThemeFile") @@ -321,123 +327,14 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico }), ); map.insert( - ::jacquard_common::smol_str::SmolStr::new_static("colours"), - ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { + ::jacquard_common::smol_str::SmolStr::new_static( + "darkScheme", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { description: None, - required: Some( - vec![ - ::jacquard_common::smol_str::SmolStr::new_static("background"), - ::jacquard_common::smol_str::SmolStr::new_static("foreground"), - ::jacquard_common::smol_str::SmolStr::new_static("primary"), - ::jacquard_common::smol_str::SmolStr::new_static("secondary"), - ::jacquard_common::smol_str::SmolStr::new_static("link"), - ::jacquard_common::smol_str::SmolStr::new_static("link_hover") - ], + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", ), - nullable: None, - properties: { - #[allow(unused_mut)] - let mut map = ::std::collections::BTreeMap::new(); - map.insert( - ::jacquard_common::smol_str::SmolStr::new_static( - "background", - ), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { - description: None, - format: None, - default: None, - min_length: None, - max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, - }), - ); - map.insert( - ::jacquard_common::smol_str::SmolStr::new_static( - "foreground", - ), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { - description: None, - format: None, - default: None, - min_length: None, - max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, - }), - ); - map.insert( - ::jacquard_common::smol_str::SmolStr::new_static("link"), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { - description: None, - format: None, - default: None, - min_length: None, - max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, - }), - ); - map.insert( - ::jacquard_common::smol_str::SmolStr::new_static( - "link_hover", - ), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { - description: None, - format: None, - default: None, - min_length: None, - max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, - }), - ); - map.insert( - ::jacquard_common::smol_str::SmolStr::new_static("primary"), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { - description: None, - format: None, - default: None, - min_length: None, - max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, - }), - ); - map.insert( - ::jacquard_common::smol_str::SmolStr::new_static( - "secondary", - ), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { - description: None, - format: None, - default: None, - min_length: None, - max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, - }), - ); - map - }, }), ); map.insert( @@ -506,6 +403,34 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico }, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "lightCodeTheme", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { + description: Some( + ::jacquard_common::CowStr::new_static( + "Syntax highlighting theme for light mode", + ), + ), + refs: vec![ + ::jacquard_common::CowStr::new_static("#codeThemeName"), + ::jacquard_common::CowStr::new_static("#codeThemeFile") + ], + closed: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static( + "lightScheme", + ), + ::jacquard_lexicon::lexicon::LexObjectProperty::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "com.atproto.repo.strongRef", + ), + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("spacing"), ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { @@ -615,12 +540,20 @@ pub type CodeThemeName<'a> = jacquard_common::CowStr<'a>; )] #[serde(rename_all = "camelCase")] pub struct Theme<'a> { + /// Syntax highlighting theme for dark mode #[serde(borrow)] - pub code_theme: ThemeCodeTheme<'a>, + pub dark_code_theme: ThemeDarkCodeTheme<'a>, + /// Reference to a dark colour scheme #[serde(borrow)] - pub colours: ThemeColours<'a>, + pub dark_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(borrow)] pub fonts: ThemeFonts<'a>, + /// Syntax highlighting theme for light mode + #[serde(borrow)] + pub light_code_theme: ThemeLightCodeTheme<'a>, + /// Reference to a light colour scheme + #[serde(borrow)] + pub light_scheme: crate::com_atproto::repo::strong_ref::StrongRef<'a>, #[serde(borrow)] pub spacing: ThemeSpacing<'a>, } @@ -635,67 +568,105 @@ pub mod theme_state { } /// State trait tracking which required fields have been set pub trait State: sealed::Sealed { - type Colours; + type DarkScheme; + type LightScheme; type Fonts; type Spacing; - type CodeTheme; + type DarkCodeTheme; + type LightCodeTheme; } /// Empty state - all required fields are unset pub struct Empty(()); impl sealed::Sealed for Empty {} impl State for Empty { - type Colours = Unset; + type DarkScheme = Unset; + type LightScheme = Unset; type Fonts = Unset; type Spacing = Unset; - type CodeTheme = Unset; + type DarkCodeTheme = Unset; + type LightCodeTheme = Unset; + } + ///State transition - sets the `dark_scheme` field to Set + pub struct SetDarkScheme(PhantomData S>); + impl sealed::Sealed for SetDarkScheme {} + impl State for SetDarkScheme { + type DarkScheme = Set; + type LightScheme = S::LightScheme; + type Fonts = S::Fonts; + type Spacing = S::Spacing; + type DarkCodeTheme = S::DarkCodeTheme; + type LightCodeTheme = S::LightCodeTheme; } - ///State transition - sets the `colours` field to Set - pub struct SetColours(PhantomData S>); - impl sealed::Sealed for SetColours {} - impl State for SetColours { - type Colours = Set; + ///State transition - sets the `light_scheme` field to Set + pub struct SetLightScheme(PhantomData S>); + impl sealed::Sealed for SetLightScheme {} + impl State for SetLightScheme { + type DarkScheme = S::DarkScheme; + type LightScheme = Set; type Fonts = S::Fonts; type Spacing = S::Spacing; - type CodeTheme = S::CodeTheme; + type DarkCodeTheme = S::DarkCodeTheme; + type LightCodeTheme = S::LightCodeTheme; } ///State transition - sets the `fonts` field to Set pub struct SetFonts(PhantomData S>); impl sealed::Sealed for SetFonts {} impl State for SetFonts { - type Colours = S::Colours; + type DarkScheme = S::DarkScheme; + type LightScheme = S::LightScheme; type Fonts = Set; type Spacing = S::Spacing; - type CodeTheme = S::CodeTheme; + type DarkCodeTheme = S::DarkCodeTheme; + type LightCodeTheme = S::LightCodeTheme; } ///State transition - sets the `spacing` field to Set pub struct SetSpacing(PhantomData S>); impl sealed::Sealed for SetSpacing {} impl State for SetSpacing { - type Colours = S::Colours; + type DarkScheme = S::DarkScheme; + type LightScheme = S::LightScheme; type Fonts = S::Fonts; type Spacing = Set; - type CodeTheme = S::CodeTheme; + type DarkCodeTheme = S::DarkCodeTheme; + type LightCodeTheme = S::LightCodeTheme; + } + ///State transition - sets the `dark_code_theme` field to Set + pub struct SetDarkCodeTheme(PhantomData S>); + impl sealed::Sealed for SetDarkCodeTheme {} + impl State for SetDarkCodeTheme { + type DarkScheme = S::DarkScheme; + type LightScheme = S::LightScheme; + type Fonts = S::Fonts; + type Spacing = S::Spacing; + type DarkCodeTheme = Set; + type LightCodeTheme = S::LightCodeTheme; } - ///State transition - sets the `code_theme` field to Set - pub struct SetCodeTheme(PhantomData S>); - impl sealed::Sealed for SetCodeTheme {} - impl State for SetCodeTheme { - type Colours = S::Colours; + ///State transition - sets the `light_code_theme` field to Set + pub struct SetLightCodeTheme(PhantomData S>); + impl sealed::Sealed for SetLightCodeTheme {} + impl State for SetLightCodeTheme { + type DarkScheme = S::DarkScheme; + type LightScheme = S::LightScheme; type Fonts = S::Fonts; type Spacing = S::Spacing; - type CodeTheme = Set; + type DarkCodeTheme = S::DarkCodeTheme; + type LightCodeTheme = Set; } /// Marker types for field names #[allow(non_camel_case_types)] pub mod members { - ///Marker type for the `colours` field - pub struct colours(()); + ///Marker type for the `dark_scheme` field + pub struct dark_scheme(()); + ///Marker type for the `light_scheme` field + pub struct light_scheme(()); ///Marker type for the `fonts` field pub struct fonts(()); ///Marker type for the `spacing` field pub struct spacing(()); - ///Marker type for the `code_theme` field - pub struct code_theme(()); + ///Marker type for the `dark_code_theme` field + pub struct dark_code_theme(()); + ///Marker type for the `light_code_theme` field + pub struct light_code_theme(()); } } @@ -703,9 +674,11 @@ pub mod theme_state { pub struct ThemeBuilder<'a, S: theme_state::State> { _phantom_state: ::core::marker::PhantomData S>, __unsafe_private_named: ( - ::core::option::Option>, - ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, ::core::option::Option>, + ::core::option::Option>, + ::core::option::Option>, ::core::option::Option>, ), _phantom: ::core::marker::PhantomData<&'a ()>, @@ -723,7 +696,7 @@ impl<'a> ThemeBuilder<'a, theme_state::Empty> { pub fn new() -> Self { ThemeBuilder { _phantom_state: ::core::marker::PhantomData, - __unsafe_private_named: (None, None, None, None), + __unsafe_private_named: (None, None, None, None, None, None), _phantom: ::core::marker::PhantomData, } } @@ -732,13 +705,13 @@ impl<'a> ThemeBuilder<'a, theme_state::Empty> { impl<'a, S> ThemeBuilder<'a, S> where S: theme_state::State, - S::CodeTheme: theme_state::IsUnset, + S::DarkCodeTheme: theme_state::IsUnset, { - /// Set the `codeTheme` field (required) - pub fn code_theme( + /// Set the `darkCodeTheme` field (required) + pub fn dark_code_theme( mut self, - value: impl Into>, - ) -> ThemeBuilder<'a, theme_state::SetCodeTheme> { + value: impl Into>, + ) -> ThemeBuilder<'a, theme_state::SetDarkCodeTheme> { self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); ThemeBuilder { _phantom_state: ::core::marker::PhantomData, @@ -751,13 +724,13 @@ where impl<'a, S> ThemeBuilder<'a, S> where S: theme_state::State, - S::Colours: theme_state::IsUnset, + S::DarkScheme: theme_state::IsUnset, { - /// Set the `colours` field (required) - pub fn colours( + /// Set the `darkScheme` field (required) + pub fn dark_scheme( mut self, - value: impl Into>, - ) -> ThemeBuilder<'a, theme_state::SetColours> { + value: impl Into>, + ) -> ThemeBuilder<'a, theme_state::SetDarkScheme> { self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); ThemeBuilder { _phantom_state: ::core::marker::PhantomData, @@ -786,6 +759,44 @@ where } } +impl<'a, S> ThemeBuilder<'a, S> +where + S: theme_state::State, + S::LightCodeTheme: theme_state::IsUnset, +{ + /// Set the `lightCodeTheme` field (required) + pub fn light_code_theme( + mut self, + value: impl Into>, + ) -> ThemeBuilder<'a, theme_state::SetLightCodeTheme> { + self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); + ThemeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ThemeBuilder<'a, S> +where + S: theme_state::State, + S::LightScheme: theme_state::IsUnset, +{ + /// Set the `lightScheme` field (required) + pub fn light_scheme( + mut self, + value: impl Into>, + ) -> ThemeBuilder<'a, theme_state::SetLightScheme> { + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); + ThemeBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + impl<'a, S> ThemeBuilder<'a, S> where S: theme_state::State, @@ -796,7 +807,7 @@ where mut self, value: impl Into>, ) -> ThemeBuilder<'a, theme_state::SetSpacing> { - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); ThemeBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -808,18 +819,22 @@ where impl<'a, S> ThemeBuilder<'a, S> where S: theme_state::State, - S::Colours: theme_state::IsSet, + S::DarkScheme: theme_state::IsSet, + S::LightScheme: theme_state::IsSet, S::Fonts: theme_state::IsSet, S::Spacing: theme_state::IsSet, - S::CodeTheme: theme_state::IsSet, + S::DarkCodeTheme: theme_state::IsSet, + S::LightCodeTheme: theme_state::IsSet, { /// Build the final struct pub fn build(self) -> Theme<'a> { Theme { - code_theme: self.__unsafe_private_named.0.unwrap(), - colours: self.__unsafe_private_named.1.unwrap(), + dark_code_theme: self.__unsafe_private_named.0.unwrap(), + dark_scheme: self.__unsafe_private_named.1.unwrap(), fonts: self.__unsafe_private_named.2.unwrap(), - spacing: self.__unsafe_private_named.3.unwrap(), + light_code_theme: self.__unsafe_private_named.3.unwrap(), + light_scheme: self.__unsafe_private_named.4.unwrap(), + spacing: self.__unsafe_private_named.5.unwrap(), extra_data: Default::default(), } } @@ -832,10 +847,12 @@ where >, ) -> Theme<'a> { Theme { - code_theme: self.__unsafe_private_named.0.unwrap(), - colours: self.__unsafe_private_named.1.unwrap(), + dark_code_theme: self.__unsafe_private_named.0.unwrap(), + dark_scheme: self.__unsafe_private_named.1.unwrap(), fonts: self.__unsafe_private_named.2.unwrap(), - spacing: self.__unsafe_private_named.3.unwrap(), + light_code_theme: self.__unsafe_private_named.3.unwrap(), + light_scheme: self.__unsafe_private_named.4.unwrap(), + spacing: self.__unsafe_private_named.5.unwrap(), extra_data: Some(extra_data), } } @@ -866,7 +883,7 @@ impl<'a> Theme<'a> { )] #[serde(tag = "$type")] #[serde(bound(deserialize = "'de: 'a"))] -pub enum ThemeCodeTheme<'a> { +pub enum ThemeDarkCodeTheme<'a> { #[serde(rename = "sh.weaver.notebook.theme#codeThemeName")] CodeThemeName(Box>), #[serde(rename = "sh.weaver.notebook.theme#codeThemeFile")] @@ -885,27 +902,21 @@ pub enum ThemeCodeTheme<'a> { Default )] #[serde(rename_all = "camelCase")] -pub struct ThemeColours<'a> { - #[serde(borrow)] - pub background: jacquard_common::CowStr<'a>, - #[serde(borrow)] - pub foreground: jacquard_common::CowStr<'a>, - #[serde(borrow)] - pub link: jacquard_common::CowStr<'a>, +pub struct ThemeFonts<'a> { #[serde(borrow)] - pub link_hover: jacquard_common::CowStr<'a>, + pub body: jacquard_common::CowStr<'a>, #[serde(borrow)] - pub primary: jacquard_common::CowStr<'a>, + pub heading: jacquard_common::CowStr<'a>, #[serde(borrow)] - pub secondary: jacquard_common::CowStr<'a>, + pub monospace: jacquard_common::CowStr<'a>, } -impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeColours<'a> { +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeFonts<'a> { fn nsid() -> &'static str { "sh.weaver.notebook.theme" } fn def_name() -> &'static str { - "ThemeColours" + "ThemeFonts" } fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { lexicon_doc_sh_weaver_notebook_theme() @@ -917,7 +928,7 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeColours<'a> { } } -#[jacquard_derive::lexicon] +#[jacquard_derive::open_union] #[derive( serde::Serialize, serde::Deserialize, @@ -925,34 +936,15 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeColours<'a> { Clone, PartialEq, Eq, - jacquard_derive::IntoStatic, - Default + jacquard_derive::IntoStatic )] -#[serde(rename_all = "camelCase")] -pub struct ThemeFonts<'a> { - #[serde(borrow)] - pub body: jacquard_common::CowStr<'a>, - #[serde(borrow)] - pub heading: jacquard_common::CowStr<'a>, - #[serde(borrow)] - pub monospace: jacquard_common::CowStr<'a>, -} - -impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeFonts<'a> { - fn nsid() -> &'static str { - "sh.weaver.notebook.theme" - } - fn def_name() -> &'static str { - "ThemeFonts" - } - fn lexicon_doc() -> ::jacquard_lexicon::lexicon::LexiconDoc<'static> { - lexicon_doc_sh_weaver_notebook_theme() - } - fn validate( - &self, - ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { - Ok(()) - } +#[serde(tag = "$type")] +#[serde(bound(deserialize = "'de: 'a"))] +pub enum ThemeLightCodeTheme<'a> { + #[serde(rename = "sh.weaver.notebook.theme#codeThemeName")] + CodeThemeName(Box>), + #[serde(rename = "sh.weaver.notebook.theme#codeThemeFile")] + CodeThemeFile(Box>), } #[jacquard_derive::lexicon] diff --git a/crates/weaver-common/src/lib.rs b/crates/weaver-common/src/lib.rs index c1cd327..4fcc173 100644 --- a/crates/weaver-common/src/lib.rs +++ b/crates/weaver-common/src/lib.rs @@ -129,7 +129,12 @@ pub trait WeaverExt: AgentSessionExt { &self, ident: &jacquard::types::ident::AtIdentifier<'_>, title: &str, - ) -> impl Future, Vec>)>, WeaverError>>; + ) -> impl Future< + Output = Result< + Option<(view::NotebookView<'static>, Vec>)>, + WeaverError, + >, + >; } impl WeaverExt for Agent { @@ -144,10 +149,8 @@ impl WeaverExt for Agent { url_path: &'a str, prev: Option, ) -> Result<(StrongRef<'a>, PublishedBlob<'a>), WeaverError> { - let mime_type = MimeType::new_owned( - blob.sniff_mime_type() - .unwrap_or("applicaction/octet-stream"), - ); + let mime_type = + MimeType::new_owned(blob.sniff_mime_type().unwrap_or("application/octet-stream")); let blob = self.upload_blob(blob, mime_type).await?; let publish_record = PublishedBlob::new() @@ -195,7 +198,9 @@ impl WeaverExt for Agent { if let Ok(list) = resp.parse() { for record in list.records { let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { - AgentError::from(ClientError::invalid_request("Failed to parse notebook record")) + AgentError::from(ClientError::invalid_request( + "Failed to parse notebook record", + )) })?; if let Some(book_title) = notebook.title && book_title == title @@ -265,10 +270,7 @@ impl WeaverExt for Agent { // Add to notebook's entry_list use weaver_api::sh_weaver::notebook::book::Book; - let new_ref = StrongRef::new() - .uri(response.uri) - .cid(response.cid) - .build(); + let new_ref = StrongRef::new().uri(response.uri).cid(response.cid).build(); self.update_record::(¬ebook_uri, |book| { book.entry_list.push(new_ref); @@ -282,10 +284,10 @@ impl WeaverExt for Agent { &self, uri: &AtUri<'_>, ) -> Result<(view::NotebookView<'static>, Vec>), WeaverError> { + use jacquard::to_data; use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; - use weaver_api::sh_weaver::notebook::book::Book; use weaver_api::sh_weaver::notebook::AuthorListView; - use jacquard::to_data; + use weaver_api::sh_weaver::notebook::book::Book; let notebook = self .get_record::(uri) @@ -302,15 +304,20 @@ impl WeaverExt for Agent { let mut authors = Vec::new(); for (index, author) in notebook.value.authors.iter().enumerate() { - let author_uri = BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", author.did)) - .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid author profile URI")))?; + let author_uri = + BskyProfile::uri(format!("at://{}/app.bsky.actor.profile/self", author.did)) + .map_err(|_| { + AgentError::from(ClientError::invalid_request("Invalid author profile URI")) + })?; let author_profile = self.fetch_record(&author_uri).await?; authors.push( AuthorListView::new() .uri(author_uri.as_uri().clone()) .record(to_data(&author_profile).map_err(|_| { - AgentError::from(ClientError::invalid_request("Failed to serialize author profile")) + AgentError::from(ClientError::invalid_request( + "Failed to serialize author profile", + )) })?) .index(index as i64) .build(), @@ -347,8 +354,8 @@ impl WeaverExt for Agent { notebook: &view::NotebookView<'a>, entry_ref: &StrongRef<'_>, ) -> Result, WeaverError> { - use weaver_api::sh_weaver::notebook::entry::Entry; use jacquard::to_data; + use weaver_api::sh_weaver::notebook::entry::Entry; let entry_uri = Entry::uri(entry_ref.uri.clone()) .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid entry URI")))?; @@ -378,8 +385,8 @@ impl WeaverExt for Agent { entries: &[StrongRef<'_>], title: &str, ) -> Result, entry::Entry<'a>)>, WeaverError> { - use weaver_api::sh_weaver::notebook::entry::Entry; use weaver_api::sh_weaver::notebook::BookEntryRef; + use weaver_api::sh_weaver::notebook::entry::Entry; for (index, entry_ref) in entries.iter().enumerate() { let resp = self @@ -426,25 +433,29 @@ impl WeaverExt for Agent { ident: &jacquard::types::ident::AtIdentifier<'_>, title: &str, ) -> Result, Vec>)>, WeaverError> { + use jacquard::to_data; use jacquard::types::collection::Collection; use jacquard::types::nsid::Nsid; use jacquard::xrpc::XrpcExt; - use weaver_api::com_atproto::repo::list_records::ListRecords; - use weaver_api::sh_weaver::notebook::book::Book; use weaver_api::app_bsky::actor::profile::Profile as BskyProfile; + use weaver_api::com_atproto::repo::list_records::ListRecords; use weaver_api::sh_weaver::notebook::AuthorListView; - use jacquard::to_data; + use weaver_api::sh_weaver::notebook::book::Book; let (repo_did, pds_url) = match ident { jacquard::types::ident::AtIdentifier::Did(did) => { let pds = self.pds_for_did(did).await.map_err(|e| { - AgentError::from(ClientError::from(e).with_context("Failed to resolve PDS for DID")) + AgentError::from( + ClientError::from(e).with_context("Failed to resolve PDS for DID"), + ) })?; (did.clone(), pds) } - jacquard::types::ident::AtIdentifier::Handle(handle) => self.pds_for_handle(handle).await.map_err(|e| { - AgentError::from(ClientError::from(e).with_context("Failed to resolve handle")) - })?, + jacquard::types::ident::AtIdentifier::Handle(handle) => { + self.pds_for_handle(handle).await.map_err(|e| { + AgentError::from(ClientError::from(e).with_context("Failed to resolve handle")) + })? + } }; // TODO: use the cursor to search through all records with this NSID for the repo @@ -463,7 +474,9 @@ impl WeaverExt for Agent { if let Ok(list) = resp.parse() { for record in list.records { let notebook: Book = jacquard::from_data(&record.value).map_err(|_| { - AgentError::from(ClientError::invalid_request("Failed to parse notebook record")) + AgentError::from(ClientError::invalid_request( + "Failed to parse notebook record", + )) })?; if let Some(book_title) = notebook.title && book_title == title @@ -477,14 +490,20 @@ impl WeaverExt for Agent { "at://{}/app.bsky.actor.profile/self", author.did )) - .map_err(|_| AgentError::from(ClientError::invalid_request("Invalid author profile URI")))?; + .map_err(|_| { + AgentError::from(ClientError::invalid_request( + "Invalid author profile URI", + )) + })?; let author_profile = self.fetch_record(&author_uri).await?; authors.push( AuthorListView::new() .uri(author_uri.as_uri().clone()) .record(to_data(&author_profile).map_err(|_| { - AgentError::from(ClientError::invalid_request("Failed to serialize author profile")) + AgentError::from(ClientError::invalid_request( + "Failed to serialize author profile", + )) })?) .index(index as i64) .build(), diff --git a/crates/weaver-renderer/src/atproto/writer.rs b/crates/weaver-renderer/src/atproto/writer.rs index bf3443d..ff1523c 100644 --- a/crates/weaver-renderer/src/atproto/writer.rs +++ b/crates/weaver-renderer/src/atproto/writer.rs @@ -26,7 +26,8 @@ impl EmbedContentProvider for () { /// /// This writer is designed for client-side rendering where embeds may have /// pre-rendered content in their attributes. -pub struct ClientWriter { +pub struct ClientWriter<'a, I: Iterator>, W: StrWrite, E = ()> { + events: I, writer: W, end_newline: bool, in_non_writing_block: bool, @@ -40,6 +41,7 @@ pub struct ClientWriter { embed_provider: Option, code_buffer: Option<(Option, String)>, // (lang, content) + _phantom: std::marker::PhantomData<&'a ()>, } #[derive(Debug, Clone, Copy)] @@ -48,9 +50,10 @@ enum TableState { Body, } -impl ClientWriter { - pub fn new(writer: W) -> Self { +impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { + pub fn new(events: I, writer: W) -> Self { Self { + events, writer, end_newline: true, in_non_writing_block: false, @@ -60,12 +63,14 @@ impl ClientWriter { numbers: HashMap::new(), embed_provider: None, code_buffer: None, + _phantom: std::marker::PhantomData, } } /// Add an embed content provider - pub fn with_embed_provider(self, provider: E) -> ClientWriter { + pub fn with_embed_provider(self, provider: E) -> ClientWriter<'a, I, W, E> { ClientWriter { + events: self.events, writer: self.writer, end_newline: self.end_newline, in_non_writing_block: self.in_non_writing_block, @@ -75,11 +80,9 @@ impl ClientWriter { numbers: self.numbers, embed_provider: Some(provider), code_buffer: self.code_buffer, + _phantom: std::marker::PhantomData, } } -} - -impl ClientWriter { #[inline] fn write_newline(&mut self) -> Result<(), W::Error> { self.end_newline = true; @@ -96,13 +99,59 @@ impl ClientWriter { } /// Process markdown events and write HTML - pub fn run<'a>(mut self, events: impl Iterator>) -> Result { - for event in events { + pub fn run(mut self) -> Result { + while let Some(event) = self.events.next() { self.process_event(event)?; } Ok(self.writer) } + // Consume raw text events until end tag, for alt attributes + fn raw_text(&mut self) -> Result<(), W::Error> { + use Event::*; + let mut nest = 0; + while let Some(event) = self.events.next() { + match event { + Start(_) => nest += 1, + End(_) => { + if nest == 0 { + break; + } + nest -= 1; + } + Html(_) => {} + InlineHtml(text) | Code(text) | Text(text) => { + // Don't use escape_html_body_text here. + // The output of this function is used in the `alt` attribute. + escape_html(&mut self.writer, &text)?; + self.end_newline = text.ends_with('\n'); + } + InlineMath(text) => { + self.write("$")?; + escape_html(&mut self.writer, &text)?; + self.write("$")?; + } + DisplayMath(text) => { + self.write("$$")?; + escape_html(&mut self.writer, &text)?; + self.write("$$")?; + } + SoftBreak | HardBreak | Rule => { + self.write(" ")?; + } + FootnoteReference(name) => { + let len = self.numbers.len() + 1; + let number = *self.numbers.entry(name.into_string()).or_insert(len); + write!(&mut self.writer, "[{}]", number)?; + } + TaskListMarker(true) => self.write("[x]")?, + TaskListMarker(false) => self.write("[ ]")?, + WeaverBlock(_) => {} + } + } + Ok(()) + } + fn process_event(&mut self, event: Event<'_>) -> Result<(), W::Error> { use Event::*; match event { @@ -368,20 +417,25 @@ impl ClientWriter { self.write("\"")?; 0 { self.write(" ")?; } escape_html(&mut self.writer, class)?; } + self.write("\"")?; } - self.write("\"")?; for (attr, value) in &attrs.attrs { self.write(" ")?; escape_html(&mut self.writer, attr)?; @@ -389,8 +443,6 @@ impl ClientWriter { escape_html(&mut self.writer, value)?; self.write("\"")?; } - } else { - self.write("\"")?; } self.write(" />") } @@ -501,7 +553,7 @@ impl ClientWriter { TagEnd::Strong => self.write(""), TagEnd::Strikethrough => self.write(""), TagEnd::Link => self.write(""), - TagEnd::Image => Ok(()), + TagEnd::Image => Ok(()), // No-op: raw_text() already consumed the End(Image) event TagEnd::Embed => Ok(()), TagEnd::WeaverBlock(_) => { self.in_non_writing_block = false; @@ -516,7 +568,7 @@ impl ClientWriter { } } -impl ClientWriter { +impl<'a, I: Iterator>, W: StrWrite, E: EmbedContentProvider> ClientWriter<'a, I, W, E> { fn write_embed( &mut self, embed_type: EmbedType, diff --git a/crates/weaver-renderer/src/code_pretty.rs b/crates/weaver-renderer/src/code_pretty.rs index 28fbde5..9b3e4a8 100644 --- a/crates/weaver-renderer/src/code_pretty.rs +++ b/crates/weaver-renderer/src/code_pretty.rs @@ -27,7 +27,7 @@ where .find_syntax_by_first_line(code.as_ref()) .unwrap_or_else(|| syn_set.find_syntax_plain_text()) }; - writer.write_str("
")?;
 
diff --git a/crates/weaver-renderer/src/css.rs b/crates/weaver-renderer/src/css.rs
index 9f3846e..7dece78 100644
--- a/crates/weaver-renderer/src/css.rs
+++ b/crates/weaver-renderer/src/css.rs
@@ -1,10 +1,9 @@
-use crate::theme::Theme;
+use crate::theme::{ResolvedTheme, ThemeDarkCodeTheme, ThemeLightCodeTheme};
 use miette::IntoDiagnostic;
 use std::io::Cursor;
 use syntect::highlighting::ThemeSet;
 use syntect::html::{ClassStyle, css_for_theme_with_class_style};
 use weaver_api::com_atproto::sync::get_blob::GetBlob;
-use weaver_api::sh_weaver::notebook::theme::ThemeCodeTheme;
 use weaver_common::jacquard::client::BasicClient;
 use weaver_common::jacquard::prelude::*;
 use weaver_common::jacquard::xrpc::XrpcExt;
@@ -13,7 +12,12 @@ use weaver_common::jacquard::xrpc::XrpcExt;
 const ROSE_PINE_THEME: &str = include_str!("../themes/rose-pine.tmTheme");
 const ROSE_PINE_DAWN_THEME: &str = include_str!("../themes/rose-pine-dawn.tmTheme");
 
-pub fn generate_base_css(theme: &Theme) -> String {
+pub fn generate_base_css(theme: &ResolvedTheme) -> String {
+    let dark = &theme.dark_scheme;
+    let light = &theme.light_scheme;
+    let fonts = &theme.fonts;
+    let spacing = &theme.spacing;
+
     format!(
         r#"/* CSS Reset */
 *, *::before, *::after {{
@@ -22,14 +26,24 @@ pub fn generate_base_css(theme: &Theme) -> String {
     padding: 0;
 }}
 
-/* CSS Variables */
+/* CSS Variables - Light Mode (default) */
 :root {{
-    --color-background: {};
-    --color-foreground: {};
-    --color-link: {};
-    --color-link-hover: {};
+    --color-base: {};
+    --color-surface: {};
+    --color-overlay: {};
+    --color-text: {};
+    --color-muted: {};
+    --color-subtle: {};
+    --color-emphasis: {};
     --color-primary: {};
     --color-secondary: {};
+    --color-tertiary: {};
+    --color-error: {};
+    --color-warning: {};
+    --color-success: {};
+    --color-border: {};
+    --color-link: {};
+    --color-highlight: {};
 
     --font-body: {};
     --font-heading: {};
@@ -40,6 +54,28 @@ pub fn generate_base_css(theme: &Theme) -> String {
     --spacing-scale: {};
 }}
 
+/* CSS Variables - Dark Mode */
+@media (prefers-color-scheme: dark) {{
+    :root {{
+        --color-base: {};
+        --color-surface: {};
+        --color-overlay: {};
+        --color-text: {};
+        --color-muted: {};
+        --color-subtle: {};
+        --color-emphasis: {};
+        --color-primary: {};
+        --color-secondary: {};
+        --color-tertiary: {};
+        --color-error: {};
+        --color-warning: {};
+        --color-success: {};
+        --color-border: {};
+        --color-link: {};
+        --color-highlight: {};
+    }}
+}}
+
 /* Base Styles */
 html {{
     font-size: var(--spacing-base);
@@ -48,8 +84,8 @@ html {{
 
 body {{
     font-family: var(--font-body);
-    color: var(--color-foreground);
-    background-color: var(--color-background);
+    color: var(--color-text);
+    background-color: var(--color-base);
     max-width: 90ch;
     margin: 0 auto;
     padding: 2rem 1rem;
@@ -95,10 +131,16 @@ a {{
 }}
 
 a:hover {{
-    color: var(--color-link-hover);
+    color: var(--color-emphasis);
     text-decoration: underline;
 }}
 
+/* Selection */
+::selection {{
+    background: var(--color-highlight);
+    color: var(--color-text);
+}}
+
 /* Lists */
 ul, ol {{
     margin-left: 2rem;
@@ -112,22 +154,27 @@ li {{
 /* Code */
 code {{
     font-family: var(--font-mono);
-    background-color: rgba(0, 0, 0, 0.05);
+    background: var(--color-surface);
     padding: 0.125rem 0.25rem;
-    border-radius: 3px;
+    border-radius: 4px;
     font-size: 0.9em;
 }}
 
 pre {{
     overflow-x: auto;
     margin-bottom: 1rem;
+    border-radius: 5px;
+    border: 1px solid var(--color-border);
+    box-sizing: border-box;
 }}
 
+/* Code blocks inside pre are handled by syntax theme */
 pre code {{
     display: block;
+    width: fit-content;
+    min-width: 100%;
     padding: 1rem;
-    background-color: rgba(0, 0, 0, 0.03);
-    border-radius: 5px;
+    background: var(--color-surface);
 }}
 
 /* Math */
@@ -143,11 +190,17 @@ pre code {{
 
 /* Blockquotes */
 blockquote {{
-    border-left: 4px solid var(--color-link);
+    border-left: 2px solid var(--color-secondary);
+    background: var(--color-surface);
     padding-left: 1rem;
     padding-right: 1rem;
+    padding-top: 0.5rem;
+    padding-bottom: 0.04rem;
     margin: 1rem 0;
-    font-style: italic;
+    font-size: 0.95em;
+    border-bottom-right-radius: 5px;
+    border-top-right-radius: 5px;
+}}
 }}
 
 /* Tables */
@@ -158,31 +211,37 @@ table {{
 }}
 
 th, td {{
-    border: 1px solid rgba(0, 0, 0, 0.1);
+    border: 1px solid var(--color-border);
     padding: 0.5rem;
     text-align: left;
 }}
 
 th {{
-    background-color: rgba(0, 0, 0, 0.05);
+    background: var(--color-surface);
     font-weight: 600;
 }}
 
+tr:hover {{
+    background: var(--color-surface);
+}}
+
 /* Footnotes */
 .footnote-reference {{
     font-size: 0.8em;
+    color: var(--color-subtle);
 }}
 
 .footnote-definition {{
     margin-top: 2rem;
     padding-top: 0.5rem;
-    border-top: 1px solid rgba(0, 0, 0, 0.1);
+    border-top: 1px solid var(--color-border);
     font-size: 0.9em;
 }}
 
 .footnote-definition-label {{
     font-weight: 600;
     margin-right: 0.5rem;
+    color: var(--color-primary);
 }}
 
 /* Images */
@@ -191,33 +250,65 @@ img {{
     height: auto;
     display: block;
     margin: 1rem 0;
+    border-radius: 4px;
 }}
 
 /* Horizontal Rule */
 hr {{
     border: none;
-    border-top: 1px solid rgba(0, 0, 0, 0.1);
+    border-top: 2px solid var(--color-border);
     margin: 2rem 0;
 }}
 "#,
-        theme.colours.background,
-        theme.colours.foreground,
-        theme.colours.link,
-        theme.colours.link_hover,
-        theme.colours.primary,
-        theme.colours.secondary,
-        theme.fonts.body,
-        theme.fonts.heading,
-        theme.fonts.monospace,
-        theme.spacing.base_size,
-        theme.spacing.line_height,
-        theme.spacing.scale,
+        // Light mode colours
+        light.base,
+        light.surface,
+        light.overlay,
+        light.text,
+        light.muted,
+        light.subtle,
+        light.emphasis,
+        light.primary,
+        light.secondary,
+        light.tertiary,
+        light.error,
+        light.warning,
+        light.success,
+        light.border,
+        light.link,
+        light.highlight,
+        // Fonts and spacing
+        fonts.body,
+        fonts.heading,
+        fonts.monospace,
+        spacing.base_size,
+        spacing.line_height,
+        spacing.scale,
+        // Dark mode colours
+        dark.base,
+        dark.surface,
+        dark.overlay,
+        dark.text,
+        dark.muted,
+        dark.subtle,
+        dark.emphasis,
+        dark.primary,
+        dark.secondary,
+        dark.tertiary,
+        dark.error,
+        dark.warning,
+        dark.success,
+        dark.border,
+        dark.link,
+        dark.highlight,
     )
 }
 
-pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result {
-    let syntect_theme = match &theme.code_theme {
-        ThemeCodeTheme::CodeThemeName(name) => {
+async fn load_syntect_dark_theme(
+    code_theme: &ThemeDarkCodeTheme<'_>,
+) -> miette::Result {
+    match code_theme {
+        ThemeDarkCodeTheme::CodeThemeName(name) => {
             match name.as_str() {
                 "rose-pine" => {
                     let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes());
@@ -225,7 +316,7 @@ pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result {
                         .into_diagnostic()
                         .map_err(|e| {
                             miette::miette!("Failed to load embedded rose-pine theme: {}", e)
-                        })?
+                        })
                 }
                 "rose-pine-dawn" => {
                     let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes());
@@ -233,7 +324,7 @@ pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result {
                         .into_diagnostic()
                         .map_err(|e| {
                             miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e)
-                        })?
+                        })
                 }
                 _ => {
                     // Fall back to syntect's built-in themes
@@ -241,12 +332,12 @@ pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result {
                     theme_set
                         .themes
                         .get(name.as_str())
-                        .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name))?
-                        .clone()
+                        .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name))
+                        .cloned()
                 }
             }
         }
-        ThemeCodeTheme::CodeThemeFile(file) => {
+        ThemeDarkCodeTheme::CodeThemeFile(file) => {
             let client = BasicClient::unauthenticated();
             let pds = client.pds_for_did(&file.did).await?;
             let blob = client
@@ -263,23 +354,109 @@ pub async fn generate_syntax_css(theme: &Theme<'_>) -> miette::Result {
             let mut cursor = Cursor::new(blob);
             ThemeSet::load_from_reader(&mut cursor)
                 .into_diagnostic()
-                .map_err(|e| miette::miette!("Failed to download theme: {}", e))?
+                .map_err(|e| miette::miette!("Failed to download theme: {}", e))
         }
         _ => {
             let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes());
             ThemeSet::load_from_reader(&mut cursor)
                 .into_diagnostic()
-                .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e))?
+                .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e))
         }
-    };
+    }
+}
 
-    let css = css_for_theme_with_class_style(
-        &syntect_theme,
+async fn load_syntect_light_theme(
+    code_theme: &ThemeLightCodeTheme<'_>,
+) -> miette::Result {
+    match code_theme {
+        ThemeLightCodeTheme::CodeThemeName(name) => {
+            match name.as_str() {
+                "rose-pine" => {
+                    let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes());
+                    ThemeSet::load_from_reader(&mut cursor)
+                        .into_diagnostic()
+                        .map_err(|e| {
+                            miette::miette!("Failed to load embedded rose-pine theme: {}", e)
+                        })
+                }
+                "rose-pine-dawn" => {
+                    let mut cursor = Cursor::new(ROSE_PINE_DAWN_THEME.as_bytes());
+                    ThemeSet::load_from_reader(&mut cursor)
+                        .into_diagnostic()
+                        .map_err(|e| {
+                            miette::miette!("Failed to load embedded rose-pine-dawn theme: {}", e)
+                        })
+                }
+                _ => {
+                    // Fall back to syntect's built-in themes
+                    let theme_set = ThemeSet::load_defaults();
+                    theme_set
+                        .themes
+                        .get(name.as_str())
+                        .ok_or_else(|| miette::miette!("Theme '{}' not found in defaults", name))
+                        .cloned()
+                }
+            }
+        }
+        ThemeLightCodeTheme::CodeThemeFile(file) => {
+            let client = BasicClient::unauthenticated();
+            let pds = client.pds_for_did(&file.did).await?;
+            let blob = client
+                .xrpc(pds)
+                .send(
+                    &GetBlob::new()
+                        .did(file.did.clone())
+                        .cid(file.content.blob().cid().clone())
+                        .build(),
+                )
+                .await?
+                .buffer()
+                .clone();
+            let mut cursor = Cursor::new(blob);
+            ThemeSet::load_from_reader(&mut cursor)
+                .into_diagnostic()
+                .map_err(|e| miette::miette!("Failed to download theme: {}", e))
+        }
+        _ => {
+            let mut cursor = Cursor::new(ROSE_PINE_THEME.as_bytes());
+            ThemeSet::load_from_reader(&mut cursor)
+                .into_diagnostic()
+                .map_err(|e| miette::miette!("Failed to load embedded rose-pine theme: {}", e))
+        }
+    }
+}
+
+pub async fn generate_syntax_css(theme: &ResolvedTheme<'_>) -> miette::Result {
+    // Load both themes
+    let dark_syntect_theme = load_syntect_dark_theme(&theme.dark_code_theme).await?;
+    let light_syntect_theme = load_syntect_light_theme(&theme.light_code_theme).await?;
+
+    // Generate dark mode CSS (default)
+    let dark_css = css_for_theme_with_class_style(
+        &dark_syntect_theme,
+        ClassStyle::SpacedPrefixed {
+            prefix: crate::code_pretty::CSS_PREFIX,
+        },
+    )
+    .into_diagnostic()?;
+
+    // Generate light mode CSS
+    let light_css = css_for_theme_with_class_style(
+        &light_syntect_theme,
         ClassStyle::SpacedPrefixed {
             prefix: crate::code_pretty::CSS_PREFIX,
         },
     )
     .into_diagnostic()?;
 
-    Ok(css)
+    // Combine with media queries
+    let mut result = String::new();
+    result.push_str("/* Syntax highlighting - Light Mode (default) */\n");
+    result.push_str(&light_css);
+    result.push_str("\n\n/* Syntax highlighting - Dark Mode */\n");
+    result.push_str("@media (prefers-color-scheme: dark) {\n");
+    result.push_str(&dark_css);
+    result.push_str("}\n");
+
+    Ok(result)
 }
diff --git a/crates/weaver-renderer/src/static_site.rs b/crates/weaver-renderer/src/static_site.rs
index afea735..3256a55 100644
--- a/crates/weaver-renderer/src/static_site.rs
+++ b/crates/weaver-renderer/src/static_site.rs
@@ -17,7 +17,7 @@ use crate::{
         document::{CssMode, write_document_footer, write_document_head},
         writer::StaticPageWriter,
     },
-    theme::default_theme,
+    theme::default_resolved_theme,
     utils::flatten_dir_to_just_one_parent,
     walker::{WalkOptions, vault_contents},
 };
@@ -239,7 +239,7 @@ where
             .await
             .into_diagnostic()?;
 
-        let default_theme = default_theme();
+        let default_theme = default_resolved_theme();
         let theme = self.context.theme.as_deref().unwrap_or(&default_theme);
 
         // Write base.css
diff --git a/crates/weaver-renderer/src/static_site/context.rs b/crates/weaver-renderer/src/static_site/context.rs
index 2c3d526..31d9ab6 100644
--- a/crates/weaver-renderer/src/static_site/context.rs
+++ b/crates/weaver-renderer/src/static_site/context.rs
@@ -1,5 +1,5 @@
 use crate::static_site::{StaticSiteOptions};
-use crate::theme::Theme;
+use crate::theme::ResolvedTheme;
 use crate::{Frontmatter, NotebookContext,default_md_options};
 use dashmap::DashMap;
 use markdown_weaver::{CowStr, EmbedType, Tag, WeaverAttributes};
@@ -39,7 +39,7 @@ pub struct StaticSiteContext {
     pub client: Option,
     agent: Option>>,
 
-    pub theme: Option>>,
+    pub theme: Option>>,
     pub katex_source: Option,
     pub syntax_set: Arc,
     pub index_file: Option,
@@ -122,6 +122,8 @@ impl StaticSiteContext {
         }
     }
     pub fn new(root: PathBuf, destination: PathBuf, session: Option) -> Self {
+        use crate::theme::default_resolved_theme;
+
         Self {
             start_at: root.clone(),
             root,
@@ -136,14 +138,14 @@ impl StaticSiteContext {
             position: 0,
             client: Some(reqwest::Client::default()),
             agent: session.map(|session| Arc::new(Agent::new(session))),
-            theme: None,
+            theme: Some(Arc::new(default_resolved_theme())),
             katex_source: None,
             syntax_set: Arc::new(SyntaxSet::load_defaults_newlines()),
             index_file: None,
         }
     }
 
-    pub fn with_theme(mut self, theme: Theme<'static>) -> Self {
+    pub fn with_theme(mut self, theme: ResolvedTheme<'static>) -> Self {
         self.theme = Some(Arc::new(theme));
         self
     }
diff --git a/crates/weaver-renderer/src/static_site/document.rs b/crates/weaver-renderer/src/static_site/document.rs
index b42f82f..d171e5b 100644
--- a/crates/weaver-renderer/src/static_site/document.rs
+++ b/crates/weaver-renderer/src/static_site/document.rs
@@ -1,6 +1,6 @@
 use crate::css::{generate_base_css, generate_syntax_css};
 use crate::static_site::context::{KaTeXSource, StaticSiteContext};
-use crate::theme::{Theme, default_theme};
+use crate::theme::default_resolved_theme;
 use miette::IntoDiagnostic;
 use weaver_common::jacquard::client::AgentSession;
 
@@ -98,7 +98,7 @@ pub async fn write_document_head(
                 .into_diagnostic()?;
         }
         CssMode::Inline => {
-            let default_theme = default_theme();
+            let default_theme = default_resolved_theme();
             let theme = context.theme.as_deref().unwrap_or(&default_theme);
 
             writer.write_all(b"