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.
This commit is contained in:
Timothy DeHerrera
2026-01-24 20:27:22 -07:00
parent d4003686a0
commit 5317da94c4
4 changed files with 295 additions and 2 deletions

View File

@@ -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};

View File

@@ -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(())
}

View File

@@ -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 {

251
static/style.css Normal file
View File

@@ -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);
}