diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json b/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json index 4088a6e..e0974e1 100644 --- a/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_entry.json @@ -15,10 +15,16 @@ "createdAt" ], "properties": { + "authors": { + "type": "array", + "items": { + "type": "ref", + "ref": "sh.weaver.actor.defs#author" + } + }, "content": { "type": "string", - "description": "The content of the notebook entry. This should be some flavor of Markdown.", - "maxLength": 200000 + "description": "The content of the notebook entry. This should be some flavor of Markdown." }, "createdAt": { "type": "string", diff --git a/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json b/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json index ae9b418..32e4b3c 100644 --- a/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json +++ b/crates/weaver-api/lexicons/sh_weaver_notebook_theme.json @@ -4,6 +4,7 @@ "defs": { "codeThemeFile": { "type": "object", + "description": "Custom syntax highlighting theme file (sublime text/textmate theme format)", "required": [ "name", "did", @@ -29,6 +30,50 @@ "codeThemeName": { "type": "string" }, + "font": { + "type": "object", + "required": [ + "value" + ], + "properties": { + "value": { + "type": "union", + "description": "Font for a notebook", + "refs": [ + "#fontName", + "#fontFile" + ] + } + } + }, + "fontFile": { + "type": "object", + "description": "Custom woff(2) or ttf font file", + "required": [ + "name", + "did", + "content" + ], + "properties": { + "content": { + "type": "blob", + "accept": [ + "*/*" + ], + "maxSize": 20000 + }, + "did": { + "type": "string", + "format": "did" + }, + "name": { + "type": "string" + } + } + }, + "fontName": { + "type": "string" + }, "main": { "type": "record", "description": "Theme for a notebook", @@ -68,6 +113,7 @@ }, "fonts": { "type": "object", + "description": "Fonts to be used in the notebook. Can specify a name or list of names (will load if available) or a file or list of files for each. Empty lists will use site defaults.", "required": [ "body", "heading", @@ -75,13 +121,25 @@ ], "properties": { "body": { - "type": "string" + "type": "array", + "items": { + "type": "ref", + "ref": "#font" + } }, "heading": { - "type": "string" + "type": "array", + "items": { + "type": "ref", + "ref": "#font" + } }, "monospace": { - "type": "string" + "type": "array", + "items": { + "type": "ref", + "ref": "#font" + } } } }, diff --git a/crates/weaver-api/src/sh_weaver/notebook/entry.rs b/crates/weaver-api/src/sh_weaver/notebook/entry.rs index b572f0b..a5c1e9e 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/entry.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/entry.rs @@ -18,6 +18,9 @@ )] #[serde(rename_all = "camelCase")] pub struct Entry<'a> { + #[serde(skip_serializing_if = "std::option::Option::is_none")] + #[serde(borrow)] + pub authors: std::option::Option>>, /// The content of the notebook entry. This should be some flavor of Markdown. #[serde(borrow)] pub content: jacquard_common::CowStr<'a>, @@ -117,6 +120,7 @@ pub mod entry_state { pub struct EntryBuilder<'a, S: entry_state::State> { _phantom_state: ::core::marker::PhantomData S>, __unsafe_private_named: ( + ::core::option::Option>>, ::core::option::Option>, ::core::option::Option, ::core::option::Option>, @@ -140,12 +144,31 @@ impl<'a> EntryBuilder<'a, entry_state::Empty> { pub fn new() -> Self { EntryBuilder { _phantom_state: ::core::marker::PhantomData, - __unsafe_private_named: (None, None, None, None, None, None, None), + __unsafe_private_named: (None, None, None, None, None, None, None, None), _phantom: ::core::marker::PhantomData, } } } +impl<'a, S: entry_state::State> EntryBuilder<'a, S> { + /// Set the `authors` field (optional) + pub fn authors( + mut self, + value: impl Into>>>, + ) -> Self { + self.__unsafe_private_named.0 = value.into(); + self + } + /// Set the `authors` field to an Option value (optional) + pub fn maybe_authors( + mut self, + value: Option>>, + ) -> Self { + self.__unsafe_private_named.0 = value; + self + } +} + impl<'a, S> EntryBuilder<'a, S> where S: entry_state::State, @@ -156,7 +179,7 @@ where mut self, value: impl Into>, ) -> EntryBuilder<'a, entry_state::SetContent> { - self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); EntryBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -175,7 +198,7 @@ where mut self, value: impl Into, ) -> EntryBuilder<'a, entry_state::SetCreatedAt> { - self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); EntryBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -187,12 +210,12 @@ where impl<'a, S: entry_state::State> EntryBuilder<'a, S> { /// Set the `embeds` field (optional) pub fn embeds(mut self, value: impl Into>>) -> Self { - self.__unsafe_private_named.2 = value.into(); + self.__unsafe_private_named.3 = value.into(); self } /// Set the `embeds` field to an Option value (optional) pub fn maybe_embeds(mut self, value: Option>) -> Self { - self.__unsafe_private_named.2 = value; + self.__unsafe_private_named.3 = value; self } } @@ -207,7 +230,7 @@ where mut self, value: impl Into>, ) -> EntryBuilder<'a, entry_state::SetPath> { - self.__unsafe_private_named.3 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.4 = ::core::option::Option::Some(value.into()); EntryBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -222,7 +245,7 @@ impl<'a, S: entry_state::State> EntryBuilder<'a, S> { mut self, value: impl Into>>, ) -> Self { - self.__unsafe_private_named.4 = value.into(); + self.__unsafe_private_named.5 = value.into(); self } /// Set the `tags` field to an Option value (optional) @@ -230,7 +253,7 @@ impl<'a, S: entry_state::State> EntryBuilder<'a, S> { mut self, value: Option>, ) -> Self { - self.__unsafe_private_named.4 = value; + self.__unsafe_private_named.5 = value; self } } @@ -245,7 +268,7 @@ where mut self, value: impl Into>, ) -> EntryBuilder<'a, entry_state::SetTitle> { - self.__unsafe_private_named.5 = ::core::option::Option::Some(value.into()); + self.__unsafe_private_named.6 = ::core::option::Option::Some(value.into()); EntryBuilder { _phantom_state: ::core::marker::PhantomData, __unsafe_private_named: self.__unsafe_private_named, @@ -260,7 +283,7 @@ impl<'a, S: entry_state::State> EntryBuilder<'a, S> { mut self, value: impl Into>, ) -> Self { - self.__unsafe_private_named.6 = value.into(); + self.__unsafe_private_named.7 = value.into(); self } /// Set the `updatedAt` field to an Option value (optional) @@ -268,7 +291,7 @@ impl<'a, S: entry_state::State> EntryBuilder<'a, S> { mut self, value: Option, ) -> Self { - self.__unsafe_private_named.6 = value; + self.__unsafe_private_named.7 = value; self } } @@ -284,13 +307,14 @@ where /// Build the final struct pub fn build(self) -> Entry<'a> { Entry { - content: self.__unsafe_private_named.0.unwrap(), - created_at: self.__unsafe_private_named.1.unwrap(), - embeds: self.__unsafe_private_named.2, - path: self.__unsafe_private_named.3.unwrap(), - tags: self.__unsafe_private_named.4, - title: self.__unsafe_private_named.5.unwrap(), - updated_at: self.__unsafe_private_named.6, + authors: self.__unsafe_private_named.0, + content: self.__unsafe_private_named.1.unwrap(), + created_at: self.__unsafe_private_named.2.unwrap(), + embeds: self.__unsafe_private_named.3, + path: self.__unsafe_private_named.4.unwrap(), + tags: self.__unsafe_private_named.5, + title: self.__unsafe_private_named.6.unwrap(), + updated_at: self.__unsafe_private_named.7, extra_data: Default::default(), } } @@ -303,13 +327,14 @@ where >, ) -> Entry<'a> { Entry { - content: self.__unsafe_private_named.0.unwrap(), - created_at: self.__unsafe_private_named.1.unwrap(), - embeds: self.__unsafe_private_named.2, - path: self.__unsafe_private_named.3.unwrap(), - tags: self.__unsafe_private_named.4, - title: self.__unsafe_private_named.5.unwrap(), - updated_at: self.__unsafe_private_named.6, + authors: self.__unsafe_private_named.0, + content: self.__unsafe_private_named.1.unwrap(), + created_at: self.__unsafe_private_named.2.unwrap(), + embeds: self.__unsafe_private_named.3, + path: self.__unsafe_private_named.4.unwrap(), + tags: self.__unsafe_private_named.5, + title: self.__unsafe_private_named.6.unwrap(), + updated_at: self.__unsafe_private_named.7, extra_data: Some(extra_data), } } @@ -392,6 +417,20 @@ fn lexicon_doc_sh_weaver_notebook_entry() -> ::jacquard_lexicon::lexicon::Lexico properties: { #[allow(unused_mut)] let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("authors"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { + description: None, + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static( + "sh.weaver.actor.defs#author", + ), + }), + min_length: None, + max_length: None, + }), + ); map.insert( ::jacquard_common::smol_str::SmolStr::new_static("content"), ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { @@ -403,7 +442,7 @@ fn lexicon_doc_sh_weaver_notebook_entry() -> ::jacquard_lexicon::lexicon::Lexico format: None, default: None, min_length: None, - max_length: Some(200000usize), + max_length: None, min_graphemes: None, max_graphemes: None, r#enum: None, @@ -643,19 +682,6 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Entry<'a> { fn validate( &self, ) -> ::std::result::Result<(), ::jacquard_lexicon::validation::ConstraintError> { - { - let value = &self.content; - #[allow(unused_comparisons)] - if ::len(value.as_ref()) > 200000usize { - return Err(::jacquard_lexicon::validation::ConstraintError::MaxLength { - path: ::jacquard_lexicon::validation::ValidationPath::from_field( - "content", - ), - max: 200000usize, - actual: ::len(value.as_ref()), - }); - } - } 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 eb62451..b56e739 100644 --- a/crates/weaver-api/src/sh_weaver/notebook/theme.rs +++ b/crates/weaver-api/src/sh_weaver/notebook/theme.rs @@ -5,6 +5,7 @@ // This file was automatically generated from Lexicon schemas. // Any manual changes will be overwritten on the next regeneration. +/// Custom syntax highlighting theme file (sublime text/textmate theme format) #[jacquard_derive::lexicon] #[derive( serde::Serialize, @@ -215,7 +216,11 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico map.insert( ::jacquard_common::smol_str::SmolStr::new_static("codeThemeFile"), ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { - description: None, + description: Some( + ::jacquard_common::CowStr::new_static( + "Custom syntax highlighting theme file (sublime text/textmate theme format)", + ), + ), required: Some( vec![ ::jacquard_common::smol_str::SmolStr::new_static("name"), @@ -286,6 +291,112 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico known_values: None, }), ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("font"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: None, + required: Some( + vec![::jacquard_common::smol_str::SmolStr::new_static("value")], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("value"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Union(::jacquard_lexicon::lexicon::LexRefUnion { + description: Some( + ::jacquard_common::CowStr::new_static("Font for a notebook"), + ), + refs: vec![ + ::jacquard_common::CowStr::new_static("#fontName"), + ::jacquard_common::CowStr::new_static("#fontFile") + ], + closed: None, + }), + ); + map + }, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("fontFile"), + ::jacquard_lexicon::lexicon::LexUserType::Object(::jacquard_lexicon::lexicon::LexObject { + description: Some( + ::jacquard_common::CowStr::new_static( + "Custom woff(2) or ttf font file", + ), + ), + required: Some( + vec![ + ::jacquard_common::smol_str::SmolStr::new_static("name"), + ::jacquard_common::smol_str::SmolStr::new_static("did"), + ::jacquard_common::smol_str::SmolStr::new_static("content") + ], + ), + nullable: None, + properties: { + #[allow(unused_mut)] + let mut map = ::std::collections::BTreeMap::new(); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("content"), + ::jacquard_lexicon::lexicon::LexObjectProperty::Blob(::jacquard_lexicon::lexicon::LexBlob { + description: None, + accept: None, + max_size: None, + }), + ); + map.insert( + ::jacquard_common::smol_str::SmolStr::new_static("did"), + ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + description: None, + format: Some( + ::jacquard_lexicon::lexicon::LexStringFormat::Did, + ), + 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("name"), + ::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( + ::jacquard_common::smol_str::SmolStr::new_static("fontName"), + ::jacquard_lexicon::lexicon::LexUserType::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("main"), ::jacquard_lexicon::lexicon::LexUserType::Record(::jacquard_lexicon::lexicon::LexRecord { @@ -357,7 +468,11 @@ fn lexicon_doc_sh_weaver_notebook_theme() -> ::jacquard_lexicon::lexicon::Lexico map.insert( ::jacquard_common::smol_str::SmolStr::new_static("fonts"), ::jacquard_lexicon::lexicon::LexObjectProperty::Object(::jacquard_lexicon::lexicon::LexObject { - description: None, + description: Some( + ::jacquard_common::CowStr::new_static( + "Fonts to be used in the notebook. Can specify a name or list of names (will load if available) or a file or list of files for each. Empty lists will use site defaults.", + ), + ), required: Some( vec![ ::jacquard_common::smol_str::SmolStr::new_static("body"), @@ -371,49 +486,40 @@ 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("body"), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { description: None, - format: None, - default: None, + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static("#font"), + }), 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("heading"), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { description: None, - format: None, - default: None, + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static("#font"), + }), 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( "monospace", ), - ::jacquard_lexicon::lexicon::LexObjectProperty::String(::jacquard_lexicon::lexicon::LexString { + ::jacquard_lexicon::lexicon::LexObjectProperty::Array(::jacquard_lexicon::lexicon::LexArray { description: None, - format: None, - default: None, + items: ::jacquard_lexicon::lexicon::LexArrayItem::Ref(::jacquard_lexicon::lexicon::LexRef { + description: None, + r#ref: ::jacquard_common::CowStr::new_static("#font"), + }), min_length: None, max_length: None, - min_graphemes: None, - max_graphemes: None, - r#enum: None, - r#const: None, - known_values: None, }), ); map @@ -544,6 +650,378 @@ impl<'a> ::jacquard_lexicon::schema::LexiconSchema for CodeThemeFile<'a> { } pub type CodeThemeName<'a> = jacquard_common::CowStr<'a>; +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct Font<'a> { + /// Font for a notebook + #[serde(borrow)] + pub value: FontValue<'a>, +} + +pub mod font_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 Value; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Value = Unset; + } + ///State transition - sets the `value` field to Set + pub struct SetValue(PhantomData S>); + impl sealed::Sealed for SetValue {} + impl State for SetValue { + type Value = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `value` field + pub struct value(()); + } +} + +/// Builder for constructing an instance of this type +pub struct FontBuilder<'a, S: font_state::State> { + _phantom_state: ::core::marker::PhantomData S>, + __unsafe_private_named: (::core::option::Option>,), + _phantom: ::core::marker::PhantomData<&'a ()>, +} + +impl<'a> Font<'a> { + /// Create a new builder for this type + pub fn new() -> FontBuilder<'a, font_state::Empty> { + FontBuilder::new() + } +} + +impl<'a> FontBuilder<'a, font_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + FontBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None,), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FontBuilder<'a, S> +where + S: font_state::State, + S::Value: font_state::IsUnset, +{ + /// Set the `value` field (required) + pub fn value( + mut self, + value: impl Into>, + ) -> FontBuilder<'a, font_state::SetValue> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + FontBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FontBuilder<'a, S> +where + S: font_state::State, + S::Value: font_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> Font<'a> { + Font { + value: self.__unsafe_private_named.0.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>, + >, + ) -> Font<'a> { + Font { + value: self.__unsafe_private_named.0.unwrap(), + extra_data: Some(extra_data), + } + } +} + +#[jacquard_derive::open_union] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(tag = "$type")] +#[serde(bound(deserialize = "'de: 'a"))] +pub enum FontValue<'a> { + #[serde(rename = "sh.weaver.notebook.theme#fontName")] + FontName(Box>), + #[serde(rename = "sh.weaver.notebook.theme#fontFile")] + FontFile(Box>), +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for Font<'a> { + fn nsid() -> &'static str { + "sh.weaver.notebook.theme" + } + fn def_name() -> &'static str { + "font" + } + 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(()) + } +} + +/// Custom woff(2) or ttf font file +#[jacquard_derive::lexicon] +#[derive( + serde::Serialize, + serde::Deserialize, + Debug, + Clone, + PartialEq, + Eq, + jacquard_derive::IntoStatic +)] +#[serde(rename_all = "camelCase")] +pub struct FontFile<'a> { + #[serde(borrow)] + pub content: jacquard_common::types::blob::BlobRef<'a>, + #[serde(borrow)] + pub did: jacquard_common::types::string::Did<'a>, + #[serde(borrow)] + pub name: jacquard_common::CowStr<'a>, +} + +pub mod font_file_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 Did; + type Content; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Name = Unset; + type Did = Unset; + type Content = 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 Did = S::Did; + type Content = S::Content; + } + ///State transition - sets the `did` field to Set + pub struct SetDid(PhantomData S>); + impl sealed::Sealed for SetDid {} + impl State for SetDid { + type Name = S::Name; + type Did = Set; + type Content = S::Content; + } + ///State transition - sets the `content` field to Set + pub struct SetContent(PhantomData S>); + impl sealed::Sealed for SetContent {} + impl State for SetContent { + type Name = S::Name; + type Did = S::Did; + type Content = 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 `did` field + pub struct did(()); + ///Marker type for the `content` field + pub struct content(()); + } +} + +/// Builder for constructing an instance of this type +pub struct FontFileBuilder<'a, S: font_file_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> FontFile<'a> { + /// Create a new builder for this type + pub fn new() -> FontFileBuilder<'a, font_file_state::Empty> { + FontFileBuilder::new() + } +} + +impl<'a> FontFileBuilder<'a, font_file_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + FontFileBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FontFileBuilder<'a, S> +where + S: font_file_state::State, + S::Content: font_file_state::IsUnset, +{ + /// Set the `content` field (required) + pub fn content( + mut self, + value: impl Into>, + ) -> FontFileBuilder<'a, font_file_state::SetContent> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + FontFileBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FontFileBuilder<'a, S> +where + S: font_file_state::State, + S::Did: font_file_state::IsUnset, +{ + /// Set the `did` field (required) + pub fn did( + mut self, + value: impl Into>, + ) -> FontFileBuilder<'a, font_file_state::SetDid> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + FontFileBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FontFileBuilder<'a, S> +where + S: font_file_state::State, + S::Name: font_file_state::IsUnset, +{ + /// Set the `name` field (required) + pub fn name( + mut self, + value: impl Into>, + ) -> FontFileBuilder<'a, font_file_state::SetName> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + FontFileBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> FontFileBuilder<'a, S> +where + S: font_file_state::State, + S::Name: font_file_state::IsSet, + S::Did: font_file_state::IsSet, + S::Content: font_file_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> FontFile<'a> { + FontFile { + content: self.__unsafe_private_named.0.unwrap(), + did: self.__unsafe_private_named.1.unwrap(), + name: 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>, + >, + ) -> FontFile<'a> { + FontFile { + content: self.__unsafe_private_named.0.unwrap(), + did: self.__unsafe_private_named.1.unwrap(), + name: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } +} + +impl<'a> ::jacquard_lexicon::schema::LexiconSchema for FontFile<'a> { + fn nsid() -> &'static str { + "sh.weaver.notebook.theme" + } + fn def_name() -> &'static str { + "fontFile" + } + 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(()) + } +} + +pub type FontName<'a> = jacquard_common::CowStr<'a>; /// Theme for a notebook #[jacquard_derive::lexicon] #[derive( @@ -566,6 +1044,7 @@ pub struct Theme<'a> { #[serde(skip_serializing_if = "std::option::Option::is_none")] #[serde(borrow)] pub default_theme: std::option::Option>, + /// Fonts to be used in the notebook. Can specify a name or list of names (will load if available) or a file or list of files for each. Empty lists will use site defaults. #[serde(borrow)] pub fonts: ThemeFonts<'a>, /// Syntax highlighting theme for light mode @@ -932,6 +1411,7 @@ pub enum ThemeDarkCodeTheme<'a> { CodeThemeFile(Box>), } +/// Fonts to be used in the notebook. Can specify a name or list of names (will load if available) or a file or list of files for each. Empty lists will use site defaults. #[jacquard_derive::lexicon] #[derive( serde::Serialize, @@ -940,17 +1420,193 @@ pub enum ThemeDarkCodeTheme<'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>, + pub body: Vec>, #[serde(borrow)] - pub heading: jacquard_common::CowStr<'a>, + pub heading: Vec>, #[serde(borrow)] - pub monospace: jacquard_common::CowStr<'a>, + pub monospace: Vec>, +} + +pub mod theme_fonts_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 Body; + type Heading; + type Monospace; + } + /// Empty state - all required fields are unset + pub struct Empty(()); + impl sealed::Sealed for Empty {} + impl State for Empty { + type Body = Unset; + type Heading = Unset; + type Monospace = Unset; + } + ///State transition - sets the `body` field to Set + pub struct SetBody(PhantomData S>); + impl sealed::Sealed for SetBody {} + impl State for SetBody { + type Body = Set; + type Heading = S::Heading; + type Monospace = S::Monospace; + } + ///State transition - sets the `heading` field to Set + pub struct SetHeading(PhantomData S>); + impl sealed::Sealed for SetHeading {} + impl State for SetHeading { + type Body = S::Body; + type Heading = Set; + type Monospace = S::Monospace; + } + ///State transition - sets the `monospace` field to Set + pub struct SetMonospace(PhantomData S>); + impl sealed::Sealed for SetMonospace {} + impl State for SetMonospace { + type Body = S::Body; + type Heading = S::Heading; + type Monospace = Set; + } + /// Marker types for field names + #[allow(non_camel_case_types)] + pub mod members { + ///Marker type for the `body` field + pub struct body(()); + ///Marker type for the `heading` field + pub struct heading(()); + ///Marker type for the `monospace` field + pub struct monospace(()); + } +} + +/// Builder for constructing an instance of this type +pub struct ThemeFontsBuilder<'a, S: theme_fonts_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> ThemeFonts<'a> { + /// Create a new builder for this type + pub fn new() -> ThemeFontsBuilder<'a, theme_fonts_state::Empty> { + ThemeFontsBuilder::new() + } +} + +impl<'a> ThemeFontsBuilder<'a, theme_fonts_state::Empty> { + /// Create a new builder with all fields unset + pub fn new() -> Self { + ThemeFontsBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: (None, None, None), + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ThemeFontsBuilder<'a, S> +where + S: theme_fonts_state::State, + S::Body: theme_fonts_state::IsUnset, +{ + /// Set the `body` field (required) + pub fn body( + mut self, + value: impl Into>>, + ) -> ThemeFontsBuilder<'a, theme_fonts_state::SetBody> { + self.__unsafe_private_named.0 = ::core::option::Option::Some(value.into()); + ThemeFontsBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ThemeFontsBuilder<'a, S> +where + S: theme_fonts_state::State, + S::Heading: theme_fonts_state::IsUnset, +{ + /// Set the `heading` field (required) + pub fn heading( + mut self, + value: impl Into>>, + ) -> ThemeFontsBuilder<'a, theme_fonts_state::SetHeading> { + self.__unsafe_private_named.1 = ::core::option::Option::Some(value.into()); + ThemeFontsBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ThemeFontsBuilder<'a, S> +where + S: theme_fonts_state::State, + S::Monospace: theme_fonts_state::IsUnset, +{ + /// Set the `monospace` field (required) + pub fn monospace( + mut self, + value: impl Into>>, + ) -> ThemeFontsBuilder<'a, theme_fonts_state::SetMonospace> { + self.__unsafe_private_named.2 = ::core::option::Option::Some(value.into()); + ThemeFontsBuilder { + _phantom_state: ::core::marker::PhantomData, + __unsafe_private_named: self.__unsafe_private_named, + _phantom: ::core::marker::PhantomData, + } + } +} + +impl<'a, S> ThemeFontsBuilder<'a, S> +where + S: theme_fonts_state::State, + S::Body: theme_fonts_state::IsSet, + S::Heading: theme_fonts_state::IsSet, + S::Monospace: theme_fonts_state::IsSet, +{ + /// Build the final struct + pub fn build(self) -> ThemeFonts<'a> { + ThemeFonts { + body: self.__unsafe_private_named.0.unwrap(), + heading: self.__unsafe_private_named.1.unwrap(), + monospace: 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>, + >, + ) -> ThemeFonts<'a> { + ThemeFonts { + body: self.__unsafe_private_named.0.unwrap(), + heading: self.__unsafe_private_named.1.unwrap(), + monospace: self.__unsafe_private_named.2.unwrap(), + extra_data: Some(extra_data), + } + } } impl<'a> ::jacquard_lexicon::schema::LexiconSchema for ThemeFonts<'a> { diff --git a/crates/weaver-app/assets/styling/cards-base.css b/crates/weaver-app/assets/styling/cards-base.css new file mode 100644 index 0000000..80aa50a --- /dev/null +++ b/crates/weaver-app/assets/styling/cards-base.css @@ -0,0 +1,150 @@ +/* Shared card primitives + Used by entry-card.css and notebook-card.css */ + +/* ========================================================================== + CARD SURFACE + Container with background, shadow (light) or border (dark) + ========================================================================== */ + +.card-surface { + background: var(--color-surface); + box-shadow: 0 1px 2px color-mix(in srgb, var(--color-text) 6%, transparent); +} + +@media (prefers-color-scheme: dark) { + .card-surface { + box-shadow: none; + border: 1px solid var(--color-border); + } +} + +/* ========================================================================== + CARD TITLE + Heading style for card titles + ========================================================================== */ + +.card-title { + font-family: var(--font-heading); + font-weight: 600; + color: var(--color-primary); + margin: 0; + transition: color 0.2s ease; +} + +/* ========================================================================== + CARD META + Muted metadata text (dates, counts, etc.) + ========================================================================== */ + +.card-meta { + color: var(--color-muted); + font-size: 0.85rem; +} + +/* ========================================================================== + CARD TAGS + Tag list container and individual tags + ========================================================================== */ + +.card-tags { + display: flex; + gap: 0.4rem; + flex-wrap: wrap; +} + +.card-tag { + padding: 0.2rem 0.4rem 0.2rem 0; + font-size: 0.75rem; + color: var(--color-subtle); + border-bottom: 1px solid var(--color-border); + transition: + color 0.15s ease, + border-color 0.15s ease; +} + +/* ========================================================================== + CARD PREVIEW + Truncated content preview with line clamping + ========================================================================== */ + +.card-preview { + color: var(--color-subtle); + font-family: var(--font-body); + font-size: 0.875rem; + line-height: 1.5; + display: -webkit-box; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.card-preview-3 { + -webkit-line-clamp: 3; +} + +.card-preview-4 { + -webkit-line-clamp: 4; +} + +/* Preview content normalization */ +.card-preview h1, +.card-preview h2, +.card-preview h3, +.card-preview h4, +.card-preview h5, +.card-preview h6 { + font-size: 0.875rem; + font-weight: 600; + margin-top: 0; + margin-left: 0; + margin-right: 0; +} + +.card-preview p { + margin: 0; + display: inline; +} + +.card-preview code { + font-size: 0.8rem; + white-space: pre-wrap; + word-break: break-all; +} + +.card-preview pre { + white-space: pre-wrap; + word-break: break-all; + max-width: 100%; +} + +/* ========================================================================== + CARD HOVER STATES + Common hover patterns for cards + ========================================================================== */ + +.card-hover-border:hover { + border-left-color: var(--color-secondary); +} + +.card-hover-title:hover .card-title { + color: var(--color-secondary); +} + +.card-hover-tags:hover .card-tag { + color: var(--color-tertiary); + border-color: var(--color-tertiary); +} + +/* ========================================================================== + LOADING/ERROR STATES + ========================================================================== */ + +.card-loading, +.card-error { + text-align: center; + padding: 2rem; + color: var(--color-muted); +} + +.card-error { + color: var(--color-error); +} diff --git a/crates/weaver-app/assets/styling/entry-card.css b/crates/weaver-app/assets/styling/entry-card.css index 3beaf57..5c64884 100644 --- a/crates/weaver-app/assets/styling/entry-card.css +++ b/crates/weaver-app/assets/styling/entry-card.css @@ -1,54 +1,6 @@ /* Entry card styling */ +/* Layout classes (.notebook-layout, etc.) are now in layouts.css */ -/* Notebook layout - sidebar in left gutter on desktop, header on mobile */ -.notebook-layout { - display: grid; - grid-template-columns: minmax(280px, 1fr) minmax(0, 90rem) minmax(280px, 1fr); - gap: 2rem; - max-width: calc(90rem + 560px + 4rem); /* content + gutters + gaps */ - margin: 0 auto; - padding: 2.5rem 1.25rem 2.5rem 0; -} - -.notebook-sidebar { - grid-column: 1; - position: sticky; - top: 2rem; - align-self: flex-start; - max-height: calc(100vh - 4rem); - overflow-y: auto; -} - -.notebook-main { - grid-column: 2; - padding: 0 1rem; -} - -/* Mobile layout - sidebar becomes header */ -@media (max-width: 1400px) { - .notebook-layout { - grid-template-columns: minmax(1rem, 1fr) minmax(0, 90rem) minmax(1rem, 1fr) !important; - gap: 0 !important; - max-width: 100vw !important; - box-sizing: border-box !important; - } - - .notebook-sidebar { - grid-column: 2; - position: static; - max-height: none; - min-width: 0; - margin-bottom: 2rem; - } - - .notebook-main { - grid-column: 2; - padding: 0; - min-width: 0; - } -} - -/* Entries list - width constrained by grid column */ .entries-list { } @@ -110,31 +62,9 @@ color: var(--color-subtle); } -a.entry-card-author, +/* Card-specific author positioning (uses embed-author base styles) */ .entry-card-author { - display: flex; - align-items: center; - gap: 0.5rem; margin-top: 0.25rem; - text-decoration: none; -} - -.entry-card-author .author-name { - font-weight: 500; - color: var(--color-text); - transition: color 0.2s ease; -} - -.entry-card-author .meta-label { - color: var(--color-muted); - font-size: 0.8rem; - transition: color 0.2s ease; -} - -.entry-card-author:hover .author-name, -.entry-card-author:hover .meta-label, -.feed-entry-card .entry-card-author:hover .meta-label { - color: var(--color-link); } .entry-card-date { @@ -176,6 +106,11 @@ a.entry-card-author, margin-left: auto; } +/* Date in header (no author) aligns right */ +.feed-entry-card .entry-card-header .entry-card-date { + margin-left: auto; +} + .entry-card-tags { display: flex; gap: 0.4rem; diff --git a/crates/weaver-app/assets/styling/layouts.css b/crates/weaver-app/assets/styling/layouts.css new file mode 100644 index 0000000..4a550a6 --- /dev/null +++ b/crates/weaver-app/assets/styling/layouts.css @@ -0,0 +1,190 @@ +/* Sidebar layouts - 3-column grid with sticky sidebar */ + +/* Base sidebar layout pattern + Uses CSS custom properties for sizing flexibility: + --layout-gutter: sidebar/gutter width (default: 240px) + --layout-content: main content max-width (default: 95ch) + --layout-gap: grid gap (default: 1.5rem) + --layout-padding: container padding (default: 2rem 1.25rem 2rem 0) +*/ + +.sidebar-layout { + display: grid; + grid-template-columns: + minmax(var(--layout-gutter, 240px), 1fr) + minmax(0, var(--layout-content, 95ch)) + minmax(var(--layout-gutter, 240px), 1fr); + gap: var(--layout-gap, 1.5rem); + max-width: calc(var(--layout-content, 95ch) + var(--layout-gutter, 240px) * 2 + 4rem); + margin: 0 auto; + padding: var(--layout-padding, 2rem 1.25rem 2rem 0); +} + +.sidebar-layout-sidebar { + grid-column: 1; + position: sticky; + top: 2rem; + align-self: flex-start; + max-height: calc(100vh - 4rem); + overflow-y: auto; +} + +.sidebar-layout-main { + grid-column: 2; + padding: 0 1rem; +} + +.sidebar-layout-actions { + grid-column: 3; + position: sticky; + top: 2rem; + align-self: flex-start; +} + +/* Mobile: sidebar collapses to header above main content */ +@media (max-width: 1400px) { + .sidebar-layout { + grid-template-columns: minmax(1rem, 1fr) minmax(0, var(--layout-content, 95ch)) minmax(1rem, 1fr) !important; + gap: 0 !important; + max-width: 100vw !important; + box-sizing: border-box !important; + } + + .sidebar-layout-sidebar { + grid-column: 2; + position: static; + max-height: none; + min-width: 0; + margin-bottom: 2rem; + } + + .sidebar-layout-main { + grid-column: 2; + padding: 0; + min-width: 0; + } + + .sidebar-layout-actions { + display: none; + } +} + +/* Variant: notebook layout (wider gutters and content) */ +.sidebar-layout-notebook { + --layout-gutter: 280px; + --layout-content: 90rem; + --layout-gap: 2rem; + --layout-padding: 2.5rem 1.25rem 2.5rem 0; +} + +/* Variant: repository/profile layout (standard sizing) */ +.sidebar-layout-repository { + --layout-gutter: 240px; + --layout-content: 95ch; + --layout-gap: 1rem; + --layout-padding: 2.25rem 1.25rem 2.25rem 0; +} + +/* ========================================================================== + BACKWARD-COMPATIBLE CLASS ALIASES + These map existing component class names to the unified layout system. + ========================================================================== */ + +/* Notebook layout (used in notebook.rs) */ +.notebook-layout { + display: grid; + grid-template-columns: + minmax(280px, 1fr) + minmax(0, 90rem) + minmax(280px, 1fr); + gap: 2rem; + max-width: calc(90rem + 560px + 4rem); + margin: 0 auto; + padding: 2.5rem 1.25rem 2.5rem 0; +} + +.notebook-sidebar { + grid-column: 1; + position: sticky; + top: 2rem; + align-self: flex-start; + max-height: calc(100vh - 4rem); + overflow-y: auto; +} + +.notebook-main { + grid-column: 2; + padding: 0 1rem; +} + +@media (max-width: 1400px) { + .notebook-layout { + grid-template-columns: minmax(1rem, 1fr) minmax(0, 90rem) minmax(1rem, 1fr) !important; + gap: 0 !important; + max-width: 100vw !important; + box-sizing: border-box !important; + } + + .notebook-sidebar { + grid-column: 2; + position: static; + max-height: none; + min-width: 0; + margin-bottom: 2rem; + } + + .notebook-main { + grid-column: 2; + padding: 0; + min-width: 0; + } +} + +/* Repository layout (used in identity.rs) */ +.repository-layout { + display: grid; + grid-template-columns: + minmax(240px, 1fr) + minmax(0, 95ch) + minmax(240px, 1fr); + gap: 1rem; + max-width: calc(95ch + 480px + 4rem); + margin: 0 auto; + padding: 2.25rem 1.25rem 2.25rem 0; +} + +.repository-sidebar { + grid-column: 1; + position: sticky; + top: 2rem; + align-self: flex-start; + overflow-y: auto; +} + +.repository-main { + grid-column: 2; + padding: 0 1rem; +} + +@media (max-width: 1400px) { + .repository-layout { + grid-template-columns: minmax(1rem, 1fr) minmax(0, 95ch) minmax(1rem, 1fr) !important; + gap: 0 !important; + max-width: 100vw !important; + box-sizing: border-box !important; + } + + .repository-sidebar { + grid-column: 2; + position: static; + max-height: none; + min-width: 0; + margin-bottom: 2rem; + } + + .repository-main { + grid-column: 2; + padding: 0; + min-width: 0; + } +} diff --git a/crates/weaver-app/assets/styling/main.css b/crates/weaver-app/assets/styling/main.css index 1197b2a..011bd60 100644 --- a/crates/weaver-app/assets/styling/main.css +++ b/crates/weaver-app/assets/styling/main.css @@ -1,3 +1,5 @@ +/* App shell styles - theme-defaults.css must load first */ + body { background-color: var(--color-base); color: var(--color-text); @@ -13,77 +15,8 @@ a { max-width: 1200px; } -.record-view-container { - max-width: 1200px; - margin: 2rem auto; - padding: 0 1rem; -} - -.uri-input-section { - margin-bottom: 2.5rem; -} - -.uri-input { - font-family: var(--font-mono); - font-size: 0.9rem; - width: 100%; - max-width: 100%; - box-sizing: border-box; - padding: 0.5rem 0.75rem; - background: var(--color-surface, rgba(0, 0, 0, 0.2)); - border: 1px solid var(--color-border); - color: var(--color-text); - outline: none; - transition: border-color 0.2s; -} - +/* .notebook-content width constraint for rendered content */ .notebook-content { width: 100%; max-width: 95ch; } - -.uri-input:focus { - border-color: var(--color-primary); -} - -.uri-input::placeholder { - color: var(--color-subtle); - opacity: 0.5; -} - -@font-face { - font-family: "Ioskeley Mono"; - font-style: normal; - font-weight: normal; - src: url("/assets/IoskeleyMono-Regular.woff2") format("woff2"); -} -@font-face { - font-family: "Ioskeley Mono"; - font-style: normal; - font-weight: lighter; - src: url("/assets/IoskeleyMono-Light.woff2") format("woff2"); -} -@font-face { - font-family: "Ioskeley Mono"; - font-style: italic; - font-weight: lighter; - src: url("/assets/IoskeleyMono-LightItalic.woff2") format("woff2"); -} -@font-face { - font-family: "Ioskeley Mono"; - font-style: normal; - font-weight: bold; - src: url("/assets/IoskeleyMono-Bold.woff2") format("woff2"); -} -@font-face { - font-family: "Ioskeley Mono"; - font-style: italic; - font-weight: normal; - src: url("/assets/IoskeleyMono-Italic.woff2") format("woff2"); -} -@font-face { - font-family: "Ioskeley Mono"; - font-style: italic; - font-weight: bold; - src: url("/assets/IoskeleyMono-BoldItalic.woff2") format("woff2"); -} diff --git a/crates/weaver-app/assets/styling/notebook-card.css b/crates/weaver-app/assets/styling/notebook-card.css index 8a5d498..9d5a268 100644 --- a/crates/weaver-app/assets/styling/notebook-card.css +++ b/crates/weaver-app/assets/styling/notebook-card.css @@ -1,60 +1,12 @@ /* Notebook card styling */ +/* Layout classes (.repository-layout, etc.) are now in layouts.css */ -/* Repository layout - sidebar in left gutter on desktop, header on mobile */ - -.repository-layout { - display: grid; - grid-template-columns: minmax(240px, 1fr) minmax(0, 95ch) minmax(240px, 1fr); - gap: 1rem; - max-width: calc(95ch + 480px + 4rem); /* content + gutters + gaps */ - margin: 0 auto; - padding: 2.25rem 1.25rem 2.25rem 0; -} - -.repository-sidebar { - grid-column: 1; - position: sticky; - top: 2rem; - align-self: flex-start; - overflow-y: auto; -} - -.repository-main { - grid-column: 2; - padding: 0 1rem; -} - -/* Mobile layout - sidebar becomes header */ -@media (max-width: 1400px) { - .repository-layout { - grid-template-columns: minmax(1rem, 1fr) minmax(0, 95ch) minmax(1rem, 1fr) !important; - gap: 0 !important; - max-width: 100vw !important; - box-sizing: border-box !important; - } - - .repository-sidebar { - grid-column: 2; - position: static; - max-height: none; - min-width: 0; - margin-bottom: 2rem; - } - - .repository-main { - grid-column: 2; - padding: 0; - min-width: 0; - } -} - -/* Notebook list - width constrained by grid column */ .notebooks-list { margin-top: 0.25rem; } .notebook-card { - margin-bottom: 2.5rem; /* 2 grid units */ + margin-bottom: calc(1.25rem * var(--spacing-scale, 1.25)); } .notebook-card-container { diff --git a/crates/weaver-app/assets/styling/notebook-cover.css b/crates/weaver-app/assets/styling/notebook-cover.css index f2ad67a..934414f 100644 --- a/crates/weaver-app/assets/styling/notebook-cover.css +++ b/crates/weaver-app/assets/styling/notebook-cover.css @@ -46,33 +46,12 @@ display: none; } -.notebook-author { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.notebook-author .avatar { +/* Cover-specific author sizing (uses embed-author base styles) */ +.notebook-author .embed-avatar { width: 48px; height: 48px; - flex-shrink: 0; -} - -.notebook-author-info { - display: flex; - flex-direction: column; - min-width: 0; -} - -.notebook-author-name { - font-weight: 600; - color: var(--color-text); - font-size: 1rem; -} - -.notebook-author-handle { - color: var(--color-subtle); - font-size: 0.875rem; + min-width: 48px; + min-height: 48px; } .notebook-cover-description { diff --git a/crates/weaver-app/assets/styling/record-view.css b/crates/weaver-app/assets/styling/record-view.css index 71e5cfa..74c362e 100644 --- a/crates/weaver-app/assets/styling/record-view.css +++ b/crates/weaver-app/assets/styling/record-view.css @@ -549,13 +549,13 @@ } /* Array Section Styling */ -.section-header { +.record-section-header { display: flex; align-items: baseline; gap: 0.5rem; } -.array-item .section-header { +.array-item .record-section-header { } .array-item .section-content .record-field { } diff --git a/crates/weaver-app/src/components/entry.rs b/crates/weaver-app/src/components/entry.rs index 49d37b1..446f6c0 100644 --- a/crates/weaver-app/src/components/entry.rs +++ b/crates/weaver-app/src/components/entry.rs @@ -5,7 +5,6 @@ use crate::Route; use crate::blobcache::BlobCache; use crate::{ components::EntryActions, - components::avatar::{Avatar, AvatarImage}, data::use_handle, }; use dioxus::prelude::*; @@ -454,45 +453,55 @@ pub fn EntryCard( } } if let Some(author) = first_author { - div { class: "entry-card-author", - { - use weaver_api::sh_weaver::actor::ProfileDataViewInner; - - match &author.record.inner { - ProfileDataViewInner::ProfileView(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { + { + use weaver_api::sh_weaver::actor::ProfileDataViewInner; + + match &author.record.inner { + ProfileDataViewInner::ProfileView(profile) => { + let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); + let handle = profile.handle.clone(); + rsx! { + span { class: "embed-author entry-card-author", if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { src: avatar_url.as_ref() } - } + img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "" } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{handle}" } } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } } } - ProfileDataViewInner::ProfileViewDetailed(profile) => { - let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); - let handle = profile.handle.clone(); - rsx! { + } + ProfileDataViewInner::ProfileViewDetailed(profile) => { + let display_name = profile.display_name.as_ref().map(|n| n.as_ref()).unwrap_or("Unknown"); + let handle = profile.handle.clone(); + rsx! { + span { class: "embed-author entry-card-author", if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { src: avatar_url.as_ref() } - } + img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "" } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{handle}" } } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } } } - ProfileDataViewInner::TangledProfileView(profile) => { - rsx! { - span { class: "author-name", "@{profile.handle.as_ref()}" } + } + ProfileDataViewInner::TangledProfileView(profile) => { + rsx! { + span { class: "embed-author entry-card-author", + span { class: "embed-author-info", + span { class: "embed-author-handle", "@{profile.handle.as_ref()}" } + } } } - _ => { - rsx! { - span { class: "author-name", "Unknown" } + } + _ => { + rsx! { + span { class: "embed-author entry-card-author", + span { class: "embed-author-info", + span { class: "embed-author-name", "Unknown" } + } } } } @@ -518,13 +527,14 @@ pub fn EntryCard( } /// Card for entries in a feed (e.g., home page) -/// Takes EntryView directly (not BookEntryView) and always shows author info +/// Takes EntryView directly (not BookEntryView) #[component] pub fn FeedEntryCard( entry_view: EntryView<'static>, entry: entry::Entry<'static>, #[props(default = false)] show_actions: bool, #[props(default = false)] is_pinned: bool, + #[props(default = true)] show_author: bool, #[props(default)] on_pinned_changed: Option>, ) -> Element { use crate::Route; @@ -555,8 +565,8 @@ pub fn FeedEntryCard( // Format date from record's created_at let formatted_date = entry.created_at.as_ref().format("%B %d, %Y").to_string(); - // Get first author - let first_author = entry_view.authors.first(); + // Get first author if we're showing it + let first_author = if show_author { entry_view.authors.first() } else { None }; // Check ownership for actions let auth_state = use_context::>(); @@ -578,19 +588,37 @@ pub fn FeedEntryCard( rsx! { div { class: "entry-card feed-entry-card", - // Title - Link { - to: Route::StandaloneEntry { - ident: ident.clone(), - rkey: rkey.clone().into() - }, - class: "entry-card-title-link", - h3 { class: "entry-card-title", "{title}" } + // Header: title (and date if no author) + div { class: "entry-card-header", + Link { + to: Route::StandaloneEntry { + ident: ident.clone(), + rkey: rkey.clone().into() + }, + class: "entry-card-title-link", + h3 { class: "entry-card-title", "{title}" } + } + // Date inline with title when no author shown + if first_author.is_none() { + div { class: "entry-card-date", + time { datetime: "{entry.created_at.as_str()}", "{formatted_date}" } + } + } + if show_actions && is_owner { + crate::components::EntryActions { + entry_uri: entry_view.uri.clone().into_static(), + entry_cid: entry_view.cid.clone().into_static(), + entry_title: title.to_string(), + in_notebook: false, + is_pinned, + on_pinned_changed + } + } } - // Byline: author + date - div { class: "entry-card-byline", - if let Some(author) = first_author { + // Byline: author + date (only when author shown) + if let Some(author) = first_author { + div { class: "entry-card-byline", { match &author.record.inner { ProfileDataViewInner::ProfileView(profile) => { @@ -599,14 +627,14 @@ pub fn FeedEntryCard( rsx! { Link { to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "entry-card-author", + class: "embed-author entry-card-author", if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { src: avatar_url.as_ref() } - } + img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "" } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{handle}" } } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } } } } @@ -616,14 +644,14 @@ pub fn FeedEntryCard( rsx! { Link { to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "entry-card-author", + class: "embed-author entry-card-author", if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { src: avatar_url.as_ref() } - } + img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "" } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{handle}" } } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } } } } @@ -632,32 +660,26 @@ pub fn FeedEntryCard( rsx! { Link { to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - class: "entry-card-author", - span { class: "author-name", "@{handle}" } + class: "embed-author entry-card-author", + span { class: "embed-author-info", + span { class: "embed-author-handle", "@{handle}" } + } } } } _ => { rsx! { - div { class: "entry-card-author", - span { class: "author-name", "Unknown" } + span { class: "embed-author entry-card-author", + span { class: "embed-author-info", + span { class: "embed-author-name", "Unknown" } + } } } } } } - } - div { class: "entry-card-date", - time { datetime: "{entry.created_at.as_str()}", "{formatted_date}" } - } - if show_actions && is_owner { - crate::components::EntryActions { - entry_uri: entry_view.uri.clone().into_static(), - entry_cid: entry_view.cid.clone().into_static(), - entry_title: title.to_string(), - in_notebook: false, - is_pinned, - on_pinned_changed + div { class: "entry-card-date", + time { datetime: "{entry.created_at.as_str()}", "{formatted_date}" } } } } @@ -738,16 +760,13 @@ pub fn EntryMetadata( rsx! { Link { to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - div { class: "entry-authors", - if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { - src: avatar_url.as_ref() - } - } - } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } + class: "embed-author", + if let Some(ref avatar_url) = profile.avatar { + img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "" } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{handle}" } } } } @@ -758,28 +777,33 @@ pub fn EntryMetadata( rsx! { Link { to: Route::RepositoryIndex { ident: AtIdentifier::Handle(handle.clone()) }, - div { class: "entry-authors", - if let Some(ref avatar_url) = profile.avatar { - Avatar { - AvatarImage { - src: avatar_url.as_ref() - } - } - } - span { class: "author-name", "{display_name}" } - span { class: "meta-label", "@{handle}" } + class: "embed-author", + if let Some(ref avatar_url) = profile.avatar { + img { class: "embed-avatar", src: avatar_url.as_ref(), alt: "" } + } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{handle}" } } } } } ProfileDataViewInner::TangledProfileView(profile) => { rsx! { - span { class: "author-name", "@{profile.handle.as_ref()}" } + span { class: "embed-author", + span { class: "embed-author-info", + span { class: "embed-author-handle", "@{profile.handle.as_ref()}" } + } + } } } _ => { rsx! { - span { class: "author-name", "Unknown" } + span { class: "embed-author", + span { class: "embed-author-info", + span { class: "embed-author-name", "Unknown" } + } + } } } } diff --git a/crates/weaver-app/src/components/identity.rs b/crates/weaver-app/src/components/identity.rs index dbff7d2..4d7e8e0 100644 --- a/crates/weaver-app/src/components/identity.rs +++ b/crates/weaver-app/src/components/identity.rs @@ -68,17 +68,16 @@ pub fn ProfileOgMeta( } } -const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); +// Card styles (entry-card, notebook-card) loaded at navbar level const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); -const ENTRY_CARD_CSS: Asset = asset!("/assets/styling/entry-card.css"); +const LAYOUTS_CSS: Asset = asset!("/assets/styling/layouts.css"); #[component] pub fn Repository(ident: ReadSignal>) -> Element { rsx! { DefaultNotebookCss { } - document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } + document::Link { rel: "stylesheet", href: LAYOUTS_CSS } document::Link { rel: "stylesheet", href: ENTRY_CSS } - document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } div { Outlet:: {} } @@ -316,7 +315,6 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { }; rsx! { - document::Stylesheet { href: NOTEBOOK_CARD_CSS } {og_meta} div { class: "repository-layout", @@ -363,7 +361,8 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { entry_view: entry_view.clone(), entry: entry.clone(), show_actions: true, - is_pinned: true + is_pinned: true, + show_author: false } } } @@ -409,7 +408,8 @@ pub fn RepositoryIndex(ident: ReadSignal>) -> Element { entry_view: entry_view.clone(), entry: entry.clone(), show_actions: true, - is_pinned: false + is_pinned: false, + show_author: false } } } @@ -434,6 +434,7 @@ pub fn NotebookCard( notebook: NotebookView<'static>, entry_refs: Vec>, #[props(default = false)] is_pinned: bool, + #[props(default)] show_author: Option, #[props(default)] on_pinned_changed: Option>, #[props(default)] on_deleted: Option>, ) -> Element { @@ -469,8 +470,8 @@ pub fn NotebookCard( // Format date let formatted_date = notebook.indexed_at.as_ref().format("%B %d, %Y").to_string(); - // Show authors only if multiple - let show_authors = notebook.authors.len() > 1; + // Show authors: explicit prop overrides, otherwise show only if multiple + let show_authors = show_author.unwrap_or(notebook.authors.len() > 1); let ident = notebook.uri.authority().clone().into_static(); let book_title: SmolStr = notebook_path.clone().into(); diff --git a/crates/weaver-app/src/components/notebook_cover.rs b/crates/weaver-app/src/components/notebook_cover.rs index aa24842..b045cc3 100644 --- a/crates/weaver-app/src/components/notebook_cover.rs +++ b/crates/weaver-app/src/components/notebook_cover.rs @@ -1,7 +1,6 @@ #![allow(non_snake_case)] use crate::Route; -use crate::components::avatar::{Avatar, AvatarImage}; use crate::components::button::{Button, ButtonVariant}; use dioxus::prelude::*; use jacquard::smol_str::SmolStr; @@ -129,15 +128,13 @@ fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'stati .unwrap_or("Unknown"); rsx! { - div { class: "notebook-author", + span { class: "embed-author notebook-author", if let Some(ref avatar) = p.avatar { - Avatar { - AvatarImage { src: avatar.as_ref() } - } + img { class: "embed-avatar", src: avatar.as_ref(), alt: "" } } - div { class: "notebook-author-info", - div { class: "notebook-author-name", "{display_name}" } - div { class: "notebook-author-handle", "@{p.handle}" } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{p.handle}" } } } } @@ -150,29 +147,31 @@ fn NotebookAuthor(author: weaver_api::sh_weaver::notebook::AuthorListView<'stati .unwrap_or("Unknown"); rsx! { - div { class: "notebook-author", + span { class: "embed-author notebook-author", if let Some(ref avatar) = p.avatar { - Avatar { - AvatarImage { src: avatar.as_ref() } - } + img { class: "embed-avatar", src: avatar.as_ref(), alt: "" } } - div { class: "notebook-author-info", - div { class: "notebook-author-name", "{display_name}" } - div { class: "notebook-author-handle", "@{p.handle}" } + span { class: "embed-author-info", + span { class: "embed-author-name", "{display_name}" } + span { class: "embed-author-handle", "@{p.handle}" } } } } } ProfileDataViewInner::TangledProfileView(p) => { rsx! { - div { class: "notebook-author", - div { class: "notebook-author-name", "@{p.handle.as_ref()}" } + span { class: "embed-author notebook-author", + span { class: "embed-author-info", + span { class: "embed-author-handle", "@{p.handle.as_ref()}" } + } } } } _ => rsx! { - div { class: "notebook-author", - "Unknown author" + span { class: "embed-author notebook-author", + span { class: "embed-author-info", + span { class: "embed-author-name", "Unknown" } + } } }, } diff --git a/crates/weaver-app/src/components/record_editor.rs b/crates/weaver-app/src/components/record_editor.rs index 259c7b4..3b92e1a 100644 --- a/crates/weaver-app/src/components/record_editor.rs +++ b/crates/weaver-app/src/components/record_editor.rs @@ -935,7 +935,7 @@ fn EditableArrayField(root: Signal>, path: String, did: String) -> default_open: true, index: 0, AccordionTrigger { - div { class: "section-header", + div { class: "record-section-header", div { class: "section-label", { let parts: Vec<&str> = path.split('.').collect(); @@ -1038,7 +1038,7 @@ fn EditableObjectField( default_open: true, index: 0, AccordionTrigger { - div { class: "section-header", + div { class: "record-section-header", div { class: "section-label", { let parts: Vec<&str> = path.split('.').collect(); diff --git a/crates/weaver-app/src/main.rs b/crates/weaver-app/src/main.rs index c28a3c6..cbde60b 100644 --- a/crates/weaver-app/src/main.rs +++ b/crates/weaver-app/src/main.rs @@ -281,12 +281,14 @@ fn App() -> Element { rsx! { document::Link { rel: "icon", href: FAVICON } - document::Link { rel: "stylesheet", href: MAIN_CSS } - document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Serif:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" } + // Preconnect for external fonts (before loading them) document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" } - + // Theme defaults first: CSS variables, font-faces, reset document::Link { rel: "stylesheet", href: THEME_DEFAULTS_CSS } + document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100..700;1,100..700&family=IBM+Plex+Serif:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;1,200;1,300;1,400;1,500;1,600;1,700&display=swap" } + // App shell styles (depends on theme variables) + document::Link { rel: "stylesheet", href: MAIN_CSS } components::toast::ToastProvider { Router:: {} } diff --git a/crates/weaver-app/src/views/home.rs b/crates/weaver-app/src/views/home.rs index ff6946f..10ad64e 100644 --- a/crates/weaver-app/src/views/home.rs +++ b/crates/weaver-app/src/views/home.rs @@ -65,9 +65,8 @@ pub fn SiteOgMeta() -> Element { } } -const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); +// Card styles (entry-card, notebook-card) loaded at navbar level const ENTRY_CSS: Asset = asset!("/assets/styling/entry.css"); -const ENTRY_CARD_CSS: Asset = asset!("/assets/styling/entry-card.css"); const HOME_CSS: Asset = asset!("/assets/styling/home.css"); /// The Home page component that will be rendered when the current route is `[Route::Home]` @@ -86,9 +85,7 @@ pub fn Home() -> Element { SiteOgMeta {} document::Link { rel: "stylesheet", href: HOME_CSS } - document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } document::Link { rel: "stylesheet", href: ENTRY_CSS } - document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } DefaultNotebookCss { } div { class: "home-container", @@ -156,7 +153,8 @@ fn PinnedNotebookCard(ident: AtIdentifier<'static>, title: SmolStr) -> Element { Some((view, entries)) => rsx! { NotebookCard { notebook: view.clone(), - entry_refs: entries.clone() + entry_refs: entries.clone(), + show_author: Some(true) } }, None => rsx! { diff --git a/crates/weaver-app/src/views/navbar.rs b/crates/weaver-app/src/views/navbar.rs index 8e07642..552fbda 100644 --- a/crates/weaver-app/src/views/navbar.rs +++ b/crates/weaver-app/src/views/navbar.rs @@ -10,6 +10,9 @@ use jacquard::types::ident::AtIdentifier; use jacquard::types::string::Did; const NAVBAR_CSS: Asset = asset!("/assets/styling/navbar.css"); +const CARDS_BASE_CSS: Asset = asset!("/assets/styling/cards-base.css"); +const ENTRY_CARD_CSS: Asset = asset!("/assets/styling/entry-card.css"); +const NOTEBOOK_CARD_CSS: Asset = asset!("/assets/styling/notebook-card.css"); /// The Navbar component that will be rendered on all pages of our app since every page is under the layout. /// @@ -61,6 +64,9 @@ pub fn Navbar() -> Element { rsx! { document::Link { rel: "stylesheet", href: NAVBAR_CSS } document::Link { rel: "stylesheet", href: asset!("/assets/styling/button.css") } + document::Link { rel: "stylesheet", href: CARDS_BASE_CSS } + document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } + document::Link { rel: "stylesheet", href: NOTEBOOK_CARD_CSS } div { id: "navbar", nav { class: "breadcrumbs", diff --git a/crates/weaver-app/src/views/notebook.rs b/crates/weaver-app/src/views/notebook.rs index c97db1b..ac9ae00 100644 --- a/crates/weaver-app/src/views/notebook.rs +++ b/crates/weaver-app/src/views/notebook.rs @@ -44,7 +44,8 @@ pub fn NotebookOgMeta( } } -const ENTRY_CARD_CSS: Asset = asset!("/assets/styling/entry-card.css"); +// Card styles loaded at navbar level +const LAYOUTS_CSS: Asset = asset!("/assets/styling/layouts.css"); /// The Blog page component that will be rendered when the current route is `[Route::Blog]` /// @@ -96,7 +97,7 @@ pub fn NotebookIndex( }; rsx! { - document::Link { rel: "stylesheet", href: ENTRY_CARD_CSS } + document::Link { rel: "stylesheet", href: LAYOUTS_CSS } match (&*notebook_data.read(), &*entries_resource.read()) { (Some(data), Some(entries)) => { diff --git a/crates/weaver-renderer/src/css.rs b/crates/weaver-renderer/src/css.rs index 3bf1319..0fc8705 100644 --- a/crates/weaver-renderer/src/css.rs +++ b/crates/weaver-renderer/src/css.rs @@ -1,9 +1,11 @@ use crate::theme::{ResolvedTheme, ThemeDarkCodeTheme, ThemeLightCodeTheme}; use miette::IntoDiagnostic; +use smol_str::format_smolstr; 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::FontValue; use weaver_common::jacquard::client::BasicClient; use weaver_common::jacquard::prelude::*; use weaver_common::jacquard::xrpc::XrpcExt; @@ -18,6 +20,38 @@ pub fn generate_base_css(theme: &ResolvedTheme) -> String { let fonts = &theme.fonts; let spacing = &theme.spacing; + // interim until handle fonts from blobs + let body = fonts + .body + .iter() + .filter_map(|f| match &f.value { + FontValue::FontName(cow_str) => Some(format_smolstr!("'{cow_str}'")), + FontValue::FontFile(_font_file) => None, + FontValue::Unknown(_data) => None, + }) + .collect::>() + .join(","); + let monospace = fonts + .monospace + .iter() + .filter_map(|f| match &f.value { + FontValue::FontName(cow_str) => Some(format_smolstr!("'{cow_str}'")), + FontValue::FontFile(_font_file) => None, + FontValue::Unknown(_data) => None, + }) + .collect::>() + .join(","); + let heading = fonts + .heading + .iter() + .filter_map(|f| match &f.value { + FontValue::FontName(cow_str) => Some(format_smolstr!("'{cow_str}'")), + FontValue::FontFile(_font_file) => None, + FontValue::Unknown(_data) => None, + }) + .collect::>() + .join(","); + format!( r#"/* CSS Reset */ *, *::before, *::after {{ @@ -930,9 +964,9 @@ hr {{ light.link, light.highlight, // Fonts and spacing - fonts.body, - fonts.heading, - fonts.monospace, + body, + heading, + monospace, spacing.base_size, spacing.line_height, spacing.scale, diff --git a/crates/weaver-renderer/src/theme.rs b/crates/weaver-renderer/src/theme.rs index 7680e4a..91800a3 100644 --- a/crates/weaver-renderer/src/theme.rs +++ b/crates/weaver-renderer/src/theme.rs @@ -1,5 +1,8 @@ use miette::IntoDiagnostic; pub use weaver_api::sh_weaver::notebook::colour_scheme::{ColourScheme, ColourSchemeColours}; +use weaver_api::sh_weaver::notebook::theme::Font; +use weaver_api::sh_weaver::notebook::theme::FontName; +use weaver_api::sh_weaver::notebook::theme::FontValue; pub use weaver_api::sh_weaver::notebook::theme::{ Theme, ThemeDarkCodeTheme, ThemeFonts, ThemeLightCodeTheme, ThemeSpacing, }; @@ -54,16 +57,63 @@ pub fn default_colour_scheme_dark() -> ColourSchemeColours<'static> { pub fn default_fonts() -> ThemeFonts<'static> { ThemeFonts { // Serif for body text, sans for headings/UI - body: CowStr::new_static( - "'Adobe Caslon Pro', 'Latin Modern Roman', 'CM Serif', Georgia, serif", - ), - heading: CowStr::new_static( - "'IBM Plex Sans', 'CM Sans','Junction', 'Proza Libre', system-ui, sans-serif", - ), - monospace: CowStr::new_static( - "'Ioskeley Mono', 'IBM Plex Mono', 'Berkeley Mono', Consolas, monospace", - ), - ..Default::default() + body: vec![ + Font::new() + .value(FontValue::FontName( + CowStr::new_static("Adobe Caslon Pro").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName( + CowStr::new_static("Latin Modern Roman").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName( + CowStr::new_static("Times New Roman").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName(CowStr::new_static("serif").into())) + .build(), + ], + heading: vec![ + Font::new() + .value(FontValue::FontName( + CowStr::new_static("IBM Plex Sans").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName(CowStr::new_static("system-ui").into())) + .build(), + Font::new() + .value(FontValue::FontName(CowStr::new_static("sans-serif").into())) + .build(), + ], + monospace: vec![ + Font::new() + .value(FontValue::FontName( + CowStr::new_static("Ioskeley Mono").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName( + CowStr::new_static("IBM Plex Mono").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName( + CowStr::new_static("Berkeley Mono").into(), + )) + .build(), + Font::new() + .value(FontValue::FontName(CowStr::new_static("Consolas").into())) + .build(), + Font::new() + .value(FontValue::FontName(CowStr::new_static("monospace").into())) + .build(), + ], + extra_data: None, } } diff --git a/lexicons/notebook/theme.json b/lexicons/notebook/theme.json index cc3441a..73bb043 100644 --- a/lexicons/notebook/theme.json +++ b/lexicons/notebook/theme.json @@ -28,7 +28,7 @@ "fonts": { "type": "object", "required": ["body", "heading", "monospace"], - "description": "Fonts to be used in the notebook. Can specify a name or list of names (will load if available) or a file or list of files for each. Empty lists will use site defaults." + "description": "Fonts to be used in the notebook. Can specify a name or list of names (will load if available) or a file or list of files for each. Empty lists will use site defaults.", "properties": { "body": { "type": "array", @@ -86,12 +86,16 @@ }, "codeThemeFile": { "type": "object", - "required": ["name", "content"], + "required": ["name", "did", "content"], "description": "Custom syntax highlighting theme file (sublime text/textmate theme format)", "properties": { "name": { "type": "string" }, + "did": { + "type": "string", + "format": "did" + }, "content": { "type": "blob", "accept": ["*/*"], @@ -115,12 +119,16 @@ }, "fontFile": { "type": "object", - "required": ["name", "content"], - "description": "Custom woff(2) or ttf font file" + "required": ["name", "did", "content"], + "description": "Custom woff(2) or ttf font file", "properties": { "name": { "type": "string" }, + "did": { + "type": "string", + "format": "did" + }, "content": { "type": "blob", "accept": ["*/*"],