Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,25 @@
*.bat text eol=crlf

*.sh text eol=lf

###############################################################################
# Agent tooling. It is configuration for the machines that help write this, not
# part of the program: kept out of the language statistics, and out of the
# archives GitHub builds for a release.
###############################################################################
.claude/** linguist-vendored export-ignore
.agents/** linguist-vendored export-ignore
.github/** export-ignore

###############################################################################
# Generated, and large enough to bury a diff. `registry-themes.ts` is written by
# `scripts/fetch-registry-themes.mjs` and `reference/theme.css` by
# `scripts/write-reference-theme.mjs`; neither is edited by hand.
###############################################################################
src/lib/theme/registry-themes.ts linguist-generated
reference/theme.css linguist-generated
package-lock.json linguist-generated

# Binary, so git offers no line diff and no merge it could get wrong.
*.png binary
*.ico binary
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

# Every pull request, and every push to main so the badge reflects what is actually on the branch.
on:
pull_request:
push:
branches: [main]

permissions:
contents: read

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
web:
name: Web
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run check
- run: npm test
- run: npm run build

host:
name: Host
# The Tauri host is built for Windows, so its tests run where it ships.
runs-on: windows-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- run: cargo fmt --manifest-path src-tauri/Cargo.toml -- --check
- run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
- run: cargo test --manifest-path src-tauri/Cargo.toml
48 changes: 38 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<img src="src-tauri/icons/128x128.png" alt="" width="72" align="left" hspace="12" />

# OpenThemeGenerator

[![CI](https://github.com/thorstenalpers/OpenThemeGenerator/actions/workflows/ci.yml/badge.svg)](https://github.com/thorstenalpers/OpenThemeGenerator/actions/workflows/ci.yml)
[![Licence: MIT](https://img.shields.io/badge/licence-MIT-blue.svg)](LICENSE)

A desktop theme generator for shadcn — and for anything else that reads CSS custom properties.

Pick a seed colour, and the generator derives a complete light and dark palette: the shadcn token
Expand Down Expand Up @@ -140,6 +145,11 @@ program is the hosted assistant, and the local binary is the default.

## Development

You need **Node 22**, a **Rust toolchain**, and on Windows the **WebView2 runtime** — which ships
with Windows 11 and every recent 10. Nothing else: the icons, the reference stylesheet and the
imported theme list are all produced by scripts in this repository rather than fetched at build
time.

```bash
npm install
npm run dev
Expand All @@ -155,16 +165,34 @@ starts drifting from the first. For the real thing:
npm run start
```

| Command | What it does |
| ------------------------------------------------- | -------------------------------------------------------- |
| `npm run check` | `svelte-check` against the app tsconfig |
| `npm run lint` | prettier and eslint |
| `npm test` | vitest — colour maths, the generator, and every exporter |
| `npm run app:build` | the NSIS installer |
| `npm run icons` | regenerates the icon set from `scripts/make-icons.mjs` |
| `npm run reference:theme` | rewrites `reference/theme.css` from a built-in preset |
| `cargo test --manifest-path src-tauri/Cargo.toml` | the host's own tests |
| Command | What it does |
| --------------------------------------------------- | -------------------------------------------------------- |
| `npm run check` | `svelte-check` against the app tsconfig |
| `npm run lint` | prettier and eslint |
| `npm run format` | writes the formatting rather than checking it |
| `npm test` | vitest — colour maths, the generator, and every exporter |
| `npm run build` | the static front end |
| `npm run app:build` | the NSIS installer |
| `npm run icons` | regenerates the icon set from `scripts/make-icons.mjs` |
| `npm run reference:theme` | rewrites `reference/theme.css` from a built-in preset |
| `cargo test --manifest-path src-tauri/Cargo.toml` | the host's own tests |
| `cargo clippy --manifest-path src-tauri/Cargo.toml` | the lints CI fails on |

## Contributing

`main` is protected: changes arrive as a pull request, and CI has to be green before it can merge.
The workflow runs the table above — lint, types, tests and build on Linux, and `cargo fmt`,
`clippy` and the host's tests on Windows, where the app actually ships.

Two things the tests care about more than style:

- **Every theme this app derives clears 4.5:1** on every surface/label pair, in both modes. So does
`app.css`, the window's own chrome, which has its own test because the generator cannot keep it
honest. Imported themes are exempt — they are other people's designs, carried as published, and
the gallery reports what they score instead of retinting them.
- **Nothing reaches the network at runtime.** The imported theme list is generated into the
repository by `scripts/fetch-registry-themes.mjs` and read from disk.

## Licence

MIT.
[MIT](LICENSE).
11 changes: 7 additions & 4 deletions src-tauri/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ fn rank(name: &str) -> u8 {
}
}

fn collect(root: &Path, current: &Path, depth: usize, found: &mut Vec<PathBuf>) {
fn collect(current: &Path, depth: usize, found: &mut Vec<PathBuf>) {
if depth > MAX_DEPTH {
return;
}
Expand All @@ -125,7 +125,7 @@ fn collect(root: &Path, current: &Path, depth: usize, found: &mut Vec<PathBuf>)
// hold a git worktree of the whole project, and scanning that reads the same tokens a
// second time under a different path — half the prompt budget spent on a copy.
if !name.starts_with('.') && !SKIP.contains(&name) {
collect(root, &path, depth + 1, found);
collect(&path, depth + 1, found);
}
} else if is_wanted(&path, name) {
found.push(path);
Expand Down Expand Up @@ -207,7 +207,7 @@ pub fn scan(root: &str) -> AppResult<ProjectContext> {
}

let mut found = Vec::new();
collect(&base, &base, 0, &mut found);
collect(&base, 0, &mut found);
found.sort_by_key(|path| {
let name = path
.file_name()
Expand Down Expand Up @@ -356,7 +356,10 @@ mod tests {

write("src/app.css", "@import 'tailwindcss';");
write("components.json", "{}");
write(".claude/worktrees/copy/src/app.css", "@import 'tailwindcss';");
write(
".claude/worktrees/copy/src/app.css",
"@import 'tailwindcss';",
);
write(".claude/skills/vendored/package.json", "{}");
write("node_modules/some-package/package.json", "{}");

Expand Down