From 5317da94c4aab98c5c1efba29a83a32ba865d5db Mon Sep 17 00:00:00 2001 From: Timothy DeHerrera Date: Sat, 24 Jan 2026 20:27:22 -0700 Subject: [PATCH] feat: add minimal base CSS with dark mode support - static/style.css: CSS variable-based theming with: - Typography (system fonts, monospace for code) - Layout (centered content, nav/main/footer structure) - Dark mode via prefers-color-scheme - Component styles (posts, cards, tags, hero) - src/main.rs: Add copy_static_assets() to copy static/ directory to public/ during build Phase 1 complete. Ready for syntax highlighting. --- src/content.rs | 2 +- src/main.rs | 42 ++++++++ src/templates.rs | 2 +- static/style.css | 251 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 static/style.css diff --git a/src/content.rs b/src/content.rs index 046f607..37e053f 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,7 +1,7 @@ //! Content discovery and frontmatter parsing. use crate::error::{Error, Result}; -use gray_matter::{Matter, engine::YAML}; +use gray_matter::{engine::YAML, Matter}; use std::fs; use std::path::{Path, PathBuf}; diff --git a/src/main.rs b/src/main.rs index ba3ccae..e4c8811 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,11 +22,15 @@ fn main() { fn run() -> Result<()> { let content_dir = Path::new("content"); let output_dir = Path::new("public"); + let static_dir = Path::new("static"); if !content_dir.exists() { return Err(Error::ContentDirNotFound(content_dir.to_path_buf())); } + // 0. Copy static assets + copy_static_assets(static_dir, output_dir)?; + // 1. Process blog posts let mut posts = process_blog_posts(content_dir, output_dir)?; @@ -208,3 +212,41 @@ fn write_output( eprintln!(" → {}", out_path.display()); Ok(()) } + +/// Copy static assets (CSS, etc.) to output directory +fn copy_static_assets(static_dir: &Path, output_dir: &Path) -> Result<()> { + if !static_dir.exists() { + return Ok(()); // No static dir is fine + } + + fs::create_dir_all(output_dir).map_err(|e| Error::CreateDir { + path: output_dir.to_path_buf(), + source: e, + })?; + + for entry in walkdir::WalkDir::new(static_dir) + .into_iter() + .filter_map(|e| e.ok()) + .filter(|e| e.file_type().is_file()) + { + let src = entry.path(); + let relative = src.strip_prefix(static_dir).unwrap(); + let dest = output_dir.join(relative); + + if let Some(parent) = dest.parent() { + fs::create_dir_all(parent).map_err(|e| Error::CreateDir { + path: parent.to_path_buf(), + source: e, + })?; + } + + fs::copy(src, &dest).map_err(|e| Error::WriteFile { + path: dest.clone(), + source: e, + })?; + + eprintln!("copying: {} → {}", src.display(), dest.display()); + } + + Ok(()) +} diff --git a/src/templates.rs b/src/templates.rs index b552669..df809be 100644 --- a/src/templates.rs +++ b/src/templates.rs @@ -1,7 +1,7 @@ //! HTML templates using maud. use crate::content::{Content, Frontmatter}; -use maud::{DOCTYPE, Markup, PreEscaped, html}; +use maud::{html, Markup, PreEscaped, DOCTYPE}; /// Render a blog post with the base layout. pub fn render_post(frontmatter: &Frontmatter, content_html: &str) -> Markup { diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..f20208e --- /dev/null +++ b/static/style.css @@ -0,0 +1,251 @@ +/* nrd.sh - Minimal Base Styles */ + +:root { + /* Typography */ + --font-sans: system-ui, -apple-system, sans-serif; + --font-mono: ui-monospace, "Cascadia Code", "Fira Code", monospace; + --font-size: 1rem; + --line-height: 1.6; + + /* Spacing */ + --space-xs: 0.25rem; + --space-sm: 0.5rem; + --space-md: 1rem; + --space-lg: 2rem; + --space-xl: 4rem; + + /* Layout */ + --max-width: 48rem; + + /* Colors - Light */ + --bg: #fafafa; + --bg-alt: #f0f0f0; + --text: #1a1a1a; + --text-muted: #666; + --accent: #0066cc; + --accent-hover: #0052a3; + --border: #ddd; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #1a1a1a; + --bg-alt: #252525; + --text: #e0e0e0; + --text-muted: #999; + --accent: #6ab0f3; + --accent-hover: #8cc4f7; + --border: #333; + } +} + +/* Reset */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +/* Base */ +html { + font-family: var(--font-sans); + font-size: var(--font-size); + line-height: var(--line-height); + background: var(--bg); + color: var(--text); +} + +body { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +/* Layout */ +nav, main, footer { + width: 100%; + max-width: var(--max-width); + margin: 0 auto; + padding: var(--space-md); +} + +nav { + display: flex; + gap: var(--space-lg); + border-bottom: 1px solid var(--border); + padding-block: var(--space-md); +} + +main { + flex: 1; + padding-block: var(--space-lg); +} + +footer { + border-top: 1px solid var(--border); + padding-block: var(--space-md); + color: var(--text-muted); + font-size: 0.875rem; +} + +/* Typography */ +h1, h2, h3 { + line-height: 1.3; + margin-block: var(--space-lg) var(--space-md); +} + +h1 { font-size: 2rem; } +h2 { font-size: 1.5rem; } +h3 { font-size: 1.25rem; } + +h1:first-child, h2:first-child, h3:first-child { + margin-top: 0; +} + +p { + margin-block: var(--space-md); +} + +a { + color: var(--accent); + text-decoration: none; +} + +a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +/* Code */ +code, pre { + font-family: var(--font-mono); + font-size: 0.9em; +} + +code { + background: var(--bg-alt); + padding: var(--space-xs) var(--space-sm); + border-radius: 3px; +} + +pre { + background: var(--bg-alt); + padding: var(--space-md); + overflow-x: auto; + border-radius: 4px; + margin-block: var(--space-md); +} + +pre code { + background: none; + padding: 0; +} + +/* Lists */ +ul, ol { + margin-block: var(--space-md); + padding-left: var(--space-lg); +} + +li { + margin-block: var(--space-xs); +} + +/* Blockquote */ +blockquote { + border-left: 3px solid var(--accent); + padding-left: var(--space-md); + margin-block: var(--space-md); + color: var(--text-muted); +} + +/* Post */ +article.post header { + margin-bottom: var(--space-lg); +} + +.date { + color: var(--text-muted); + font-size: 0.9rem; +} + +.description { + color: var(--text-muted); + font-style: italic; +} + +.tags { + list-style: none; + display: flex; + gap: var(--space-sm); + padding: 0; + margin-top: var(--space-sm); +} + +.tags li a { + background: var(--bg-alt); + padding: var(--space-xs) var(--space-sm); + border-radius: 3px; + font-size: 0.85rem; +} + +/* Post List */ +.post-list { + list-style: none; + padding: 0; +} + +.post-list li { + margin-block: var(--space-md); + padding-bottom: var(--space-md); + border-bottom: 1px solid var(--border); +} + +.post-list li:last-child { + border-bottom: none; +} + +.post-list .title { + display: block; + font-weight: 600; +} + +/* Project Cards */ +.project-cards { + list-style: none; + padding: 0; + display: grid; + gap: var(--space-md); +} + +.card { + background: var(--bg-alt); + padding: var(--space-md); + border-radius: 4px; +} + +.card h2 { + font-size: 1.1rem; + margin: 0 0 var(--space-sm); +} + +.card p { + margin: 0; + color: var(--text-muted); + font-size: 0.9rem; +} + +/* Hero */ +.hero { + text-align: center; + padding-block: var(--space-xl); +} + +.hero h1 { + font-size: 2.5rem; +} + +.tagline { + font-size: 1.25rem; + color: var(--text-muted); +}