Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
0f7245b
feat: Implement ThemeColorEditor with preset themes and custom color …
spencrmartin Feb 13, 2026
a1e6790
fix: Add DialogDescription for accessibility compliance
spencrmartin Feb 13, 2026
aae5b6f
fix: Add ToSchema derives and register theme endpoints in OpenAPI
spencrmartin Feb 13, 2026
4129305
fix: Define ThemePreset DTOs in config_management to avoid module pat…
spencrmartin Feb 13, 2026
0975e28
fix: Use authenticated API client instead of raw fetch for theme presets
spencrmartin Feb 13, 2026
eb5e9da
fix: Replace invalid bg-background-default with bg-background-primary…
spencrmartin Feb 13, 2026
2559bca
feat: Add split-panel live preview to Custom Colors tab
spencrmartin Feb 13, 2026
daea086
feat: Transform Theme Editor into full-screen builder flow
spencrmartin Feb 13, 2026
af8816c
fix: Force dialog to true full-screen with !important overrides
spencrmartin Feb 13, 2026
3daa7b2
feat: Replace previews with 1:1 exact replicas of real Goose UI compo…
spencrmartin Feb 13, 2026
9300f3b
feat: Use exact sidebar menu structure in preview with real icons
spencrmartin Feb 13, 2026
82390e2
feat: Add 7 new theme presets and fix header spacing for macOS stoplight
spencrmartin Feb 13, 2026
4c0da49
feat: Convert action buttons to icon-only with tooltips
spencrmartin Feb 13, 2026
7596069
fix: Use RotateCcw icon instead of non-existent Refresh
spencrmartin Feb 13, 2026
01d8362
feat: Add icons to tab buttons and make them full-width
spencrmartin Feb 13, 2026
18021aa
fix: Make preset gallery use full vertical height
spencrmartin Feb 13, 2026
b886b3b
feat: Custom Colors now inherits system theme mode automatically
spencrmartin Feb 13, 2026
7dc6587
feat: Show usage context as subtext below each color title
spencrmartin Feb 13, 2026
bcf0af9
feat: Center preview components vertically in right panel
spencrmartin Feb 13, 2026
c85fefa
feat: Remove all labels and wrappers from preview components - pure i…
spencrmartin Feb 13, 2026
0351fb7
feat: Replace gradient color picker with simplified square-based picker
spencrmartin Feb 13, 2026
e29eaa7
feat: Make color picker squares smaller and more compact
spencrmartin Feb 13, 2026
e760d21
feat: Expand shade grid to 8x10 for more precise color selection
spencrmartin Feb 13, 2026
ca59420
feat: Expand shade grid to 10x15 for maximum color precision
spencrmartin Feb 13, 2026
a5f9e8f
feat: Double hue options from 12 to 24 for complete spectrum coverage
spencrmartin Feb 13, 2026
f89c13f
feat: Expand grayscale from 6 to 16 shades for precise neutral control
spencrmartin Feb 13, 2026
24d693f
feat: Make color picker squares scale dynamically with aspect-square
spencrmartin Feb 13, 2026
1ec749e
fix: Remove massive white square by including white in grayscale row
spencrmartin Feb 13, 2026
3574b79
feat: Auto-select closest hue when color picker opens
spencrmartin Feb 13, 2026
9c5dcf9
feat: Simplify danger background preview to just show the button
spencrmartin Feb 13, 2026
3f0fdd5
feat: Simplify secondary background preview to just show sidebar menu
spencrmartin Feb 13, 2026
dbaea99
feat: Split preview layout - header top-aligned, examples centered
spencrmartin Feb 13, 2026
e33c6c0
fix: Force user message text to use inverse color in all themes
spencrmartin Feb 13, 2026
5beb85e
feat: Live theme preview - all colors update dynamically in preview p…
spencrmartin Feb 13, 2026
14155af
feat: Update inverse background preview with action button + user cha…
spencrmartin Feb 13, 2026
25315bf
feat: Replace emoji with Pipette icon in empty state
spencrmartin Feb 13, 2026
e1a6ca1
feat: Theme presets show only current mode colors (light or dark)
spencrmartin Feb 13, 2026
7670579
feat: Make preset theme color preview squares smaller
spencrmartin Feb 13, 2026
a89e8bf
feat: Show 4 color squares in preset theme previews
spencrmartin Feb 13, 2026
a6bb362
feat: Make preset theme cards narrower with more columns
spencrmartin Feb 13, 2026
41ac814
feat: Remove search bar from theme presets - only keep tag filters
spencrmartin Feb 13, 2026
e138c72
feat: Icon-only apply button at card bottom with applied state indicator
spencrmartin Feb 13, 2026
0893983
feat: Make tab buttons hug content instead of full-width
spencrmartin Feb 13, 2026
2aff4ea
feat: Add custom theme info card at top of color picker panel
spencrmartin Feb 13, 2026
fc31ace
feat: Make theme card color preview 3x taller (h-8 to h-24)
spencrmartin Feb 13, 2026
612c6b0
feat: Bottom-align theme info, preview, and button in preset cards
spencrmartin Feb 13, 2026
d1eb0a9
fix: Keep color preview at top, bottom-align info and button
spencrmartin Feb 13, 2026
a87536b
feat: Add ability to save custom theme
spencrmartin Feb 13, 2026
0f72b31
feat: Save custom themes as reusable presets
spencrmartin Feb 13, 2026
5c86ab8
chore: Regenerate OpenAPI spec with custom theme endpoints
spencrmartin Feb 13, 2026
1adc4cb
feat: Track active theme and add delete functionality
spencrmartin Feb 13, 2026
cebdada
fix: Use applyThemePreset to properly track active custom themes
spencrmartin Feb 13, 2026
f0274cf
fix: Include custom themes when looking up theme by ID
spencrmartin Feb 13, 2026
f50cdc5
fix: Add theme-api.ts file that was missing
spencrmartin Feb 13, 2026
ba66aef
fix: Improve color-text-info preview examples
spencrmartin Feb 14, 2026
13b37f1
fix: Add sidebar background color for mobile overlay
spencrmartin Feb 17, 2026
d19e740
feat: Add edit functionality for custom themes
spencrmartin Feb 17, 2026
78bfac9
fix: Replace hardcoded colors with theme variables in settings
spencrmartin Feb 17, 2026
ffc08c3
feat: Update info background preview to show Prompts Settings card
spencrmartin Feb 17, 2026
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
1 change: 1 addition & 0 deletions crates/goose-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod openapi;
pub mod routes;
pub mod state;
pub mod theme_css;
pub mod theme_presets;
pub mod tunnel;

// Re-export commonly used items
Expand Down
1 change: 1 addition & 0 deletions crates/goose-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod openapi;
mod routes;
mod state;
mod theme_css;
mod theme_presets;
mod tunnel;

use clap::{Parser, Subcommand};
Expand Down
11 changes: 11 additions & 0 deletions crates/goose-server/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,11 @@ derive_utoipa!(Icon as IconSchema);
super::routes::config_management::get_pricing,
super::routes::config_management::get_theme_variables,
super::routes::config_management::save_theme,
super::routes::config_management::get_theme_presets,
super::routes::config_management::get_active_theme,
super::routes::config_management::apply_theme_preset,
super::routes::config_management::save_custom_theme,
super::routes::config_management::delete_custom_theme,
super::routes::prompts::get_prompts,
super::routes::prompts::get_prompt,
super::routes::prompts::save_prompt,
Expand Down Expand Up @@ -450,6 +455,12 @@ derive_utoipa!(Icon as IconSchema);
super::routes::config_management::PricingData,
super::routes::config_management::ThemeVariablesResponse,
super::routes::config_management::SaveThemeRequest,
super::routes::config_management::ThemePresetsResponse,
super::routes::config_management::ThemePreset,
super::routes::config_management::ThemeColorsDto,
super::routes::config_management::ActiveThemeResponse,
super::routes::config_management::ApplyPresetRequest,
super::routes::config_management::SaveCustomThemeRequest,
super::routes::prompts::PromptsListResponse,
super::routes::prompts::PromptContentResponse,
super::routes::prompts::SavePromptRequest,
Expand Down
199 changes: 199 additions & 0 deletions crates/goose-server/src/routes/config_management.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::routes::errors::ErrorResponse;
use crate::routes::utils::check_provider_configured;
use crate::state::AppState;
use crate::theme_presets;
use axum::routing::put;
use axum::{
extract::Path,
Expand Down Expand Up @@ -883,6 +884,199 @@ pub async fn save_theme(Json(request): Json<SaveThemeRequest>) -> Result<Json<St
}
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct ThemePreset {
pub id: String,
pub name: String,
pub author: String,
pub description: String,
pub tags: Vec<String>,
pub colors: ThemeColorsDto,
pub version: String,
#[serde(default)]
pub is_custom: bool,
}

#[derive(Serialize, Deserialize, ToSchema)]
pub struct ThemeColorsDto {
pub light: HashMap<String, String>,
pub dark: HashMap<String, String>,
}

#[derive(Serialize, ToSchema)]
pub struct ThemePresetsResponse {
presets: Vec<ThemePreset>,
}

#[utoipa::path(
get,
path = "/theme/presets",
responses(
(status = 200, description = "List of all theme presets (built-in and custom)", body = ThemePresetsResponse)
)
)]
pub async fn get_theme_presets() -> Json<ThemePresetsResponse> {
let presets = theme_presets::get_all_presets_with_custom()
.into_iter()
.map(|p| ThemePreset {
id: p.id,
name: p.name,
author: p.author,
description: p.description,
tags: p.tags,
colors: ThemeColorsDto {
light: p.colors.light,
dark: p.colors.dark,
},
version: p.version,
is_custom: p.is_custom,
})
.collect();
Json(ThemePresetsResponse { presets })
}

#[derive(Serialize, ToSchema)]
pub struct ActiveThemeResponse {
theme_id: Option<String>,
}

#[utoipa::path(
get,
path = "/theme/active",
responses(
(status = 200, description = "Get the currently active theme ID", body = ActiveThemeResponse)
)
)]
pub async fn get_active_theme() -> Json<ActiveThemeResponse> {
let active_theme_path = Paths::in_data_dir("active_theme.txt");
let theme_id = std::fs::read_to_string(&active_theme_path)
.ok()
.map(|s| s.trim().to_string());

Json(ActiveThemeResponse { theme_id })
}

#[derive(Deserialize, ToSchema)]
pub struct ApplyPresetRequest {
preset_id: String,
}

#[utoipa::path(
post,
path = "/theme/apply-preset",
request_body = ApplyPresetRequest,
responses(
(status = 200, description = "Theme preset applied successfully", body = String),
(status = 404, description = "Theme preset not found"),
(status = 500, description = "Failed to apply theme preset")
)
)]
pub async fn apply_theme_preset(
Json(request): Json<ApplyPresetRequest>,
) -> Result<Json<String>, ErrorResponse> {
let preset = theme_presets::get_preset(&request.preset_id)
.ok_or_else(|| ErrorResponse::not_found(format!("Theme preset '{}' not found", request.preset_id)))?;

// Convert preset to CSS format
let mut css_lines = Vec::new();

// Light mode
css_lines.push(":root {".to_string());
for (key, value) in &preset.colors.light {
css_lines.push(format!(" --{}: {};", key, value));
}
css_lines.push("}".to_string());
css_lines.push("".to_string());

// Dark mode
css_lines.push(".dark {".to_string());
for (key, value) in &preset.colors.dark {
css_lines.push(format!(" --{}: {};", key, value));
}
css_lines.push("}".to_string());

let css = css_lines.join("\n");

// Save to theme.css
let theme_path = Paths::in_data_dir("theme.css");
std::fs::write(&theme_path, css)?;

// Store the active theme ID
let active_theme_path = Paths::in_data_dir("active_theme.txt");
std::fs::write(&active_theme_path, &request.preset_id)?;

Ok(Json(format!("Applied theme preset: {}", preset.name)))
}

#[derive(Deserialize, ToSchema)]
pub struct SaveCustomThemeRequest {
pub id: String,
pub name: String,
pub author: String,
pub description: String,
pub tags: Vec<String>,
pub colors: ThemeColorsDto,
}

#[utoipa::path(
post,
path = "/theme/save-custom",
request_body = SaveCustomThemeRequest,
responses(
(status = 200, description = "Custom theme saved successfully", body = String),
(status = 500, description = "Failed to save custom theme")
)
)]
pub async fn save_custom_theme(
Json(request): Json<SaveCustomThemeRequest>,
) -> Result<Json<String>, ErrorResponse> {
let theme = theme_presets::ThemePreset {
id: request.id.clone(),
name: request.name,
author: request.author,
description: request.description,
tags: request.tags,
colors: theme_presets::ThemeColors {
light: request.colors.light,
dark: request.colors.dark,
},
version: "1.0.0".to_string(),
is_custom: true,
};

theme_presets::save_custom_theme(theme)
.map_err(|e| ErrorResponse::internal(format!("Failed to save custom theme: {}", e)))?;

Ok(Json(format!("Custom theme '{}' saved successfully", request.id)))
}

#[utoipa::path(
delete,
path = "/theme/saved/{id}",
params(
("id" = String, Path, description = "Theme ID to delete")
),
responses(
(status = 200, description = "Custom theme deleted successfully", body = String),
(status = 404, description = "Theme not found"),
(status = 500, description = "Failed to delete theme")
)
)]
pub async fn delete_custom_theme(
Path(id): Path<String>,
) -> Result<Json<String>, ErrorResponse> {
theme_presets::delete_custom_theme(&id)
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
ErrorResponse::not_found(format!("Theme '{}' not found", id))
} else {
ErrorResponse::internal(format!("Failed to delete theme: {}", e))
}
})?;

Ok(Json(format!("Custom theme '{}' deleted successfully", id)))
}

pub fn routes(state: Arc<AppState>) -> Router {
Router::new()
.route("/config", get(read_all_config))
Expand Down Expand Up @@ -917,6 +1111,11 @@ pub fn routes(state: Arc<AppState>) -> Router {
)
.route("/theme/variables", get(get_theme_variables))
.route("/theme/save", post(save_theme))
.route("/theme/presets", get(get_theme_presets))
.route("/theme/active", get(get_active_theme))
.route("/theme/apply-preset", post(apply_theme_preset))
.route("/theme/save-custom", post(save_custom_theme))
.route("/theme/saved/{id}", delete(delete_custom_theme))
.with_state(state)
}

Expand Down
Loading