makeover 0.9.0

Shared theme loading for the make-family apps: TOML theme files parsed into intent-based color tokens, with perceptual derivations and WCAG contrast.
Documentation
# makeover

Shared theme loading for the make-family apps. Parses theme metadata and color values from `.toml` files on disk, resolves them into intent-based tokens, and derives the rest perceptually (OKLab) with WCAG contrast checks.

The crate ships the theme set it loads, in `themes/`. Consumers get working themes from a clean checkout without depending on any sibling repo.

Used by MNW server, GoingsOn, Balanced Breakfast, audiofiles, and Alloy.

The crate ships 30 themes. `bundled_themes_dir()` hands back the directory; `embedded_themes()` hands back the same set as `(id, toml_source)` pairs with no filesystem path involved, for consumers that embed at compile time or bundle assets at build time.

## Usage

```rust
use makeover::{load_theme, list_themes_from_dirs, bundled_themes_dir};
use std::path::PathBuf;

// Set up theme directories (later entries override earlier ones)
let bundled = bundled_themes_dir().expect("makeover ships themes/");
let custom = PathBuf::from("/path/to/user/custom-themes");
let dirs = vec![(bundled, false), (custom, true)];

// List available themes (sorted by name)
let themes = list_themes_from_dirs(&dirs);
for t in &themes {
    println!("{} ({}, {})", t.name, t.id, t.variant);
}

// Load a specific theme by ID
let theme = load_theme(&dirs, "catppuccin-mocha").unwrap();
println!("Name: {}", theme.meta.name);         // "Catppuccin Mocha"
println!("Variant: {}", theme.meta.variant);     // "dark"
println!("BG: {}", theme.colors["surface.page"]);        // "#1e1e2e"

// Build-from-source fallback: the themes this crate ships
if let Some(dir) = bundled_themes_dir() {
    // dir = <makeover checkout>/themes
}
```

## Theme File Format

Colors are declared by **intent** (the role a color plays), not by hue. See `themes/` for the 30 bundled themes.

```toml
[meta]
name = "Nord"             # Display name; falls back to the filename
variant = "dark"          # "dark", "light", or "high-contrast" (default: "dark")

[surface]                 # Container backgrounds by elevation
page = "#2e3440"
raised = "#3b4252"
sunken = "#434c5e"
overlay = "#3b4252"

[content]                 # Text and ink by emphasis
primary = "#d8dee9"
secondary = "#e5e9f0"
muted = "#616e88"

[action]                  # Interactive / brand color
primary = "#81a1c1"

[status]                  # State semantics
danger = "#bf616a"
success = "#a3be8c"
warning = "#ebcb8b"
info = "#88c0d0"

[line]
border = "#4c566a"

[category]                # Optional: tag and label colors
```

### Derived tokens

Interactive states are not authored. `resolve()` derives hover, active,
selection, row striping, and contrast pairings perceptually in OKLab, so theme
files stay small and every consuming app derives them identically rather than
each recomputing its own.

`intent_css_vars()` renders a resolved theme as a `:root { … }` block for web
consumers; native consumers read RGB tuples off the same resolved tokens.

### Theme ID

The theme ID is the filename without `.toml` (e.g., `catppuccin-mocha.toml` has ID `catppuccin-mocha`). IDs must contain only alphanumeric characters, hyphens, and underscores. Path traversal characters are rejected.

## API

| Function | Description |
|----------|-------------|
| `list_themes_from_dirs(dirs)` | Scan directories for `.toml` files, return sorted `Vec<ThemeMeta>` |
| `load_theme(dirs, id)` | Load a theme by ID, returning `ThemeColors` (metadata + color map) |
| `find_theme_path(dirs, id)` | Find the file path for a theme ID (highest-priority directory wins) |
| `parse_meta(id, table, is_custom)` | Parse `[meta]` from a TOML table into `ThemeMeta` |
| `extract_colors(table)` | Flatten color sections into a `HashMap<String, String>` |
| `validate_theme_id(id)` | Check that an ID contains only safe characters |
| `bundled_themes_dir()` | The `themes/` directory this crate ships, for build-from-source fallback |
| `embedded_themes()` | The shipped themes as `(id, toml_source)` pairs, embedded at compile time (no path needed) |
| `parse_theme_str(id, source, is_custom)` | Parse a theme from a string, for use with `embedded_themes()` |
| `resolve(theme)` | Resolve authored intents into the full token set, deriving interactive states |
| `intent_css_vars(tokens)` | Render resolved tokens as a `:root { … }` CSS block |

## Directory Priority

`list_themes_from_dirs` and `load_theme` accept a list of `(PathBuf, bool)` pairs. Later directories override earlier ones by theme ID. The `bool` marks whether the directory contains user-custom themes (`is_custom` on `ThemeMeta`).

Typical setup for a Tauri app:
1. Bundled themes, packaged by the app or from `bundled_themes_dir()` (is_custom = false)
2. User themes from an app data directory (is_custom = true)

## License

MIT.

The themes in `themes/` adapt palettes from third-party projects (Catppuccin, Dracula, Nord, gruvbox, Tokyo Night, Rosé Pine, and others). Each upstream, its license, and the exact copyright line that license requires reproducing are recorded in [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), verified against the upstream LICENSE files themselves. Every adapted theme file also carries that information in a header comment, so credit travels with the file.

If an attribution is wrong or you would prefer your work not be included, write to info@makenot.work and it will be corrected or removed.