feat: add onyx dark theme support

Add onyx theme as a new dark theme option. Register it in the dark
themes set, add the theme option to the settings dropdown, and define
complete color variables and scrollbar styling for the onyx theme.

Also improve sync error handling to gracefully skip upload and conflict
resolution actions when files are missing, preventing sync failures due
to files moved or deleted between action computation and execution.
This commit is contained in:
Tristan Michael 2026-04-06 17:17:00 -07:00 committed by GitButler
parent c40cf15f08
commit 973d575b51
4 changed files with 51 additions and 3 deletions

View file

@ -139,6 +139,33 @@ body {
--color-danger: #ff5555; --color-danger: #ff5555;
} }
[data-theme="onyx"] {
--color-primary: #ffffff;
--color-primary-hover: #e0e0e0;
--color-surface-light: #0a0a0a;
--color-surface-dark: #0a0a0a;
--color-card-light: #161616;
--color-card-dark: #161616;
--color-text-light: #c4a95a;
--color-text-dark: #c4a95a;
--color-text-secondary-light: #917a3e;
--color-text-secondary-dark: #917a3e;
--color-border-light: #2a2418;
--color-border-dark: #2a2418;
--color-danger: #ef4444;
}
[data-theme="onyx"] * {
scrollbar-color: #917a3e transparent;
}
[data-theme="onyx"] ::-webkit-scrollbar-thumb {
background: #917a3e;
}
[data-theme="onyx"] select option {
background-color: #0a0a0a;
color: #c4a95a;
}
[data-theme="solarized"] { [data-theme="solarized"] {
--color-primary: #268bd2; --color-primary: #268bd2;
--color-primary-hover: #1e7ac0; --color-primary-hover: #1e7ac0;

View file

@ -259,6 +259,7 @@
<option value="nord">Nord</option> <option value="nord">Nord</option>
<option value="dracula">Dracula</option> <option value="dracula">Dracula</option>
<option value="solarized">Solarized Dark</option> <option value="solarized">Solarized Dark</option>
<option value="onyx">Onyx</option>
</select> </select>
</section> </section>

View file

@ -67,7 +67,7 @@ let hasWorkspace = $derived(
Object.keys(config.workspaces).length > 0, Object.keys(config.workspaces).length > 0,
); );
const DARK_THEMES = new Set(["dark", "nord", "dracula", "solarized"]); const DARK_THEMES = new Set(["dark", "nord", "dracula", "solarized", "onyx"]);
let currentTheme = $derived( let currentTheme = $derived(
config?.current_workspace config?.current_workspace
? config.workspaces[config.current_workspace]?.theme ?? null ? config.workspaces[config.current_workspace]?.theme ?? null

View file

@ -688,7 +688,18 @@ async fn execute_action(
match action { match action {
SyncAction::Upload { path } => { SyncAction::Upload { path } => {
let local_path = workspace_path.join(path.replace('/', std::path::MAIN_SEPARATOR_STR)); let local_path = workspace_path.join(path.replace('/', std::path::MAIN_SEPARATOR_STR));
let data = std::fs::read(&local_path)?; let data = match std::fs::read(&local_path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// File was moved or deleted since the action was computed (e.g. task
// moved between lists). Drop the stale upload and clean up sync state
// so the next cycle can re-evaluate from a clean baseline.
report(&format!(" - Skipping upload for missing file {}", path));
sync_state.files.remove(path.as_str());
return Ok(());
}
Err(e) => return Err(e.into()),
};
let checksum = compute_checksum(&data); let checksum = compute_checksum(&data);
if let Some(parent) = path_parent(path) { if let Some(parent) = path_parent(path) {
@ -707,7 +718,16 @@ async fn execute_action(
SyncAction::Conflict { path } => { SyncAction::Conflict { path } => {
let local_path = workspace_path.join(path.replace('/', std::path::MAIN_SEPARATOR_STR)); let local_path = workspace_path.join(path.replace('/', std::path::MAIN_SEPARATOR_STR));
let local_data = std::fs::read(&local_path)?; let local_data = match std::fs::read(&local_path) {
Ok(d) => d,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
// Local file gone — treat as remote-wins: download it on next cycle.
report(&format!(" - Skipping conflict for missing local file {}", path));
sync_state.files.remove(path.as_str());
return Ok(());
}
Err(e) => return Err(e.into()),
};
let local_checksum = compute_checksum(&local_data); let local_checksum = compute_checksum(&local_data);
let remote_data = client.get_file(path).await?; let remote_data = client.get_file(path).await?;