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
8 changes: 4 additions & 4 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -892,11 +892,11 @@ pub fn run(cli_args: CliArgs) {

let mut settings = get_settings(app.handle());

// Apply the persisted appearance theme to the Windows title bar before
// Apply the persisted appearance theme to the native title bar before
// the window is shown, so it matches the in-app palette without a flash
// of the wrong theme. On macOS/Linux, Tauri themes are app-wide and
// would also affect windows that intentionally keep the system theme.
#[cfg(target_os = "windows")]
// of the wrong theme. See `apply_window_theme` for what this does per
// platform.
#[cfg(any(target_os = "windows", target_os = "macos"))]
shortcut::apply_window_theme(app.handle(), settings.theme);

// CLI --debug flag overrides debug_mode and log level (runtime-only, not persisted)
Expand Down
20 changes: 14 additions & 6 deletions src-tauri/src/shortcut/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -584,16 +584,24 @@ pub fn change_theme_setting(app: AppHandle, theme: String) -> Result<(), String>
};
settings.theme = parsed;
settings::write_settings(&app, settings);
#[cfg(target_os = "windows")]
#[cfg(any(target_os = "windows", target_os = "macos"))]
apply_window_theme(&app, parsed);
// Notify other webviews (the recording overlay) so they re-apply the palette
// live — they set `data-theme` on their own document and can't see this one.
let _ = app.emit("theme-changed", parsed);
Ok(())
}

/// Applies the appearance setting to the Windows title bar, which CSS
/// `data-theme` cannot reach. `System` clears the override so the window follows
/// Windows. Call this on startup and whenever the setting changes to keep the
/// title bar in sync with the in-app palette.
#[cfg(target_os = "windows")]
/// Applies the appearance setting to the native window chrome (title bar), which
/// CSS `data-theme` cannot reach. `System` clears the override so the window
/// follows the OS. Call this on startup and whenever the setting changes to keep
/// the title bar in sync with the in-app palette.
///
/// On Windows this themes the title bar only. On macOS `set_theme` sets
/// `NSApp.appearance` app-wide, which is what we want here: it darkens the title
/// bar and keeps the overlay in step. Linux is left to `data-theme` alone, since
/// its window theming is backend-dependent and unreliable.
#[cfg(any(target_os = "windows", target_os = "macos"))]
pub fn apply_window_theme(app: &AppHandle, theme: Theme) {
let window_theme = match theme {
Theme::System => None,
Expand Down
25 changes: 0 additions & 25 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,31 +61,6 @@
}
}

/*
* Explicit theme override, driven by the `theme` setting via
* `document.documentElement.dataset.theme`. When set, the higher-specificity
* attribute selectors win over the `prefers-color-scheme` media query above,
* so a user can keep Handy light or dark regardless of the OS setting.
* `system` (or no attribute) leaves the OS in charge.
*/
:root[data-theme="light"] {
--color-text: var(--light-color-text);
--color-background: var(--light-color-background);
--color-logo-primary: var(--light-color-logo-primary);
--color-logo-stroke: var(--light-color-logo-stroke);
--color-warning: var(--light-color-warning);
--color-error: var(--light-color-error);
}

:root[data-theme="dark"] {
--color-text: var(--dark-color-text);
--color-background: var(--dark-color-background);
--color-logo-primary: var(--dark-color-logo-primary);
--color-logo-stroke: var(--dark-color-logo-stroke);
--color-warning: var(--dark-color-warning);
--color-error: var(--dark-color-error);
}

/* macOS - tint native overlay scrollbar thumb */
:root[data-platform="macos"] {
scrollbar-color: var(--scrollbar-thumb) transparent;
Expand Down
83 changes: 63 additions & 20 deletions src/overlay/RecordingOverlay.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,43 @@
--s-surface: color-mix(in srgb, var(--color-background) 98%, transparent);
/* Accent = logo pink; flips automatically via --color-logo-primary. */
--s-accent: var(--color-logo-primary);
--s-accent-soft: color-mix(
/* Light/dark pairs for the tokens that can't flip through a shared palette
variable on their own. Same shape as theme.css: each value is written once
here, and the active token below is re-pointed by the media query or by the
`data-theme` overrides. */
--light-s-accent-soft: color-mix(
in srgb,
var(--color-background-ui) 16%,
transparent
);
/* Neutrals. Hairlines reuse the app's mid-gray at low opacity — same vocabulary
as the app's border-mid-gray/* utilities. muted/faint are overlay-specific
secondary/tertiary text grays (no theme equivalent). */
--s-muted: #6e6e6e;
--s-faint: #9a9a9a;
--s-border: color-mix(in srgb, var(--color-mid-gray) 22%, transparent);
--s-hair: color-mix(in srgb, var(--color-mid-gray) 12%, transparent);
/* Dark uses the lighter logo pink — reads better on the dark surface than the
deeper UI pink. */
--dark-s-accent-soft: color-mix(
in srgb,
var(--color-logo-primary) 20%,
transparent
);
/* Neutrals. muted/faint are overlay-specific secondary/tertiary text grays
(no theme equivalent). */
--light-s-muted: #6e6e6e;
--dark-s-muted: #a3a09a;
--light-s-faint: #9a9a9a;
--dark-s-faint: #6f6c66;
/* Hairlines reuse the app's mid-gray at low opacity in light — same vocabulary
as the app's border-mid-gray/* utilities — and flip to the near-white text
token for contrast on dark. */
--light-s-border: color-mix(in srgb, var(--color-mid-gray) 22%, transparent);
--dark-s-border: color-mix(in srgb, var(--color-text) 12%, transparent);
--light-s-hair: color-mix(in srgb, var(--color-mid-gray) 12%, transparent);
--dark-s-hair: color-mix(in srgb, var(--color-text) 8%, transparent);

/* Active neutrals — default light, flipped by the media query below or forced
by `data-theme`. */
--s-accent-soft: var(--light-s-accent-soft);
--s-muted: var(--light-s-muted);
--s-faint: var(--light-s-faint);
--s-border: var(--light-s-border);
--s-hair: var(--light-s-hair);

/* Card geometry — single source of truth for the visible overlay sizes. The
native overlay window (overlay.rs) is sized from these: it must be at least
Expand All @@ -36,21 +61,39 @@
}
@media (prefers-color-scheme: dark) {
:root {
/* Accent uses the lighter logo pink in dark — reads better on the dark
surface than the deeper UI pink. */
--s-accent-soft: color-mix(
in srgb,
var(--color-logo-primary) 20%,
transparent
);
--s-muted: #a3a09a;
--s-faint: #6f6c66;
/* Hairlines flip to the near-white text token for contrast on dark. */
--s-border: color-mix(in srgb, var(--color-text) 12%, transparent);
--s-hair: color-mix(in srgb, var(--color-text) 8%, transparent);
--s-accent-soft: var(--dark-s-accent-soft);
--s-muted: var(--dark-s-muted);
--s-faint: var(--dark-s-faint);
--s-border: var(--dark-s-border);
--s-hair: var(--dark-s-hair);
}
}

/*
* Explicit theme override, matching the `data-theme` blocks in theme.css.
* `--s-surface` and `--s-accent` honour the setting for free because they derive
* from shared palette tokens, but the neutrals above are chosen here, so without
* this block a forced theme on a differently-themed OS mixes the two — e.g. dark
* surface with light-theme secondary text at ~2.8:1 contrast. macOS masks that
* (setting the app appearance flips the webview's `prefers-color-scheme` to
* match), so this is what Windows and Linux need.
*/
:root[data-theme="light"] {
--s-accent-soft: var(--light-s-accent-soft);
--s-muted: var(--light-s-muted);
--s-faint: var(--light-s-faint);
--s-border: var(--light-s-border);
--s-hair: var(--light-s-hair);
}

:root[data-theme="dark"] {
--s-accent-soft: var(--dark-s-accent-soft);
--s-muted: var(--dark-s-muted);
--s-faint: var(--dark-s-faint);
--s-border: var(--dark-s-border);
--s-hair: var(--dark-s-hair);
}

/* ============================================================
Unified overlay stage — one fixed, transparent container shared by the compact
pill and the Live panel (both render the same `.scard` base). The card is
Expand Down
15 changes: 15 additions & 0 deletions src/overlay/main.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,23 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { listen } from "@tauri-apps/api/event";
import RecordingOverlay from "./RecordingOverlay";
import {
applyTheme,
getStoredTheme,
syncThemeFromSettings,
} from "@/lib/utils/theme";
import type { Theme } from "@/bindings";
import "@/i18n";

// A separate webview from the settings window, so the overlay has to set
// `data-theme` on its own document: last-known theme before render (shared
// localStorage) to avoid a flash, reconcile with the persisted setting in case
// the overlay booted first, then follow live changes.
applyTheme(getStoredTheme());
syncThemeFromSettings();
listen<Theme>("theme-changed", (event) => applyTheme(event.payload));

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RecordingOverlay />
Expand Down
30 changes: 29 additions & 1 deletion src/styles/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
--color-mid-gray: #808080;

/* Active palette — defaults to light, follows the OS via the media query
below, or is forced by `data-theme` (see App.css). */
below, or is forced by `data-theme` (see the overrides below). */
--color-text: var(--light-color-text);
--color-background: var(--light-color-background);
--color-logo-primary: var(--light-color-logo-primary);
Expand All @@ -47,3 +47,31 @@
--color-error: var(--dark-color-error);
}
}

/*
* Explicit theme override, driven by the `theme` setting via
* `document.documentElement.dataset.theme`. When set, the higher-specificity
* attribute selectors win over the `prefers-color-scheme` media query above,
* so a user can keep Handy light or dark regardless of the OS setting.
* `system` (or no attribute) leaves the OS in charge.
*
* Lives here (not in App.css) so every window that imports the shared palette —
* the settings window and the recording overlay alike — honours the override.
*/
:root[data-theme="light"] {
--color-text: var(--light-color-text);
--color-background: var(--light-color-background);
--color-logo-primary: var(--light-color-logo-primary);
--color-logo-stroke: var(--light-color-logo-stroke);
--color-warning: var(--light-color-warning);
--color-error: var(--light-color-error);
}

:root[data-theme="dark"] {
--color-text: var(--dark-color-text);
--color-background: var(--dark-color-background);
--color-logo-primary: var(--dark-color-logo-primary);
--color-logo-stroke: var(--dark-color-logo-stroke);
--color-warning: var(--dark-color-warning);
--color-error: var(--dark-color-error);
}