A bunch more theme stuff

This commit is contained in:
Gregory Schier
2024-05-22 23:14:53 -07:00
parent 83aaeb94f6
commit 8e662e6feb
61 changed files with 8374 additions and 530 deletions

View File

@@ -36,11 +36,11 @@ jobs:
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
# webkitgtk 4.0 is for Tauri v1 - webkitgtk 4.1 is for Tauri v2.
# You can remove the one that doesn't apply to your app to speed up the workflow a bit.
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
- name: Install dependencies
run: npm ci
- name: Run lint
run: npm run lint
- name: Run tests
run: npm test
- uses: tauri-apps/tauri-action@v0

2
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@codemirror/commands": "^6.2.1",
"@codemirror/lang-javascript": "^6.1.4",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.6.0",

View File

@@ -29,7 +29,7 @@
},
"dependencies": {
"@codemirror/commands": "^6.2.1",
"@codemirror/lang-javascript": "^6.1.4",
"@codemirror/lang-javascript": "^6.2.2",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.6.0",

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme, appearance, update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "48ec5fdf20f34add763c540061caa25054545503704e19f149987f99b1a0e4f0"
}

View File

@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme, appearance, theme_dark, theme_light, update_channel\n ) = (?, ?, ?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "8e88c7070a34a6e151da66f521deeafaea9a12e2aa68081daaf235d2003b513d"
}

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance, update_channel\n FROM settings\n WHERE id = 'default'\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel\n FROM settings\n WHERE id = 'default'\n ",
"describe": {
"columns": [
{
@@ -34,9 +34,19 @@
"type_info": "Text"
},
{
"name": "update_channel",
"name": "theme_dark",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "theme_light",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "update_channel",
"ordinal": 8,
"type_info": "Text"
}
],
"parameters": {
@@ -49,8 +59,10 @@
false,
false,
false,
false,
false,
false
]
},
"hash": "b32994b09ae7a06eb0f031069d327e55127a5bce60cbb499b83d1701386a23cb"
"hash": "cae02809532d086fbde12a246d12e3839ec8610b66e08315106dbdbf25d8699c"
}

11
src-tauri/Cargo.lock generated
View File

@@ -2350,6 +2350,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex_color"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d37f101bf4c633f7ca2e4b5e136050314503dd198e78e325ea602c327c484ef0"
dependencies = [
"rand 0.8.5",
]
[[package]]
name = "hkdf"
version = "0.12.3"
@@ -7650,6 +7659,7 @@ dependencies = [
"cocoa",
"datetime",
"grpc",
"hex_color",
"http 0.2.10",
"log",
"objc",
@@ -7674,6 +7684,7 @@ dependencies = [
"tokio",
"tokio-stream",
"uuid",
"windows 0.56.0",
]
[[package]]

View File

@@ -20,6 +20,13 @@ tauri-build = { version = "2.0.0-beta", features = [] }
objc = "0.2.7"
cocoa = "0.25.0"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.56.0", features = [
"Win32_Graphics_Dwm",
"Win32_Foundation",
"Win32_UI_Controls",
] }
[target.'cfg(target_os = "linux")'.dependencies]
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
@@ -51,3 +58,4 @@ reqwest_cookie_store = "0.6.0"
grpc = { path = "./grpc" }
tokio-stream = "0.1.15"
regex = "1.10.2"
hex_color = "3.0.0"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
ALTER TABLE settings
ADD COLUMN theme_dark TEXT DEFAULT 'yaak-dark' NOT NULL;
ALTER TABLE settings
ADD COLUMN theme_light TEXT DEFAULT 'yaak-light' NOT NULL;

View File

@@ -34,7 +34,6 @@ use tokio::sync::Mutex;
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
use ::grpc::manager::{DynamicMessage, GrpcHandle};
use window_ext::TrafficLightWindowExt;
use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::grpc::metadata_to_map;
@@ -69,8 +68,11 @@ mod notifications;
mod plugin;
mod render;
mod updates;
mod window_ext;
mod window_menu;
#[cfg(target_os = "macos")]
mod mac;
#[cfg(target_os = "windows")]
mod win;
async fn migrate_db(app_handle: &AppHandle, db: &Mutex<Pool<Sqlite>>) -> Result<(), String> {
let pool = &*db.lock().await;
@@ -230,8 +232,8 @@ async fn cmd_grpc_go(
..Default::default()
},
)
.await
.map_err(|e| e.to_string())?
.await
.map_err(|e| e.to_string())?
};
let conn_id = conn.id.clone();
@@ -328,8 +330,8 @@ async fn cmd_grpc_go(
..base_msg.clone()
},
)
.await
.unwrap();
.await
.unwrap();
});
return;
}
@@ -344,8 +346,8 @@ async fn cmd_grpc_go(
..base_msg.clone()
},
)
.await
.unwrap();
.await
.unwrap();
});
}
Ok(IncomingMsg::Commit) => {
@@ -384,8 +386,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
async move {
let (maybe_stream, maybe_msg) = match (
@@ -431,8 +433,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
}
match maybe_msg {
@@ -446,13 +448,13 @@ async fn cmd_grpc_go(
} else {
"Received response with metadata"
}
.to_string(),
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
upsert_grpc_event(
&w,
&GrpcEvent {
@@ -461,8 +463,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
upsert_grpc_event(
&w,
&GrpcEvent {
@@ -472,8 +474,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
}
Some(Err(e)) => {
upsert_grpc_event(
@@ -496,8 +498,8 @@ async fn cmd_grpc_go(
},
}),
)
.await
.unwrap();
.await
.unwrap();
}
None => {
// Server streaming doesn't return initial message
@@ -515,13 +517,13 @@ async fn cmd_grpc_go(
} else {
"Received response with metadata"
}
.to_string(),
.to_string(),
event_type: GrpcEventType::Info,
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
stream.into_inner()
}
Some(Err(e)) => {
@@ -545,8 +547,8 @@ async fn cmd_grpc_go(
},
}),
)
.await
.unwrap();
.await
.unwrap();
return;
}
None => return,
@@ -564,8 +566,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
}
Ok(None) => {
let trailers = stream
@@ -583,8 +585,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
break;
}
Err(status) => {
@@ -598,8 +600,8 @@ async fn cmd_grpc_go(
..base_event.clone()
},
)
.await
.unwrap();
.await
.unwrap();
}
}
}
@@ -700,7 +702,7 @@ async fn cmd_send_ephemeral_request(
None,
&mut cancel_rx,
)
.await
.await
}
#[tauri::command]
@@ -767,7 +769,7 @@ async fn cmd_import_data(
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
)
.await;
.await;
result = Some(r);
break;
}
@@ -798,7 +800,7 @@ async fn cmd_import_data(
let maybe_gen_id_opt = |id: Option<String>,
model: ModelType,
ids: &mut HashMap<String, String>|
-> Option<String> {
-> Option<String> {
match id {
Some(id) => Some(maybe_gen_id(id.as_str(), model, ids)),
None => None,
@@ -944,7 +946,7 @@ async fn cmd_export_data(
AnalyticsAction::Export,
None,
)
.await;
.await;
Ok(())
}
@@ -995,8 +997,8 @@ async fn cmd_send_http_request(
None,
None,
)
.await
.expect("Failed to create response");
.await
.expect("Failed to create response");
let download_path = if let Some(p) = download_dir {
Some(std::path::Path::new(p).to_path_buf())
@@ -1021,7 +1023,7 @@ async fn cmd_send_http_request(
download_path,
&mut cancel_rx,
)
.await
.await
}
async fn response_err(
@@ -1129,8 +1131,8 @@ async fn cmd_create_cookie_jar(
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1149,8 +1151,8 @@ async fn cmd_create_environment(
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1171,8 +1173,8 @@ async fn cmd_create_grpc_request(
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1281,8 +1283,8 @@ async fn cmd_create_folder(
..Default::default()
},
)
.await
.map_err(|e| e.to_string())
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -1412,8 +1414,8 @@ async fn cmd_list_cookie_jars(
..Default::default()
},
)
.await
.expect("Failed to create CookieJar");
.await
.expect("Failed to create CookieJar");
Ok(vec![cookie_jar])
} else {
Ok(cookie_jars)
@@ -1482,8 +1484,8 @@ async fn cmd_list_workspaces(w: WebviewWindow) -> Result<Vec<Workspace>, String>
..Default::default()
},
)
.await
.expect("Failed to create Workspace");
.await
.expect("Failed to create Workspace");
Ok(vec![workspace])
} else {
Ok(workspaces)
@@ -1733,16 +1735,16 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
window_id,
WebviewUrl::App(url.unwrap_or_default().into()),
)
.resizable(true)
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.inner_size(1100.0, 600.0)
.position(
// Randomly offset so windows don't stack exactly
100.0 + random::<f64>() * 30.0,
100.0 + random::<f64>() * 30.0,
)
.title(handle.package_info().name.to_string());
.resizable(true)
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.inner_size(1100.0, 600.0)
.position(
// Randomly offset so windows don't stack exactly
100.0 + random::<f64>() * 30.0,
100.0 + random::<f64>() * 30.0,
)
.title(handle.package_info().name.to_string());
// Add macOS-only things
#[cfg(target_os = "macos")]
@@ -1798,25 +1800,19 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
}
});
let win3 = win.clone();
win.on_window_event(move |e| {
let apply_offset = || {
win3.position_traffic_lights();
};
#[cfg(target_os = "macos")]
{
use mac::setup_mac_window;
let mut m_win = win.clone();
setup_mac_window(&mut m_win);
};
#[cfg(target_os = "windows")]
{
use win::setup_win_window;
let mut m_win = win.clone();
setup_win_window(&mut m_win);
}
match e {
WindowEvent::Resized(..) => apply_offset(),
WindowEvent::ThemeChanged(..) => apply_offset(),
WindowEvent::Focused(..) => apply_offset(),
WindowEvent::ScaleFactorChanged { .. } => apply_offset(),
WindowEvent::CloseRequested { .. } => {
// api.prevent_close();
}
_ => {}
}
});
win.position_traffic_lights();
win
}

355
src-tauri/src/mac.rs Normal file
View File

@@ -0,0 +1,355 @@
// Borrowed from our friends at Hoppscotch
// https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs
use hex_color::HexColor;
use tauri::{Manager, WebviewWindow};
struct UnsafeWindowHandle(*mut std::ffi::c_void);
unsafe impl Send for UnsafeWindowHandle {}
unsafe impl Sync for UnsafeWindowHandle {}
const WINDOW_CONTROL_PAD_X: f64 = 13.0;
const WINDOW_CONTROL_PAD_Y: f64 = 18.0;
#[cfg(target_os = "macos")]
fn update_window_theme(window: &WebviewWindow, color: HexColor) {
use cocoa::appkit::{
NSAppearance, NSAppearanceNameVibrantDark, NSAppearanceNameVibrantLight, NSWindow,
};
let brightness = (color.r as u64 + color.g as u64 + color.b as u64) / 3;
unsafe {
let window_handle = UnsafeWindowHandle(window.ns_window().unwrap());
let _ = window.run_on_main_thread(move || {
let handle = window_handle;
let selected_appearance = if brightness >= 128 {
NSAppearance(NSAppearanceNameVibrantLight)
} else {
NSAppearance(NSAppearanceNameVibrantDark)
};
NSWindow::setAppearance(handle.0 as cocoa::base::id, selected_appearance);
set_window_controls_pos(
handle.0 as cocoa::base::id,
WINDOW_CONTROL_PAD_X,
WINDOW_CONTROL_PAD_Y,
);
});
}
}
#[cfg(target_os = "macos")]
fn set_window_controls_pos(window: cocoa::base::id, x: f64, y: f64) {
use cocoa::{
appkit::{NSView, NSWindow, NSWindowButton},
foundation::NSRect,
};
unsafe {
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize = window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
#[cfg(target_os = "macos")]
#[derive(Debug)]
struct AppState {
window: WebviewWindow,
}
#[cfg(target_os = "macos")]
pub fn setup_mac_window(window: &mut WebviewWindow) {
use cocoa::delegate;
use cocoa::appkit::NSWindow;
use cocoa::base::{BOOL, id};
use cocoa::foundation::NSUInteger;
use objc::runtime::{Object, Sel};
use std::ffi::c_void;
fn with_app_state<F: FnOnce(&mut AppState) -> T, T>(this: &Object, func: F) {
let ptr = unsafe {
let x: *mut c_void = *this.get_ivar("yaakApp");
&mut *(x as *mut AppState)
};
func(ptr);
}
unsafe {
let ns_win = window.ns_window().unwrap() as id;
let current_delegate: id = ns_win.delegate();
extern "C" fn on_window_should_close(this: &Object, _cmd: Sel, sender: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, windowShouldClose: sender]
}
}
extern "C" fn on_window_will_close(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillClose: notification];
}
}
extern "C" fn on_window_did_resize(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_app_state(&*this, |state| {
let id = state.window.ns_window().unwrap() as id;
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResize: notification];
}
}
extern "C" fn on_window_did_move(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidMove: notification];
}
}
extern "C" fn on_window_did_change_backing_properties(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidChangeBackingProperties: notification];
}
}
extern "C" fn on_window_did_become_key(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidBecomeKey: notification];
}
}
extern "C" fn on_window_did_resign_key(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidResignKey: notification];
}
}
extern "C" fn on_dragging_entered(this: &Object, _cmd: Sel, notification: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, draggingEntered: notification]
}
}
extern "C" fn on_prepare_for_drag_operation(
this: &Object,
_cmd: Sel,
notification: id,
) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, prepareForDragOperation: notification]
}
}
extern "C" fn on_perform_drag_operation(this: &Object, _cmd: Sel, sender: id) -> BOOL {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, performDragOperation: sender]
}
}
extern "C" fn on_conclude_drag_operation(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, concludeDragOperation: notification];
}
}
extern "C" fn on_dragging_exited(this: &Object, _cmd: Sel, notification: id) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, draggingExited: notification];
}
}
extern "C" fn on_window_will_use_full_screen_presentation_options(
this: &Object,
_cmd: Sel,
window: id,
proposed_options: NSUInteger,
) -> NSUInteger {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
msg_send![super_del, window: window willUseFullScreenPresentationOptions: proposed_options]
}
}
extern "C" fn on_window_did_enter_full_screen(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_app_state(&*this, |state| {
state.window.emit("did-enter-fullscreen", ()).unwrap();
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidEnterFullScreen: notification];
}
}
extern "C" fn on_window_will_enter_full_screen(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_app_state(&*this, |state| {
state.window.emit("will-enter-fullscreen", ()).unwrap();
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillEnterFullScreen: notification];
}
}
extern "C" fn on_window_did_exit_full_screen(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_app_state(&*this, |state| {
state.window.emit("did-exit-fullscreen", ()).unwrap();
let id = state.window.ns_window().unwrap() as id;
set_window_controls_pos(id, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidExitFullScreen: notification];
}
}
extern "C" fn on_window_will_exit_full_screen(this: &Object, _cmd: Sel, notification: id) {
unsafe {
with_app_state(&*this, |state| {
state.window.emit("will-exit-fullscreen", ()).unwrap();
});
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowWillExitFullScreen: notification];
}
}
extern "C" fn on_window_did_fail_to_enter_full_screen(
this: &Object,
_cmd: Sel,
window: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, windowDidFailToEnterFullScreen: window];
}
}
extern "C" fn on_effective_appearance_did_change(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![super_del, effectiveAppearanceDidChange: notification];
}
}
extern "C" fn on_effective_appearance_did_changed_on_main_thread(
this: &Object,
_cmd: Sel,
notification: id,
) {
unsafe {
let super_del: id = *this.get_ivar("super_delegate");
let _: () = msg_send![
super_del,
effectiveAppearanceDidChangedOnMainThread: notification
];
}
}
// extern fn on_dealloc(this: &Object, cmd: Sel) {
// unsafe {
// let super_del: id = *this.get_ivar("super_delegate");
// let _: () = msg_send![super_del, dealloc];
// }
// }
// extern fn on_mark_is_checking_zoomed_in(this: &Object, cmd: Sel) {
// unsafe {
// let super_del: id = *this.get_ivar("super_delegate");
// let _: () = msg_send![super_del, markIsCheckingZoomedIn];
// }
// }
// extern fn on_clear_is_checking_zoomed_in(this: &Object, cmd: Sel) {
// unsafe {
// let super_del: id = *this.get_ivar("super_delegate");
// let _: () = msg_send![super_del, clearIsCheckingZoomedIn];
// }
// }
// Are we deallocing this properly ? (I miss safe Rust :( )
let w = window.clone();
let app_state = AppState { window: w };
let app_box = Box::into_raw(Box::new(app_state)) as *mut c_void;
set_window_controls_pos(ns_win, WINDOW_CONTROL_PAD_X, WINDOW_CONTROL_PAD_Y);
ns_win.setDelegate_(delegate!("MainWindowDelegate", {
window: id = ns_win,
yaakApp: *mut c_void = app_box,
toolbar: id = cocoa::base::nil,
super_delegate: id = current_delegate,
// (dealloc) => on_dealloc as extern fn(&Object, Sel),
// (markIsCheckingZoomedIn) => on_mark_is_checking_zoomed_in as extern fn(&Object, Sel),
// (clearIsCheckingZoomedIn) => on_clear_is_checking_zoomed_in as extern fn(&Object, Sel),
(windowShouldClose:) => on_window_should_close as extern fn(&Object, Sel, id) -> BOOL,
(windowWillClose:) => on_window_will_close as extern fn(&Object, Sel, id),
(windowDidResize:) => on_window_did_resize as extern fn(&Object, Sel, id),
(windowDidMove:) => on_window_did_move as extern fn(&Object, Sel, id),
(windowDidChangeBackingProperties:) => on_window_did_change_backing_properties as extern fn(&Object, Sel, id),
(windowDidBecomeKey:) => on_window_did_become_key as extern fn(&Object, Sel, id),
(windowDidResignKey:) => on_window_did_resign_key as extern fn(&Object, Sel, id),
(draggingEntered:) => on_dragging_entered as extern fn(&Object, Sel, id) -> BOOL,
(prepareForDragOperation:) => on_prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(performDragOperation:) => on_perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL,
(concludeDragOperation:) => on_conclude_drag_operation as extern fn(&Object, Sel, id),
(draggingExited:) => on_dragging_exited as extern fn(&Object, Sel, id),
(window:willUseFullScreenPresentationOptions:) => on_window_will_use_full_screen_presentation_options as extern fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
(windowDidEnterFullScreen:) => on_window_did_enter_full_screen as extern fn(&Object, Sel, id),
(windowWillEnterFullScreen:) => on_window_will_enter_full_screen as extern fn(&Object, Sel, id),
(windowDidExitFullScreen:) => on_window_did_exit_full_screen as extern fn(&Object, Sel, id),
(windowWillExitFullScreen:) => on_window_will_exit_full_screen as extern fn(&Object, Sel, id),
(windowDidFailToEnterFullScreen:) => on_window_did_fail_to_enter_full_screen as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChange:) => on_effective_appearance_did_change as extern fn(&Object, Sel, id),
(effectiveAppearanceDidChangedOnMainThread:) => on_effective_appearance_did_changed_on_main_thread as extern fn(&Object, Sel, id)
}))
}
let app = window.app_handle();
let window = window.clone();
update_window_theme(&window, HexColor::WHITE);
// Control window theme based on app update_window
let window = window.clone();
app.listen_any("yaak_bg_changed", move |ev| {
let payload = serde_json::from_str::<&str>(ev.payload())
.unwrap()
.trim();
let color = HexColor::parse_rgb(payload).unwrap();
update_window_theme(&window, color);
});
}

View File

@@ -4,9 +4,9 @@ use std::fs;
use log::error;
use rand::distributions::{Alphanumeric, DistString};
use serde::{Deserialize, Serialize};
use sqlx::types::chrono::NaiveDateTime;
use sqlx::types::{Json, JsonValue};
use sqlx::{Pool, Sqlite};
use sqlx::types::{Json, JsonValue};
use sqlx::types::chrono::NaiveDateTime;
use tauri::{AppHandle, Manager, WebviewWindow, Wry};
use tokio::sync::Mutex;
@@ -52,6 +52,8 @@ pub struct Settings {
pub updated_at: NaiveDateTime,
pub theme: String,
pub appearance: String,
pub theme_dark: String,
pub theme_light: String,
pub update_channel: String,
}
@@ -883,7 +885,8 @@ async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings, sqlx::Error>
Settings,
r#"
SELECT
id, model, created_at, updated_at, theme, appearance, update_channel
id, model, created_at, updated_at, theme, appearance,
theme_dark, theme_light, update_channel
FROM settings
WHERE id = 'default'
"#,
@@ -919,11 +922,13 @@ pub async fn update_settings(
sqlx::query!(
r#"
UPDATE settings SET (
theme, appearance, update_channel
) = (?, ?, ?) WHERE id = 'default';
theme, appearance, theme_dark, theme_light, update_channel
) = (?, ?, ?, ?, ?) WHERE id = 'default';
"#,
settings.theme,
settings.appearance,
settings.theme_dark,
settings.theme_light,
settings.update_channel
)
.execute(&db)

View File

@@ -1,7 +1,7 @@
use std::fmt::{Display, Formatter};
use std::time::SystemTime;
use log::{debug, info};
use log::info;
use tauri::AppHandle;
use tauri_plugin_dialog::DialogExt;
use tauri_plugin_updater::UpdaterExt;
@@ -118,7 +118,6 @@ impl YaakUpdater {
// Don't check if dev
if is_dev() {
debug!("Not checking for updates in dev");
return Ok(false);
}

79
src-tauri/src/win.rs Normal file
View File

@@ -0,0 +1,79 @@
// Borrowed from our friends at Hoppscotch
// https://github.com/hoppscotch/hoppscotch/blob/286fcd2bb08a84f027b10308d1e18da368f95ebf/packages/hoppscotch-selfhost-desktop/src-tauri/src/mac/window.rs
use std::mem::transmute;
use hex_color::HexColor;
use windows::Win32::Foundation::BOOL;
use windows::Win32::Foundation::COLORREF;
use windows::Win32::Foundation::HWND;
use windows::Win32::Graphics::Dwm::DwmSetWindowAttribute;
use windows::Win32::Graphics::Dwm::DWMWA_CAPTION_COLOR;
use windows::Win32::Graphics::Dwm::DWMWA_USE_IMMERSIVE_DARK_MODE;
use windows::Win32::UI::Controls::{WTA_NONCLIENT, WTNCA_NODRAWICON, WTNCA_NOMIRRORHELP, WTNCA_NOSYSMENU};
use windows::Win32::UI::Controls::SetWindowThemeAttribute;
use windows::Win32::UI::Controls::WTNCA_NODRAWCAPTION;
fn hex_color_to_colorref(color: HexColor) -> COLORREF {
// TODO: Remove this unsafe, This operation doesn't need to be unsafe!
unsafe {
COLORREF(transmute::<[u8; 4], u32>([color.r, color.g, color.b, 0]))
}
}
struct WinThemeAttribute {
flag: u32,
mask: u32
}
#[cfg(target_os = "windows")]
fn update_bg_color(hwnd: &HWND, bg_color: HexColor) {
let use_dark_mode = BOOL::from(true);
let final_color = hex_color_to_colorref(bg_color);
unsafe {
DwmSetWindowAttribute(
HWND(hwnd.0),
DWMWA_USE_IMMERSIVE_DARK_MODE,
ptr::addr_of!(use_dark_mode) as *const c_void,
size_of::<BOOL>().try_into().unwrap()
).unwrap();
DwmSetWindowAttribute(
HWND(hwnd.0),
DWMWA_CAPTION_COLOR,
ptr::addr_of!(final_color) as *const c_void,
size_of::<COLORREF>().try_into().unwrap()
).unwrap();
let flags = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON;
let mask = WTNCA_NODRAWCAPTION | WTNCA_NODRAWICON | WTNCA_NOSYSMENU | WTNCA_NOMIRRORHELP;
let options = WinThemeAttribute { flag: flags, mask };
SetWindowThemeAttribute(
HWND(hwnd.0),
WTA_NONCLIENT,
ptr::addr_of!(options) as *const c_void,
size_of::<WinThemeAttribute>().try_into().unwrap()
).unwrap();
}
}
#[cfg(target_os = "windows")]
pub fn setup_win_window(window: &mut WebviewWindow) {
let win_handle = window.hwnd().unwrap();
let win_clone = win_handle.clone();
window.listen_global("yaak_bg_changed", move |ev| {
let payload = serde_json::from_str::<&str>(ev.payload().unwrap())
.unwrap()
.trim();
let color = HexColor::parse_rgb(payload).unwrap();
update_bg_color(&HWND(win_clone.0), color);
});
update_bg_color(&HWND(win_handle.0), HexColor::rgb(23, 23, 23));
}

View File

@@ -1,53 +0,0 @@
use tauri::WebviewWindow;
const TRAFFIC_LIGHT_OFFSET_X: f64 = 13.0;
const TRAFFIC_LIGHT_OFFSET_Y: f64 = 18.0;
pub trait TrafficLightWindowExt {
fn position_traffic_lights(&self);
}
impl TrafficLightWindowExt for WebviewWindow {
#[cfg(not(target_os = "macos"))]
fn position_traffic_lights(&self) {
// No-op on other platforms
}
#[cfg(target_os = "macos")]
fn position_traffic_lights(&self) {
use cocoa::appkit::{NSView, NSWindow, NSWindowButton};
use cocoa::foundation::NSRect;
let window = self.ns_window().unwrap() as cocoa::base::id;
let x = TRAFFIC_LIGHT_OFFSET_X;
let y = TRAFFIC_LIGHT_OFFSET_Y;
unsafe {
let close = window.standardWindowButton_(NSWindowButton::NSWindowCloseButton);
let miniaturize =
window.standardWindowButton_(NSWindowButton::NSWindowMiniaturizeButton);
let zoom = window.standardWindowButton_(NSWindowButton::NSWindowZoomButton);
let title_bar_container_view = close.superview().superview();
let close_rect: NSRect = msg_send![close, frame];
let button_height = close_rect.size.height;
let title_bar_frame_height = button_height + y;
let mut title_bar_rect = NSView::frame(title_bar_container_view);
title_bar_rect.size.height = title_bar_frame_height;
title_bar_rect.origin.y = NSView::frame(window).size.height - title_bar_frame_height;
let _: () = msg_send![title_bar_container_view, setFrame: title_bar_rect];
let window_buttons = vec![close, miniaturize, zoom];
let space_between = NSView::frame(miniaturize).origin.x - NSView::frame(close).origin.x;
for (i, button) in window_buttons.into_iter().enumerate() {
let mut rect: NSRect = NSView::frame(button);
rect.origin.x = x + (i as f64 * space_between);
button.setFrameOrigin(rect.origin);
}
}
}
}

View File

@@ -1,7 +1,5 @@
import { useQueryClient } from '@tanstack/react-query';
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { foldersQueryKey } from '../hooks/useFolders';
@@ -19,12 +17,11 @@ import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { settingsQueryKey } from '../hooks/useSettings';
import { useSyncAppearance } from '../hooks/useSyncAppearance';
import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument';
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import type { Model } from '../lib/models';
import { modelsEq } from '../lib/models';
import { setPathname } from '../lib/persistPathname';
const DEFAULT_FONT_SIZE = 16;
@@ -35,7 +32,7 @@ export function GlobalHooks() {
useRecentRequests();
// Other useful things
useSyncAppearance();
useSyncThemeToDocument();
useSyncWindowTitle();
useGlobalCommands();
useCommandPalette();
@@ -44,12 +41,6 @@ export function GlobalHooks() {
const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null);
// Listen for location changes and update the pathname
const location = useLocation();
useEffect(() => {
setPathname(location.pathname).catch(console.error);
}, [location.pathname]);
interface ModelPayload {
model: Model;
windowLabel: string;

View File

@@ -0,0 +1,15 @@
import type { ReactNode } from 'react';
import { useAppInfo } from '../hooks/useAppInfo';
interface Props {
children: ReactNode;
}
export function IsDev({ children }: Props) {
const appInfo = useAppInfo();
if (!appInfo?.isDev) {
return null;
}
return <>{children}</>;
}

View File

@@ -20,9 +20,11 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
import { EmptyStateText } from './EmptyStateText';
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
import { ResponseHeaders } from './ResponseHeaders';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
interface Props {
@@ -76,7 +78,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
'x-theme-responsePane',
'max-h-full h-full',
'bg-background rounded-md border border-background-highlight',
'shadow relative',
'relative',
)}
>
{activeResponse == null ? (
@@ -154,6 +156,10 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
</div>
) : contentType?.startsWith('image') ? (
<ImageViewer className="pb-2" response={activeResponse} />
) : contentType?.startsWith('audio') ? (
<AudioViewer response={activeResponse} />
) : contentType?.startsWith('video') ? (
<VideoViewer response={activeResponse} />
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
<EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
@@ -161,7 +167,11 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : (
<TextViewer response={activeResponse} pretty={viewMode === 'pretty'} />
<TextViewer
className="-mr-2" // Pull to the right
response={activeResponse}
pretty={viewMode === 'pretty'}
/>
)}
</TabContent>
</Tabs>

View File

@@ -0,0 +1,162 @@
import React from 'react';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useResolvedAppearance } from '../../hooks/useResolvedAppearance';
import { useResolvedTheme } from '../../hooks/useResolvedTheme';
import { useSettings } from '../../hooks/useSettings';
import { useThemes } from '../../hooks/useThemes';
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
import { trackEvent } from '../../lib/analytics';
import { isThemeDark } from '../../lib/theme/window';
import type { ButtonProps } from '../core/Button';
import { Editor } from '../core/Editor';
import type { IconProps } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import type { SelectOption } from '../core/Select';
import { Select } from '../core/Select';
import { HStack, VStack } from '../core/Stacks';
const buttonColors: ButtonProps['color'][] = [
'primary',
'info',
'success',
'notice',
'warning',
'danger',
'secondary',
'default',
];
const icons: IconProps['icon'][] = [
'info',
'box',
'update',
'alert',
'arrowBigRightDash',
'download',
'copy',
'magicWand',
'settings',
'trash',
'sparkles',
'pencil',
'paste',
'search',
'sendHorizontal',
];
export function SettingsAppearance() {
const workspace = useActiveWorkspace();
const settings = useSettings();
const updateSettings = useUpdateSettings();
const appearance = useResolvedAppearance();
const { themes } = useThemes();
const activeTheme = useResolvedTheme();
if (settings == null || workspace == null) {
return null;
}
const lightThemes: SelectOption<string>[] = themes
.filter((theme) => !isThemeDark(theme))
.map((theme) => ({
label: theme.name,
value: theme.id,
}));
const darkThemes: SelectOption<string>[] = themes
.filter((theme) => isThemeDark(theme))
.map((theme) => ({
label: theme.name,
value: theme.id,
}));
return (
<VStack space={2} className="mb-4">
<Select
name="appearance"
label="Appearance"
labelPosition="left"
size="sm"
value={settings.appearance}
onChange={async (appearance) => {
await updateSettings.mutateAsync({ ...settings, appearance });
trackEvent('setting', 'update', { appearance });
}}
options={[
{ label: 'Sync with OS', value: 'system' },
{ label: 'Light', value: 'light' },
{ label: 'Dark', value: 'dark' },
]}
/>
<div className="grid grid-cols-2 gap-3">
<Select
name="lightTheme"
label={'Light Theme' + (appearance !== 'dark' ? ' (active)' : '')}
labelPosition="top"
size="sm"
value={activeTheme.light.id}
options={lightThemes}
onChange={async (themeLight) => {
await updateSettings.mutateAsync({ ...settings, themeLight });
trackEvent('setting', 'update', { themeLight });
}}
/>
<Select
name="darkTheme"
label={'Dark Theme' + (appearance === 'dark' ? ' (active)' : '')}
labelPosition="top"
size="sm"
value={activeTheme.dark.id}
options={darkThemes}
onChange={async (themeDark) => {
await updateSettings.mutateAsync({ ...settings, themeDark });
trackEvent('setting', 'update', { themeDark });
}}
/>
</div>
<VStack
space={3}
className="mt-3 w-full bg-background p-3 border border-dashed border-background-highlight rounded"
>
<div className="text-sm text-fg font-bold">
Theme Preview <span className="text-fg-subtle">({appearance})</span>
</div>
<HStack space={1.5} alignItems="center" className="w-full">
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
size="2xs"
iconSize="xs"
icon={icons[i % icons.length]!}
iconClassName="text-fg"
title={`${c}`}
/>
))}
{buttonColors.map((c, i) => (
<IconButton
key={c}
color={c}
variant="border"
size="2xs"
iconSize="xs"
icon={icons[i % icons.length]!}
iconClassName="text-fg"
title={`${c}`}
/>
))}
</HStack>
<Editor
defaultValue={[
'let foo = { // Demo code editor',
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
heightMode="auto"
contentType="application/javascript"
/>
</VStack>
</VStack>
);
}

View File

@@ -0,0 +1,103 @@
import React from 'react';
import { capitalize } from '../../lib/capitalize';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
import { Editor } from '../core/Editor';
import type { IconProps } from '../core/Icon';
import { Icon } from '../core/Icon';
import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input';
const buttonColors = [
'primary',
'secondary',
'info',
'success',
'warning',
'danger',
'default',
] as const;
const icons: IconProps['icon'][] = [
'info',
'box',
'update',
'alert',
'arrowBigRightDash',
'download',
'copy',
'magicWand',
'settings',
'trash',
'sparkles',
'pencil',
'paste',
'search',
'sendHorizontal',
];
export function SettingsDesign() {
return (
<div className="p-2 flex flex-col gap-3">
<Input
label="Field Label"
name="demo"
placeholder="Placeholder"
size="sm"
rightSlot={<IconButton title="search" size="xs" className="w-8 m-0.5" icon="search" />}
/>
<Editor
defaultValue={[
'// Demo code editor',
'let foo = {',
' foo: ("bar" || "baz" ?? \'qux\'),',
' baz: [1, 10.2, null, false, true],',
'};',
].join('\n')}
heightMode="auto"
contentType="application/javascript"
/>
<div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1">
{buttonColors.map((c, i) => (
<Button key={c} color={c} size="sm" leftSlot={<Icon size="sm" icon={icons[i]!} />}>
{capitalize(c).slice(0, 4)}
</Button>
))}
</div>
<div className="flex flex-wrap gap-1">
{buttonColors.map((c, i) => (
<Button
key={c}
color={c}
variant="border"
size="sm"
leftSlot={<Icon size="sm" icon={icons[i]!} />}
>
{capitalize(c).slice(0, 4)}
</Button>
))}
</div>
<div className="flex gap-1">
{icons.map((v, i) => (
<IconButton
color={buttonColors[i % buttonColors.length]}
title={v}
variant="border"
size="sm"
key={v}
icon={v}
/>
))}
</div>
</div>
<div className="flex flex-col gap-1">
<Banner color="primary">Primary banner</Banner>
<Banner color="secondary">Secondary banner</Banner>
<Banner color="danger">Danger banner</Banner>
<Banner color="warning">Warning banner</Banner>
<Banner color="success">Success banner</Banner>
</div>
</div>
);
}

View File

@@ -0,0 +1,51 @@
import classNames from 'classnames';
import { createGlobalState } from 'react-use';
import { useAppInfo } from '../../hooks/useAppInfo';
import { capitalize } from '../../lib/capitalize';
import { TabContent, Tabs } from '../core/Tabs/Tabs';
import { SettingsAppearance } from './SettingsAppearance';
import { SettingsDesign } from './SettingsDesign';
import { SettingsGeneral } from './SettingsGeneral';
enum Tab {
General = 'general',
Appearance = 'appearance',
// Dev-only
Design = 'design',
}
const tabs = [Tab.General, Tab.Appearance, Tab.Design];
const useTabState = createGlobalState<string>(Tab.Appearance);
export const SettingsDialog = () => {
const [tab, setTab] = useTabState();
const appInfo = useAppInfo();
const isDev = appInfo?.isDev ?? false;
return (
<div className={classNames('w-[70vw] max-w-[40rem]', 'h-[80vh]')}>
<Tabs
value={tab}
addBorders
label="Settings"
tabListClassName="h-md !-ml-1 mt-2"
onChangeValue={setTab}
tabs={tabs
.filter((t) => t !== Tab.Design || isDev)
.map((value) => ({ value, label: capitalize(value) }))}
>
<TabContent value={Tab.General} className="pt-3 overflow-y-auto h-full px-4">
<SettingsGeneral />
</TabContent>
<TabContent value={Tab.Appearance} className="pt-3 overflow-y-auto h-full px-4">
<SettingsAppearance />
</TabContent>
<TabContent value={Tab.Design} className="pt-3 overflow-y-auto h-full px-4">
<SettingsDesign />
</TabContent>
</Tabs>
</div>
);
};

View File

@@ -1,20 +1,20 @@
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppInfo } from '../hooks/useAppInfo';
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
import { useSettings } from '../hooks/useSettings';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { trackEvent } from '../lib/analytics';
import { Checkbox } from './core/Checkbox';
import { Heading } from './core/Heading';
import { IconButton } from './core/IconButton';
import { Input } from './core/Input';
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
import { Select } from './core/Select';
import { Separator } from './core/Separator';
import { VStack } from './core/Stacks';
import { useActiveWorkspace } from '../../hooks/useActiveWorkspace';
import { useAppInfo } from '../../hooks/useAppInfo';
import { useCheckForUpdates } from '../../hooks/useCheckForUpdates';
import { useSettings } from '../../hooks/useSettings';
import { useUpdateSettings } from '../../hooks/useUpdateSettings';
import { useUpdateWorkspace } from '../../hooks/useUpdateWorkspace';
import { trackEvent } from '../../lib/analytics';
import { Checkbox } from '../core/Checkbox';
import { Heading } from '../core/Heading';
import { IconButton } from '../core/IconButton';
import { Input } from '../core/Input';
import { KeyValueRow, KeyValueRows } from '../core/KeyValueRow';
import { Select } from '../core/Select';
import { Separator } from '../core/Separator';
import { VStack } from '../core/Stacks';
export const SettingsDialog = () => {
export function SettingsGeneral() {
const workspace = useActiveWorkspace();
const updateWorkspace = useUpdateWorkspace(workspace?.id ?? null);
const settings = useSettings();
@@ -28,32 +28,6 @@ export const SettingsDialog = () => {
return (
<VStack space={2} className="mb-4">
<Select
name="appearance"
label="Appearance"
labelPosition="left"
size="sm"
value={settings.appearance}
onChange={async (appearance) => {
await updateSettings.mutateAsync({ ...settings, appearance });
trackEvent('setting', 'update', { appearance });
}}
options={[
{
label: 'System',
value: 'system',
},
{
label: 'Light',
value: 'light',
},
{
label: 'Dark',
value: 'dark',
},
]}
/>
<div className="grid grid-cols-[minmax(0,1fr)_auto] gap-1">
<Select
name="updateChannel"
@@ -133,10 +107,10 @@ export const SettingsDialog = () => {
<Heading size={2}>App Info</Heading>
<KeyValueRows>
<KeyValueRow label="Version" value={appInfo.data?.version} />
<KeyValueRow label="Data Directory" value={appInfo.data?.appDataDir} />
<KeyValueRow label="Logs Directory" value={appInfo.data?.appLogDir} />
<KeyValueRow label="Version" value={appInfo?.version} />
<KeyValueRow label="Data Directory" value={appInfo?.appDataDir} />
<KeyValueRow label="Logs Directory" value={appInfo?.appLogDir} />
</KeyValueRows>
</VStack>
);
};
}

View File

@@ -11,7 +11,7 @@ import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { useDialog } from './DialogContext';
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
import { SettingsDialog } from './SettingsDialog';
import { SettingsDialog } from './Settings/SettingsDialog';
export function SettingsDropdown() {
const importData = useImportData();
@@ -24,8 +24,9 @@ export function SettingsDropdown() {
const showSettings = () => {
dialog.show({
id: 'settings',
size: 'md',
title: 'Settings',
size: 'dynamic',
noScroll: true,
noPadding: true,
render: () => <SettingsDialog />,
});
};
@@ -69,7 +70,7 @@ export function SettingsDropdown() {
leftSlot: <Icon icon="folderOutput" />,
onSelect: () => exportData.mutate(),
},
{ type: 'separator', label: `Yaak v${appInfo.data?.version}` },
{ type: 'separator', label: `Yaak v${appInfo?.version}` },
{
key: 'update-check',
label: 'Check for Updates',
@@ -88,7 +89,7 @@ export function SettingsDropdown() {
label: 'Changelog',
leftSlot: <Icon icon="cake" />,
rightSlot: <Icon icon="externalLink" />,
onSelect: () => open(`https://yaak.app/changelog/${appInfo.data?.version}`),
onSelect: () => open(`https://yaak.app/changelog/${appInfo?.version}`),
},
]}
>

View File

@@ -468,47 +468,6 @@ export function Sidebar({ className }: Props) {
handleEnd={handleEnd}
handleDragStart={handleDragStart}
/>
{/*<div className="p-2 flex flex-col gap-1">*/}
{/* <div className="flex flex-wrap gap-1">*/}
{/* <Button color="primary">Primary</Button>*/}
{/* <Button color="secondary">Secondary</Button>*/}
{/* <Button color="info">Info</Button>*/}
{/* <Button color="success">Success</Button>*/}
{/* <Button color="warning">Warning</Button>*/}
{/* <Button color="danger">Danger</Button>*/}
{/* <Button color="default">Default</Button>*/}
{/* </div>*/}
{/* <div className="flex flex-wrap gap-1">*/}
{/* <Button variant="border" color="primary">*/}
{/* Primary*/}
{/* </Button>*/}
{/* <Button variant="border" color="secondary">*/}
{/* Secondary*/}
{/* </Button>*/}
{/* <Button variant="border" color="info">*/}
{/* Info*/}
{/* </Button>*/}
{/* <Button variant="border" color="success">*/}
{/* Success*/}
{/* </Button>*/}
{/* <Button variant="border" color="warning">*/}
{/* Warning*/}
{/* </Button>*/}
{/* <Button variant="border" color="danger">*/}
{/* Danger*/}
{/* </Button>*/}
{/* <Button variant="border" color="default">*/}
{/* Default*/}
{/* </Button>*/}
{/* </div>*/}
{/* <div className="flex flex-col gap-1">*/}
{/* <Banner color="primary">Primary banner</Banner>*/}
{/* <Banner color="secondary">Secondary banner</Banner>*/}
{/* <Banner color="danger">Danger banner</Banner>*/}
{/* <Banner color="warning">Warning banner</Banner>*/}
{/* <Banner color="success">Success banner</Banner>*/}
{/* </div>*/}
{/*</div>*/}
</aside>
);
}
@@ -800,11 +759,14 @@ const SidebarItem = forwardRef(function SidebarItem(
const name = await prompt({
id: 'rename-request',
title: 'Rename Request',
description: (
<>
Enter a new name for <InlineCode>{itemName}</InlineCode>
</>
),
description:
itemName === '' ? (
'Enter a new name'
) : (
<>
Enter a new name for <InlineCode>{itemName}</InlineCode>
</>
),
name: 'name',
label: 'Name',
placeholder: 'New Name',
@@ -893,7 +855,7 @@ const SidebarItem = forwardRef(function SidebarItem(
{isResponseLoading(latestHttpResponse) ? (
<Icon spin size="sm" icon="refresh" className="text-fg-subtler" />
) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestHttpResponse} />
<StatusTag className="text-2xs" response={latestHttpResponse} />
)}
</div>
) : null}

View File

@@ -59,7 +59,7 @@ export const UrlBar = memo(function UrlBar({
};
return (
<form onSubmit={handleSubmit} className={classNames(className)}>
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
<Input
autocompleteVariables
ref={inputRef}
@@ -76,7 +76,7 @@ export const UrlBar = memo(function UrlBar({
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
onPaste={onPaste}
containerClassName="shadow bg-background border"
containerClassName="bg-background border border-background-highlight"
onChange={onUrlChange}
defaultValue={url}
placeholder={placeholder}

View File

@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
interface Props {
children: ReactNode;
className?: string;
color?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger';
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
}
export function Banner({ children, className, color = 'secondary' }: Props) {

View File

@@ -14,11 +14,12 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
| 'primary'
| 'info'
| 'success'
| 'notice'
| 'warning'
| 'danger';
variant?: 'border' | 'solid';
isLoading?: boolean;
size?: 'xs' | 'sm' | 'md';
size?: '2xs' | 'xs' | 'sm' | 'md';
justify?: 'start' | 'center';
type?: 'button' | 'submit';
forDropdown?: boolean;
@@ -60,24 +61,27 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
`x-theme-button--${variant}`,
`x-theme-button--${variant}--${color}`,
'text-fg',
'border', // They all have borders to ensure the same width
'max-w-full min-w-0', // Help with truncation
'hocus:opacity-100', // Force opacity for certain hover effects
'whitespace-nowrap outline-none',
'flex-shrink-0 flex items-center',
'focus-visible-or-class:ring rounded-md',
'focus-visible-or-class:ring',
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
size === 'md' && 'h-md px-3',
size === 'sm' && 'h-sm px-2.5 text-sm',
size === 'xs' && 'h-xs px-2 text-sm',
size === 'md' && 'h-md px-3 rounded-md',
size === 'sm' && 'h-sm px-2.5 text-sm rounded-md',
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
size === '2xs' && 'h-5 px-1 text-xs rounded',
// Solids
variant === 'solid' && 'border-transparent',
variant === 'solid' && color === 'custom' && 'ring-blue-400',
variant === 'solid' &&
color !== 'custom' &&
color !== 'default' &&
'bg-background enabled:hocus:bg-background-highlight ring-background-highlight-secondary',
variant === 'solid' && color === 'custom' && 'ring-blue-400',
variant === 'solid' &&
color === 'default' &&
'enabled:hocus:bg-background-highlight ring-fg-info',

View File

@@ -30,7 +30,7 @@ export function Checkbox({
alignItems="center"
className={classNames(className, 'text-fg text-sm', disabled && 'opacity-disabled')}
>
<div className={classNames(inputWrapperClassName, 'relative flex')}>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<input
aria-hidden
className={classNames(

View File

@@ -94,7 +94,7 @@ export function Dialog({
className={classNames(
'h-full w-full grid grid-cols-[minmax(0,1fr)]',
!noPadding && 'px-6 py-2',
!noScroll && 'overflow-y-auto',
!noScroll && 'overflow-y-auto overflow-x-hidden',
)}
>
{children}

View File

@@ -413,7 +413,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
)}
{isOpen && (
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
<div className="x-theme-dialog">
<div className="x-theme-menu">
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={handleClose} />
<motion.div
tabIndex={0}

View File

@@ -7,7 +7,7 @@
.cm-cursor {
@apply border-fg !important;
/* Widen the cursor */
@apply border-l-2;
@apply border-l-[2px];
}
&.cm-focused {

View File

@@ -283,7 +283,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}
return (
<div className="group relative h-full w-full">
<div className="group relative h-full w-full x-theme-editor bg-background">
{cmContainer}
{decoratedActions && (
<HStack

View File

@@ -46,45 +46,23 @@ export const myHighlightStyle = HighlightStyle.define([
fontStyle: 'italic',
},
{
tag: [t.paren],
tag: [t.paren, t.bracket, t.brace],
color: 'var(--fg)',
},
{
tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number],
tag: [t.link, t.name, t.tagName, t.angleBracket, t.docString, t.number],
color: 'var(--fg-info)',
},
{ tag: [t.variableName], color: 'var(--fg-success)' },
{ tag: [t.bool], color: 'var(--fg-info)' }, // TODO: Should be pink
{ tag: [t.bool], color: 'var(--fg-warning)' },
{ tag: [t.attributeName, t.propertyName], color: 'var(--fg-primary)' },
{ tag: [t.attributeValue], color: 'var(--fg-warning)' },
{ tag: [t.string], color: 'var(--fg-warning)' }, // TODO: Should be yellow
{ tag: [t.keyword, t.meta, t.operator], color: 'var(--fg-danger)' },
{ tag: [t.string], color: 'var(--fg-notice)' },
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' },
]);
const myTheme = EditorView.theme({}, { dark: true });
// export const defaultHighlightStyle = HighlightStyle.define([
// { tag: t.meta, color: '#404740' },
// { tag: t.link, textDecoration: 'underline' },
// { tag: t.heading, textDecoration: 'underline', fontWeight: 'bold' },
// { tag: t.emphasis, fontStyle: 'italic' },
// { tag: t.strong, fontWeight: 'bold' },
// { tag: t.strikethrough, textDecoration: 'line-through' },
// { tag: t.keyword, color: '#708' },
// { tag: [t.atom, t.bool, t.url, t.contentSeparator, t.labelName], color: '#219' },
// { tag: [t.literal, t.inserted], color: '#164' },
// { tag: [t.string, t.deleted], color: '#a11' },
// { tag: [t.regexp, t.escape, t.special(t.string)], color: '#e40' },
// { tag: t.definition(t.variableName), color: '#00f' },
// { tag: t.local(t.variableName), color: '#30a' },
// { tag: [t.typeName, t.namespace], color: '#085' },
// { tag: t.className, color: '#167' },
// { tag: [t.special(t.variableName), t.macroName], color: '#256' },
// { tag: t.definition(t.propertyName), color: '#00c' },
// { tag: t.comment, color: '#940' },
// { tag: t.invalid, color: '#f00' },
// ]);
const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
'application/json': json(),

View File

@@ -66,7 +66,7 @@ const icons = {
export interface IconProps {
icon: keyof typeof icons;
className?: string;
size?: 'xs' | 'sm' | 'md' | 'lg';
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg';
spin?: boolean;
title?: string;
}
@@ -83,6 +83,7 @@ export const Icon = memo(function Icon({ icon, spin, size = 'md', className, tit
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',
size === '2xs' && 'h-2.5 w-2.5',
spin && 'animate-spin',
)}
/>

View File

@@ -57,6 +57,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
size === 'md' && 'w-9',
size === 'sm' && 'w-8',
size === 'xs' && 'w-6',
size === '2xs' && 'w-5',
)}
{...props}
>

View File

@@ -6,8 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
<code
className={classNames(
className,
'font-mono text-xs bg-background-highlight-secondary',
'px-1.5 py-0.5 rounded text-fg shadow-inner',
'font-mono text-xs bg-background-highlight-secondary border border-background-highlight',
'px-1.5 py-0.5 rounded text-fg-info shadow-inner',
)}
{...props}
/>

View File

@@ -145,6 +145,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
alignItems="stretch"
className={classNames(
containerClassName,
'x-theme-input',
'relative w-full rounded-md text-fg',
'border',
focused ? 'border-border-focus' : 'border-background-highlight',

View File

@@ -1,3 +1,4 @@
import type { ReactNode } from 'react';
import { useMemo } from 'react';
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
import { Dropdown } from './Dropdown';
@@ -9,6 +10,7 @@ export type RadioDropdownItem<T = string | null> =
label: string;
shortLabel?: string;
value: T;
rightSlot?: ReactNode;
}
| DropdownItemSeparator;
@@ -37,9 +39,10 @@ export function RadioDropdown<T = string | null>({
key: item.label,
label: item.label,
shortLabel: item.shortLabel,
rightSlot: item.rightSlot,
onSelect: () => onChange(item.value),
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
};
} as DropdownProps['items'][0];
}
}),
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),

View File

@@ -7,12 +7,17 @@ interface Props<T extends string> {
labelClassName?: string;
hideLabel?: boolean;
value: T;
options: { label: string; value: T }[];
options: SelectOption<T>[];
onChange: (value: T) => void;
size?: 'xs' | 'sm' | 'md' | 'lg';
className?: string;
}
export interface SelectOption<T extends string> {
label: string;
value: T;
}
export function Select<T extends string>({
labelPosition = 'top',
name,
@@ -30,6 +35,7 @@ export function Select<T extends string>({
<div
className={classNames(
className,
'x-theme-input',
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
labelPosition === 'left' && 'flex items-center gap-2',

View File

@@ -1,7 +1,6 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { memo, useCallback, useEffect, useRef } from 'react';
import { Button } from '../Button';
import { Icon } from '../Icon';
import type { RadioDropdownProps } from '../RadioDropdown';
import { RadioDropdown } from '../RadioDropdown';
@@ -25,6 +24,7 @@ interface Props {
tabListClassName?: string;
className?: string;
children: ReactNode;
addBorders?: boolean;
}
export function Tabs({
@@ -35,6 +35,7 @@ export function Tabs({
tabs,
className,
tabListClassName,
addBorders,
}: Props) {
const ref = useRef<HTMLDivElement | null>(null);
@@ -78,12 +79,15 @@ export function Tabs({
'-ml-5 pl-3 pr-1 py-1',
)}
>
<HStack space={2} className="flex-shrink-0">
<HStack space={2} className="h-full flex-shrink-0">
{tabs.map((t) => {
const isActive = t.value === value;
const btnClassName = classNames(
isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
'h-full flex items-center text-sm rounded',
'!px-2 ml-[1px]',
addBorders && 'border',
isActive ? 'text-fg' : 'text-fg-subtler hover:text-fg-subtle',
isActive && addBorders ? 'border-background-highlight' : 'border-transparent',
);
if ('options' in t) {
@@ -97,39 +101,34 @@ export function Tabs({
value={t.options.value}
onChange={t.options.onChange}
>
<Button
<button
color="custom"
size="sm"
onClick={isActive ? undefined : () => handleTabChange(t.value)}
className={btnClassName}
rightSlot={
<Icon
size="sm"
icon="chevronDown"
className={classNames(
'-mr-1.5 mt-0.5',
isActive ? 'text-fg-subtle' : 'opacity-50',
)}
/>
}
>
{option && 'shortLabel' in option
? option.shortLabel
: option?.label ?? 'Unknown'}
</Button>
<TabAccent enabled isActive={isActive} />
<Icon
size="sm"
icon="chevronDown"
className={classNames('ml-1', isActive ? 'text-fg-subtle' : 'opacity-50')}
/>
</button>
</RadioDropdown>
);
} else {
return (
<Button
<button
key={t.value}
color="custom"
size="sm"
onClick={() => handleTabChange(t.value)}
className={btnClassName}
>
{t.label}
</Button>
<TabAccent enabled isActive={isActive} />
</button>
);
}
})}
@@ -161,3 +160,14 @@ export const TabContent = memo(function TabContent({
</div>
);
});
function TabAccent({ isActive, enabled }: { isActive: boolean; enabled: boolean }) {
return (
<div
className={classNames(
'w-full opacity-40 border-b-2',
isActive && enabled ? 'border-b-background-highlight' : 'border-b-transparent',
)}
/>
);
}

View File

@@ -52,11 +52,11 @@ export function Toast({
transition={{ duration: 0.2 }}
className={classNames(
className,
'x-theme-dialog',
'x-theme-toast',
'pointer-events-auto',
'relative bg-background pointer-events-auto',
'rounded-lg',
'border border-background-highlight dark:border-background-highlight-secondary shadow-xl',
'border border-background-highlight shadow-lg',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
'w-[22rem] max-h-[80vh]',
'm-2 grid grid-cols-[1fr_auto]',

View File

@@ -0,0 +1,18 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import React from 'react';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: HttpResponse;
}
export function AudioViewer({ response }: Props) {
if (response.bodyPath === null) {
return <div>Empty response body</div>;
}
const src = convertFileSrc(response.bodyPath);
// eslint-disable-next-line jsx-a11y/media-has-caption
return <audio className="w-full" controls src={src}></audio>;
}

View File

@@ -2,7 +2,6 @@ import { convertFileSrc } from '@tauri-apps/api/core';
import classNames from 'classnames';
import { useState } from 'react';
import type { HttpResponse } from '../../lib/models';
import { Button } from '../core/Button';
interface Props {
response: HttpResponse;
@@ -23,13 +22,9 @@ export function ImageViewer({ response, className }: Props) {
<>
<div className="text-sm italic text-fg-subtler">
Response body is too large to preview.{' '}
<Button
className="cursor-pointer underline hover:text-fg"
color="secondary"
onClick={() => setShow(true)}
>
<button className="cursor-pointer underline hover:text-fg" onClick={() => setShow(true)}>
Show anyway
</Button>
</button>
</div>
</>
);

View File

@@ -18,9 +18,10 @@ const extraExtensions = [hyperlink];
interface Props {
response: HttpResponse;
pretty: boolean;
className?: string;
}
export function TextViewer({ response, pretty }: Props) {
export function TextViewer({ response, pretty, className }: Props) {
const [isSearching, toggleIsSearching] = useToggle();
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
@@ -85,6 +86,7 @@ export function TextViewer({ response, pretty }: Props) {
return (
<Editor
readOnly
className={className}
forceUpdateKey={body}
defaultValue={body}
contentType={contentType}

View File

@@ -0,0 +1,18 @@
import { convertFileSrc } from '@tauri-apps/api/core';
import React from 'react';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: HttpResponse;
}
export function VideoViewer({ response }: Props) {
if (response.bodyPath === null) {
return <div>Empty response body</div>;
}
const src = convertFileSrc(response.bodyPath);
// eslint-disable-next-line jsx-a11y/media-has-caption
return <video className="w-full" controls src={src}></video>;
}

View File

@@ -16,5 +16,5 @@ export function useAppInfo() {
const metadata = await invoke('cmd_metadata');
return metadata as AppInfo;
},
});
}).data;
}

View File

@@ -8,7 +8,7 @@ export function useCommandPalette() {
const appInfo = useAppInfo();
useHotKey('command_palette.toggle', () => {
// Disabled in production for now
if (!appInfo.data?.isDev) {
if (!appInfo?.isDev) {
return;
}

View File

@@ -1,35 +1,23 @@
import { useEffect, useState } from 'react';
import type { Appearance } from '../lib/theme/window';
import {
setAppearanceOnDocument,
getPreferredAppearance,
subscribeToPreferredAppearanceChange,
} from '../lib/theme/window';
import { getPreferredAppearance, subscribeToPreferredAppearanceChange } from '../lib/theme/window';
import { useSettings } from './useSettings';
export function useSyncAppearance() {
export function useResolvedAppearance() {
const [preferredAppearance, setPreferredAppearance] = useState<Appearance>(
getPreferredAppearance(),
);
const settings = useSettings();
// Set appearance when preferred theme changes
useEffect(() => {
return subscribeToPreferredAppearanceChange(setPreferredAppearance);
}, []);
const settings = useSettings();
const appearance =
settings == null || settings?.appearance === 'system'
? preferredAppearance
: settings.appearance;
useEffect(() => {
if (settings == null) {
return;
}
setAppearanceOnDocument(settings.appearance as Appearance);
}, [appearance, settings]);
return { appearance };
return appearance;
}

View File

@@ -0,0 +1,20 @@
import { isThemeDark } from '../lib/theme/window';
import { useResolvedAppearance } from './useResolvedAppearance';
import { useSettings } from './useSettings';
import { useThemes } from './useThemes';
export function useResolvedTheme() {
const appearance = useResolvedAppearance();
const settings = useSettings();
const { themes, fallback } = useThemes();
const darkThemes = themes.filter((t) => isThemeDark(t));
const lightThemes = themes.filter((t) => !isThemeDark(t));
const dark = darkThemes.find((t) => t.id === settings?.themeDark) ?? fallback.dark;
const light = lightThemes.find((t) => t.id === settings?.themeLight) ?? fallback.light;
const active = appearance === 'dark' ? dark : light;
return { dark, light, active };
}

View File

@@ -0,0 +1,23 @@
import { emit } from '@tauri-apps/api/event';
import { useEffect } from 'react';
import type { YaakTheme } from '../lib/theme/window';
import { addThemeStylesToDocument, setThemeOnDocument } from '../lib/theme/window';
import { useResolvedTheme } from './useResolvedTheme';
export function useSyncThemeToDocument() {
const theme = useResolvedTheme();
useEffect(() => {
setThemeOnDocument(theme.active);
emitBgChange(theme.active);
}, [theme.active]);
useEffect(() => {
addThemeStylesToDocument(theme.active);
}, [theme.active]);
}
function emitBgChange(t: YaakTheme) {
if (t.background == null) return;
emit('yaak_bg_changed', t.background.hex()).catch(console.error);
}

View File

@@ -1,23 +1,24 @@
// import { useEffect } from 'react';
// import { fallbackRequestName } from '../lib/fallbackRequestName';
// import { useActiveEnvironment } from './useActiveEnvironment';
// import { useActiveRequest } from './useActiveRequest';
// import { useActiveWorkspace } from './useActiveWorkspace';
import { useEffect } from 'react';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { useActiveEnvironment } from './useActiveEnvironment';
import { useActiveRequest } from './useActiveRequest';
import { useActiveWorkspace } from './useActiveWorkspace';
export function useSyncWindowTitle() {
// const activeRequest = useActiveRequest();
// const activeWorkspace = useActiveWorkspace();
// const activeEnvironment = useActiveEnvironment();
// useEffect(() => {
// let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
// if (activeEnvironment) {
// newTitle += ` [${activeEnvironment.name}]`;
// }
// if (activeRequest) {
// newTitle += ` ${fallbackRequestName(activeRequest)}`;
// }
//
// // TODO: This resets the stoplight position so we can't use it yet
// // getCurrent().setTitle(newTitle).catch(console.error);
// }, [activeEnvironment, activeRequest, activeWorkspace]);
const activeRequest = useActiveRequest();
const activeWorkspace = useActiveWorkspace();
const activeEnvironment = useActiveEnvironment();
useEffect(() => {
let newTitle = activeWorkspace ? activeWorkspace.name : 'Yaak';
if (activeEnvironment) {
newTitle += ` [${activeEnvironment.name}]`;
}
if (activeRequest) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
newTitle += ` ${fallbackRequestName(activeRequest)}`;
}
// TODO: This resets the stoplight position so we can't use it yet
// invoke('cmd_set_title', { title: newTitle }).catch(console.error);
}, [activeEnvironment, activeRequest, activeWorkspace]);
}

View File

@@ -0,0 +1,13 @@
import { yaakDark, yaakLight, yaakThemes } from '../lib/theme/themes';
export function useThemes() {
const dark = yaakDark;
const light = yaakLight;
const otherThemes = yaakThemes
.filter((t) => t.id !== dark.id && t.id !== light.id)
.sort((a, b) => a.name.localeCompare(b.name));
const themes = [dark, light, ...otherThemes];
return { themes, fallback: { dark, light } };
}

View File

@@ -23,7 +23,6 @@ export function fallbackRequestName(r: HttpRequest | GrpcRequest | null): string
try {
const url = new URL(fixedUrl);
const pathname = url.pathname === '/' ? '' : url.pathname;
console.log('hello', fixedUrl);
return `${url.host}${pathname}`;
} catch (_) {
// Nothing

View File

@@ -34,6 +34,8 @@ export interface Settings extends BaseModel {
readonly model: 'settings';
theme: string;
appearance: string;
themeLight: string;
themeDark: string;
updateChannel: string;
}

View File

@@ -1,21 +0,0 @@
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { getKeyValue, setKeyValue } from './keyValueStore';
const key = ['window_pathname', getCurrent().label];
const namespace = 'no_sync';
const fallback = undefined;
export async function setPathname(value: string) {
await setKeyValue<string | undefined>({ key, namespace, value });
}
export async function maybeRestorePathname() {
if (window.location.pathname !== '/') {
return;
}
const pathname = await getKeyValue<string | undefined>({ key, namespace, fallback });
if (pathname != null) {
window.location.replace(pathname);
}
}

View File

@@ -22,7 +22,15 @@ export class Color {
}
static transparent(): Color {
return new Color('rgba(0, 0, 0, 0.1)', 'light');
return new Color('rgb(0,0,0)', 'light').translucify(1);
}
static white(): Color {
return new Color('rgb(0,0,0)', 'light').lower(1);
}
static black(): Color {
return new Color('rgb(0,0,0)', 'light').lift(1);
}
private clone(): Color {
@@ -80,6 +88,10 @@ export class Color {
return `hsla(${h}, ${s}%, ${l}%, ${a})`;
}
hex(): string {
return parseColor(this.css()).hex;
}
private _lighten(mod: number): Color {
const c = this.clone();
c.lightness = this.lightness + (100 - this.lightness) * mod;

376
src-web/lib/theme/themes.ts Normal file
View File

@@ -0,0 +1,376 @@
import { Color } from './color';
import type { YaakTheme } from './window';
export const yaakLight: YaakTheme = {
id: 'yaak-light',
name: 'Yaak',
background: new Color('#f2f4f7', 'light').lower(1),
foreground: new Color('hsl(219,23%,15%)', 'light'),
colors: {
primary: new Color('hsl(266,100%,70%)', 'light'),
secondary: new Color('hsl(220,24%,59%)', 'light'),
info: new Color('hsl(206,100%,48%)', 'light'),
success: new Color('hsl(155,95%,33%)', 'light'),
notice: new Color('hsl(45,100%,41%)', 'light'),
warning: new Color('hsl(30,100%,43%)', 'light'),
danger: new Color('hsl(335,75%,57%)', 'light'),
},
components: {
sidebar: {
background: new Color('#f2f4f7', 'light').lower(1),
},
},
};
export const yaakDark: YaakTheme = {
id: 'yaak-dark',
name: 'Yaak',
background: new Color('hsl(244,23%,12%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.17),
backgroundHighlightSecondary: new Color('hsl(244,23%,12%)', 'dark').lift(0.1),
foreground: new Color('#bcbad4', 'dark'),
colors: {
primary: new Color('hsl(266,100%,79%)', 'dark'),
secondary: new Color('hsl(245,23%,60%)', 'dark'),
info: new Color('hsl(206,100%,63%)', 'dark'),
success: new Color('hsl(150,99%,44%)', 'dark'),
notice: new Color('hsl(48,80%,63%)', 'dark'),
warning: new Color('hsl(28,100%,61%)', 'dark'),
danger: new Color('hsl(342,90%,68%)', 'dark'),
},
components: {
button: {
colors: {
primary: new Color('hsl(266,100%,79%)', 'dark').lower(0.1),
secondary: new Color('hsl(245,23%,60%)', 'dark').lower(0.1),
info: new Color('hsl(206,100%,63%)', 'dark').lower(0.1),
success: new Color('hsl(150,99%,44%)', 'dark').lower(0.1),
notice: new Color('hsl(48,80%,63%)', 'dark').lower(0.1),
warning: new Color('hsl(28,100%,61%)', 'dark').lower(0.1),
danger: new Color('hsl(342,90%,68%)', 'dark').lower(0.1),
},
},
input: {
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.18),
},
dialog: {
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.11),
},
sidebar: {
background: new Color('hsl(243,23%,15%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.09),
},
responsePane: {
background: new Color('hsl(243,23%,15%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.09),
},
appHeader: {
background: new Color('hsl(244,23%,12%)', 'dark'),
backgroundHighlight: new Color('hsl(244,23%,12%)', 'dark').lift(0.1),
},
},
};
const monokaiProOctagon: YaakTheme = {
id: 'monokai-pro-octagon',
name: 'Monokai Pro Octagon',
background: new Color('#282a3a', 'dark'),
foreground: new Color('#eaf2f1', 'dark'),
foregroundSubtle: new Color('#b2b9bd', 'dark'),
foregroundSubtler: new Color('#767b81', 'dark'),
colors: {
primary: new Color('#c39ac9', 'dark'),
secondary: new Color('#b2b9bd', 'dark'),
info: new Color('#9cd1bb', 'dark'),
success: new Color('#bad761', 'dark'),
notice: new Color('#ffd76d', 'dark'),
warning: new Color('#ff9b5e', 'dark'),
danger: new Color('#ff657a', 'dark'),
},
components: {
appHeader: {
background: new Color('#1e1f2b', 'dark'),
foreground: new Color('#b2b9bd', 'dark'),
foregroundSubtle: new Color('#767b81', 'dark'),
foregroundSubtler: new Color('#696d77', 'dark'),
},
button: {
colors: {
primary: new Color('#c39ac9', 'dark').lower(0.1).desaturate(0.1),
secondary: new Color('#b2b9bd', 'dark').lower(0.1).desaturate(0.1),
info: new Color('#9cd1bb', 'dark').lower(0.1).desaturate(0.1),
success: new Color('#bad761', 'dark').lower(0.1).desaturate(0.1),
notice: new Color('#ffd76d', 'dark').lower(0.1).desaturate(0.1),
warning: new Color('#ff9b5e', 'dark').lower(0.1).desaturate(0.1),
danger: new Color('#ff657a', 'dark').lower(0.1).desaturate(0.1),
},
},
},
};
const catppuccinLatte: YaakTheme = {
name: 'Catppuccin Latte',
id: 'catppuccin-light',
background: new Color('#eff1f5', 'light'),
foreground: new Color('#4c4f69', 'dark'),
foregroundSubtle: new Color('#6c6f85', 'light'),
foregroundSubtler: new Color('#8c8fa1', 'light'),
colors: {
primary: new Color('#8839ef', 'light'),
secondary: new Color('#6c6f85', 'light'),
info: new Color('#7287fd', 'light'),
success: new Color('#179299', 'light'),
notice: new Color('#df8e1d', 'light'),
warning: new Color('#fe640b', 'light'),
danger: new Color('#e64553', 'light'),
},
components: {
sidebar: {
background: new Color('#e6e9ef', 'light'),
backgroundHighlight: new Color('#e6e9ef', 'light').lift(0.05),
foregroundSubtler: new Color('#7287fd', 'light'),
},
appHeader: {
background: new Color('#dce0e8', 'light'),
backgroundHighlight: new Color('#e6e9ef', 'light').lift(0.05),
foregroundSubtler: new Color('#7287fd', 'light'),
},
},
};
const catppuccinMocha: YaakTheme = {
name: 'Catppuccin Mocha',
id: 'catppuccin-mocha',
background: new Color('#181825', 'dark'),
foreground: new Color('#cdd6f4', 'dark'),
foregroundSubtle: new Color('#a6adc8', 'dark'),
foregroundSubtler: new Color('#7f849c', 'dark'),
colors: {
primary: new Color('#c6a0f6', 'dark'),
secondary: new Color('#bac2de', 'dark'),
info: new Color('#89b4fa', 'dark'),
success: new Color('#a6e3a1', 'dark'),
notice: new Color('#f9e2af', 'dark'),
warning: new Color('#fab387', 'dark'),
danger: new Color('#f38ba8', 'dark'),
},
components: {
dialog: {
background: new Color('#181825', 'dark'),
},
sidebar: {
background: new Color('#1e1e2e', 'dark'),
backgroundHighlight: new Color('#1e1e2e', 'dark').lift(0.05),
},
appHeader: {
background: new Color('#11111b', 'dark'),
backgroundHighlight: new Color('#11111b', 'dark').lift(0.1),
},
responsePane: {
background: new Color('#1e1e2e', 'dark'),
backgroundHighlight: new Color('#1e1e2e', 'dark').lift(0.05),
},
button: {
colors: {
primary: new Color('#cba6f7', 'dark').lower(0.2).desaturate(0.2),
secondary: new Color('#bac2de', 'dark').lower(0.2).desaturate(0.2),
info: new Color('#89b4fa', 'dark').lower(0.2).desaturate(0.2),
success: new Color('#a6e3a1', 'dark').lower(0.2).desaturate(0.2),
notice: new Color('#f9e2af', 'dark').lower(0.2).desaturate(0.2),
warning: new Color('#fab387', 'dark').lower(0.2).desaturate(0.2),
danger: new Color('#f38ba8', 'dark').lower(0.2).desaturate(0.2),
},
},
},
};
const relaxing: YaakTheme = {
name: 'Relaxing',
id: 'relaxing',
background: new Color('#2b1e3b', 'dark'),
foreground: new Color('#ede2f5', 'dark'),
colors: {
primary: new Color('#cba6f7', 'dark'),
secondary: new Color('#bac2de', 'dark'),
info: new Color('#89b4fa', 'dark'),
success: new Color('#a6e3a1', 'dark'),
notice: new Color('#f9e2af', 'dark'),
warning: new Color('#fab387', 'dark'),
danger: new Color('#f38ba8', 'dark'),
},
};
export const rosePineMoon: YaakTheme = {
id: 'rose-pine-moon',
name: 'Rosé Pine Moon',
background: new Color('#232136', 'dark'),
foreground: new Color('#e0def4', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark'),
foregroundSubtler: new Color('#6e6a86', 'dark'),
colors: {
primary: new Color('#c4a7e7', 'dark'),
secondary: new Color('#908caa', 'dark'),
info: new Color('#68aeca', 'dark'),
success: new Color('#9ccfd8', 'dark'),
notice: new Color('#f6c177', 'dark'),
warning: new Color('#ea9a97', 'dark'),
danger: new Color('#eb6f92', 'dark'),
},
components: {
responsePane: {
background: new Color('#2a273f', 'dark'),
},
sidebar: {
background: new Color('#2a273f', 'dark'),
},
menu: {
background: new Color('#393552', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark').lift(0.15),
foregroundSubtler: new Color('#6e6a86', 'dark').lift(0.15),
},
},
};
export const rosePineDawn: YaakTheme = {
id: 'rose-pine-dawn',
name: 'Rosé Pine Dawn',
background: new Color('#faf4ed', 'light'),
backgroundHighlight: new Color('#dfdad9', 'light'),
backgroundHighlightSecondary: new Color('#f4ede8', 'light'),
foreground: new Color('#575279', 'light'),
foregroundSubtle: new Color('#797593', 'light'),
foregroundSubtler: new Color('#9893a5', 'light'),
colors: {
primary: new Color('#9070ad', 'light'),
secondary: new Color('#6e6a86', 'light'),
info: new Color('#2d728d', 'light'),
success: new Color('#4f8c96', 'light'),
notice: new Color('#cb862d', 'light'),
warning: new Color('#ce7b78', 'light'),
danger: new Color('#b4637a', 'light'),
},
components: {
responsePane: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
sidebar: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
appHeader: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
input: {
backgroundHighlight: new Color('#dfdad9', 'light'),
},
dialog: {
backgroundHighlight: new Color('#e8e4e2', 'light'),
},
menu: {
background: new Color('#f2e9e1', 'light'),
backgroundHighlight: new Color('#dfdad9', 'light'),
backgroundHighlightSecondary: new Color('#6e6a86', 'light'),
},
},
};
export const rosePine: YaakTheme = {
id: 'rose-pine',
name: 'Rosé Pine',
background: new Color('#191724', 'dark'),
foreground: new Color('#e0def4', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark'),
foregroundSubtler: new Color('#6e6a86', 'dark'),
colors: {
primary: new Color('#c4a7e7', 'dark'),
secondary: new Color('#6e6a86', 'dark'),
info: new Color('#67abcb', 'dark'),
success: new Color('#9cd8d8', 'dark'),
notice: new Color('#f6c177', 'dark'),
warning: new Color('#f1a3a1', 'dark'),
danger: new Color('#eb6f92', 'dark'),
},
components: {
responsePane: {
background: new Color('#1f1d2e', 'dark'),
},
sidebar: {
background: new Color('#1f1d2e', 'dark'),
},
menu: {
background: new Color('#393552', 'dark'),
foregroundSubtle: new Color('#908caa', 'dark').lift(0.15),
foregroundSubtler: new Color('#6e6a86', 'dark').lift(0.15),
},
},
};
export const githubDark: YaakTheme = {
id: 'github-dark',
name: 'GitHub',
background: new Color('#0d1218', 'dark'),
backgroundHighlight: new Color('#171c23', 'dark'),
backgroundHighlightSecondary: new Color('#1c2127', 'dark'),
foreground: new Color('#dce3eb', 'dark'),
foregroundSubtle: new Color('#88919b', 'dark'),
foregroundSubtler: new Color('#6b727d', 'dark'),
colors: {
primary: new Color('#a579ef', 'dark').lift(0.1),
secondary: new Color('#6b727d', 'dark').lift(0.1),
info: new Color('#458def', 'dark').lift(0.1),
success: new Color('#3eb24f', 'dark').lift(0.1),
notice: new Color('#dca132', 'dark').lift(0.1),
warning: new Color('#ec7934', 'dark').lift(0.1),
danger: new Color('#ee5049', 'dark').lift(0.1),
},
components: {
button: {
colors: {
primary: new Color('#a579ef', 'dark'),
secondary: new Color('#6b727d', 'dark'),
info: new Color('#458def', 'dark'),
success: new Color('#3eb24f', 'dark'),
notice: new Color('#dca132', 'dark'),
warning: new Color('#ec7934', 'dark'),
danger: new Color('#ee5049', 'dark'),
},
},
},
};
export const githubLight: YaakTheme = {
id: 'github-light',
name: 'GitHub',
background: new Color('#ffffff', 'light'),
backgroundHighlight: new Color('#e8ebee', 'light'),
backgroundHighlightSecondary: new Color('#f6f8fa', 'light'),
foreground: new Color('#1f2328', 'light'),
foregroundSubtle: new Color('#636c76', 'light'),
foregroundSubtler: new Color('#828d94', 'light'),
colors: {
primary: new Color('#8250df', 'light'),
secondary: new Color('#6e7781', 'light'),
info: new Color('#0969da', 'light'),
success: new Color('#1a7f37', 'light'),
notice: new Color('#9a6700', 'light'),
warning: new Color('#bc4c00', 'light'),
danger: new Color('#d1242f', 'light'),
},
};
export const yaakThemes = [
yaakLight,
yaakDark,
catppuccinMocha,
catppuccinLatte,
relaxing,
monokaiProOctagon,
rosePine,
rosePineMoon,
rosePineDawn,
githubLight,
githubDark,
];

View File

@@ -1,10 +1,9 @@
import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { indent } from '../indent';
import { Color } from './color';
export type Appearance = 'dark' | 'light' | 'system';
const DEFAULT_APPEARANCE: Appearance = 'system';
interface ThemeComponent {
background?: Color;
backgroundHighlight?: Color;
@@ -13,19 +12,26 @@ interface ThemeComponent {
foreground?: Color;
foregroundSubtle?: Color;
foregroundSubtler?: Color;
shadow?: Color;
colors?: Partial<RootColors>;
}
interface YaakTheme extends ThemeComponent {
export interface YaakTheme extends ThemeComponent {
id: string;
name: string;
components?: {
dialog?: ThemeComponent;
menu?: ThemeComponent;
toast?: ThemeComponent;
sidebar?: ThemeComponent;
responsePane?: ThemeComponent;
appHeader?: ThemeComponent;
button?: ThemeComponent;
banner?: ThemeComponent;
placeholder?: ThemeComponent;
urlBar?: ThemeComponent;
editor?: ThemeComponent;
input?: ThemeComponent;
};
}
@@ -42,94 +48,6 @@ interface RootColors {
type ColorName = keyof RootColors;
type ComponentName = keyof NonNullable<YaakTheme['components']>;
const yaakThemes: Record<string, YaakTheme> = {
yaakLight: {
name: 'Yaak (Light)',
background: new Color('#f2f4f7', 'light').lower(1),
foreground: new Color('hsl(219,23%,15%)', 'light'),
colors: {
primary: new Color('hsl(266,100%,70%)', 'light'),
secondary: new Color('hsl(220,24%,59%)', 'light'),
info: new Color('hsl(206,100%,48%)', 'light'),
success: new Color('hsl(155,95%,33%)', 'light'),
notice: new Color('hsl(45,100%,41%)', 'light'),
warning: new Color('hsl(30,100%,43%)', 'light'),
danger: new Color('hsl(335,75%,57%)', 'light'),
},
components: {
sidebar: {
background: new Color('#f2f4f7', 'light'),
},
},
} as YaakTheme,
yaakDark: {
name: 'Yaak Dark',
background: new Color('hsl(244,23%,12%)', 'dark'),
foreground: new Color('#bcbad4', 'dark'),
colors: {
primary: new Color('hsl(266,100%,79%)', 'dark'),
secondary: new Color('hsl(245,23%,60%)', 'dark'),
info: new Color('hsl(206,100%,63%)', 'dark'),
success: new Color('hsl(150,100%,37%)', 'dark'),
notice: new Color('hsl(48,80%,63%)', 'dark'),
warning: new Color('hsl(28,100%,61%)', 'dark'),
danger: new Color('hsl(342,90%,68%)', 'dark'),
},
components: {
sidebar: {
background: new Color('hsl(243,23%,15%)', 'dark'),
},
responsePane: {
background: new Color('hsl(243,23%,15%)', 'dark'),
},
},
},
catppuccin: {
name: 'Catppuccin',
background: new Color('#181825', 'dark'),
foreground: new Color('#cdd6f4', 'dark'),
foregroundSubtle: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.3),
foregroundSubtler: new Color('#cdd6f4', 'dark').lower(0.1).translucify(0.55),
colors: {
primary: new Color('#cba6f7', 'dark'),
secondary: new Color('#bac2de', 'dark'),
info: new Color('#89b4fa', 'dark'),
success: new Color('#a6e3a1', 'dark'),
notice: new Color('#f9e2af', 'dark'),
warning: new Color('#fab387', 'dark'),
danger: new Color('#f38ba8', 'dark'),
},
components: {
dialog: {
background: new Color('#181825', 'dark'),
},
sidebar: {
background: new Color('#1e1e2e', 'dark'),
},
appHeader: {
background: new Color('#11111b', 'dark'),
},
responsePane: {
background: new Color('#1e1e2e', 'dark'),
},
button: {
colors: {
primary: new Color('#cba6f7', 'dark').lower(0.2),
secondary: new Color('#bac2de', 'dark').lower(0.2),
info: new Color('#89b4fa', 'dark').lower(0.2),
success: new Color('#a6e3a1', 'dark').lower(0.2),
notice: new Color('#f9e2af', 'dark').lower(0.2),
warning: new Color('#fab387', 'dark').lower(0.2),
danger: new Color('#f38ba8', 'dark').lower(0.2),
},
},
},
},
};
type CSSVariables = Record<string, string | undefined>;
function themeVariables(theme?: ThemeComponent, base?: CSSVariables): CSSVariables | null {
@@ -147,6 +65,7 @@ function themeVariables(theme?: ThemeComponent, base?: CSSVariables): CSSVariabl
'--fg-subtle': theme?.foregroundSubtle?.css() ?? theme?.foreground?.lower(0.2).css(),
'--fg-subtler': theme?.foregroundSubtler?.css() ?? theme?.foreground?.lower(0.3).css(),
'--border-focus': theme?.colors?.info?.css(),
'--shadow': theme?.shadow?.css() ?? Color.black().translucify(0.7).css(),
};
for (const [color, value] of Object.entries(theme?.colors ?? {})) {
@@ -261,7 +180,7 @@ function placeholderCSS(color: ColorName, colors?: Partial<RootColors>): string
].join('\n\n');
}
function isThemeDark(theme: YaakTheme): boolean {
export function isThemeDark(theme: YaakTheme): boolean {
if (theme.background && theme.foreground) {
return theme.foreground.lighterThan(theme.background);
}
@@ -269,10 +188,11 @@ function isThemeDark(theme: YaakTheme): boolean {
return false;
}
setThemeOnDocument(yaakThemes.yaakLight!);
setThemeOnDocument(yaakThemes.yaakDark!);
export function getThemeCSS(theme: YaakTheme): string {
theme.components = theme.components ?? {};
// Toast defaults to menu styles
theme.components.toast = theme.components.toast ?? theme.components.menu;
let themeCSS = '';
try {
const baseCss = variablesToCSS(null, themeVariables(theme));
@@ -298,29 +218,24 @@ export function getThemeCSS(theme: YaakTheme): string {
return themeCSS;
}
export function setAppearanceOnDocument(appearance: Appearance = DEFAULT_APPEARANCE) {
const resolvedAppearance = appearance === 'system' ? getPreferredAppearance() : appearance;
document.documentElement.setAttribute('data-resolved-appearance', resolvedAppearance);
}
export function setThemeOnDocument(theme: YaakTheme) {
document.documentElement.setAttribute('data-theme', theme.name);
const darkOrLight = isThemeDark(theme) ? 'dark' : 'light';
let existingStyleEl = document.head.querySelector(`style[data-theme-definition=${darkOrLight}]`);
if (!existingStyleEl) {
const styleEl = document.createElement('style');
export function addThemeStylesToDocument(theme: YaakTheme) {
let styleEl = document.head.querySelector(`style[data-theme]`);
if (!styleEl) {
styleEl = document.createElement('style');
document.head.appendChild(styleEl);
existingStyleEl = styleEl;
}
existingStyleEl.textContent = [
styleEl.setAttribute('data-theme', theme.id);
styleEl.textContent = [
`/* ${theme.name} */`,
`[data-resolved-appearance="${isThemeDark(theme) ? 'dark' : 'light'}"] {`,
`[data-theme="${theme.id}"] {`,
getThemeCSS(theme),
'}',
].join('\n');
existingStyleEl.setAttribute('data-theme-definition', darkOrLight);
}
export function setThemeOnDocument(theme: YaakTheme) {
document.documentElement.setAttribute('data-theme', theme.id);
}
export function getPreferredAppearance(): Appearance {
@@ -330,8 +245,13 @@ export function getPreferredAppearance(): Appearance {
export function subscribeToPreferredAppearanceChange(
cb: (appearance: Appearance) => void,
): () => void {
const listener = (e: MediaQueryListEvent) => cb(e.matches ? 'dark' : 'light');
const m = window.matchMedia('(prefers-color-scheme: dark)');
m.addEventListener('change', listener);
return () => m.removeEventListener('change', listener);
const container = { unsubscribe: () => {} };
getCurrent()
.onThemeChanged((t) => cb(t.payload))
.then((l) => {
container.unsubscribe = l;
});
return () => container.unsubscribe();
}

View File

@@ -2,11 +2,9 @@ import { getCurrent } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { attachConsole } from 'tauri-plugin-log-api';
import { App } from './components/App';
import './main.css';
import { getSettings } from './lib/store';
import type { Appearance } from './lib/theme/window';
import { setAppearanceOnDocument } from './lib/theme/window';
// Hide decorations here because it doesn't work in Rust for some reason (bug?)
const osType = await type();
@@ -14,11 +12,7 @@ if (osType !== 'macos') {
await getCurrent().setDecorations(false);
}
// await attachConsole();
// await maybeRestorePathname();
const settings = await getSettings();
setAppearanceOnDocument(settings.appearance as Appearance);
await attachConsole();
window.addEventListener('keydown', (e) => {
// Hack to not go back in history on backspace. Check for document body

View File

@@ -59,6 +59,10 @@ module.exports = {
'4xl': '2.5rem',
'5xl': '3rem',
},
boxShadow: {
DEFAULT: '0 1px 3px 0 var(--shadow);',
lg: '0 10px 15px -3px var(--shadow)',
},
colors: {
'transparent': 'transparent',
'placeholder': 'var(--fg-subtler)',
@@ -88,7 +92,7 @@ module.exports = {
require('@tailwindcss/container-queries'),
plugin(function ({addVariant}) {
addVariant('hocus', ['&:hover', '&:focus-visible', '&.focus:focus']);
addVariant('focus-visible-or-class', ['&:focus-visible']);
addVariant('focus-visible-or-class', ['&:focus-visible', '&.focus:focus']);
}),
],
};