refactor: convert to bun-workspaces monorepo (desktop/ + www/)

- Move the Tauri app (React frontend + Rust backend) into desktop/ via
  git mv — configs unchanged (relative paths: tauri.conf.json, vite.config.ts)
- Root package.json becomes a private bun workspace container with
  orchestration scripts; desktop package renamed gridline-desktop
- Scaffold www/ with Astro 7 + Tailwind v4 (hand-wired vite plugin,
  static output, ready for Dokploy)
- Update release.yml for the new layout: projectPath: desktop,
  desktop/src-tauri resource paths, rust-cache workspace path
- Update README + AGENTS.md structure trees and dev commands
- Verified: vitest (1074), cargo test (354), desktop build, tauri dev
  (window launches), tauri build (dmg + app bundles), Astro build
This commit is contained in:
2026-08-16 19:31:53 +08:00
parent d752642b4d
commit 6854d889c5
400 changed files with 857 additions and 149 deletions
+116
View File
@@ -0,0 +1,116 @@
import { useEffect } from "react";
import type { Theme, FontSize } from "../lib/types";
const LIGHT_COLOR_SCHEME_QUERY = "(prefers-color-scheme: light)";
/**
* Resolves a Theme setting to the concrete color scheme it maps to (pure).
* For "system" this reads the webview's prefers-color-scheme, which correctly
* mirrors the OS only while the native window is NOT forced to a specific
* theme (see applyTheme's system handling).
*/
export function resolveTheme(theme: Theme): "light" | "dark" {
if (theme === "dark") return "dark";
if (theme === "light") return "light";
// "system" — follow the OS preference. Guard for environments without matchMedia.
if (typeof window.matchMedia !== "function") return "dark";
return window.matchMedia(LIGHT_COLOR_SCHEME_QUERY).matches ? "light" : "dark";
}
/**
* Syncs the native window chrome. Pass null to reset the window to follow the
* OS theme — required for the "system" setting, because forcing the window
* theme changes the webview's prefers-color-scheme (WKWebView follows the
* window appearance), which would otherwise pollute matchMedia.
*/
async function syncWindowTheme(effective: "light" | "dark" | null): Promise<void> {
try {
// Dynamic import keeps the Tauri API out of the hot path for
// non-Tauri bundles and non-Tauri test environments.
const { getCurrentWindow } = await import("@tauri-apps/api/window");
const win = getCurrentWindow();
if (effective === null) {
await win.setTheme(null);
return;
}
await win.setTheme(effective);
// macOS "Overlay" title bar paints the WINDOW background color in the
// title bar strip; keep it in sync with the theme.
await win.setBackgroundColor(effective === "light" ? "#FAFAFA" : "#0A0A0B");
} catch {
// Outside Tauri — nothing to sync.
}
}
/**
* Applies the theme to the document root and returns a cleanup that
* stops tracking the system preference while the theme is "system".
* Also syncs the native window chrome (fire-and-forget; noop outside Tauri).
*/
export function applyTheme(theme: Theme): () => void {
const root = document.documentElement;
if (theme === "dark") {
root.classList.remove("light");
void syncWindowTheme("dark");
return () => {};
}
if (theme === "light") {
root.classList.add("light");
void syncWindowTheme("light");
return () => {};
}
// "system" — follow the OS preference and keep it in sync.
// Guard for environments without matchMedia (e.g. jsdom).
if (typeof window.matchMedia !== "function") {
root.classList.remove("light");
void syncWindowTheme(null);
return () => {};
}
const mq = window.matchMedia(LIGHT_COLOR_SCHEME_QUERY);
const applySystem = () => {
root.classList.toggle("light", mq.matches);
void syncWindowTheme(null);
};
applySystem();
// Reset the native window to follow the OS first, then re-read matchMedia
// once it actually mirrors the OS — the immediate applySystem above may be
// stale if the window was previously forced to the other theme.
void syncWindowTheme(null).then(applySystem);
mq.addEventListener("change", applySystem);
return () => mq.removeEventListener("change", applySystem);
}
/** Applies the font-size scale by toggling a data attribute on the root. */
export function applyFontSize(fontSize: FontSize): void {
const root = document.documentElement;
if (fontSize === "medium") {
delete root.dataset.fontSize;
} else {
root.dataset.fontSize = fontSize;
}
}
/**
* Applies the accent color as a CSS custom property on the root. Invalid or
* non-hex values fall back to the theme default (via removal).
*/
export function applyAccentColor(accent: string): void {
const root = document.documentElement;
if (/^#[0-9a-fA-F]{6}$/.test(accent)) {
root.style.setProperty("--color-accent", accent);
} else {
root.style.removeProperty("--color-accent");
}
}
/** Keeps the document appearance in sync with the theme/font-size settings. */
export function useAppearance(theme: Theme, fontSize: FontSize, accentColor: string): void {
useEffect(() => {
const cleanupTheme = applyTheme(theme);
applyFontSize(fontSize);
applyAccentColor(accentColor);
return cleanupTheme;
}, [theme, fontSize, accentColor]);
}