diff --git a/CHANGELOG.md b/CHANGELOG.md index 68705c4..1022ea0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ ShellCanvas follows semantic versioning for published SDK packages. Desktop rele - Changed: **Apps** is now **App Manager**, a store-style manager with tiles, search, per-app details (status, source, approved access, update, disable and confirmed removal) and an **Add apps** page for GitHub and package-file installs. - Added (app packages and SDK): optional one-line `description` and embedded `icon` fields. `shellcanvas-app build` embeds a PNG, JPEG, WebP or SVG icon of at most 256 KiB, and `shellcanvas-app init --description` and `repository` fill descriptions. Apps without an icon get a generic one. Packages that use the new fields need a desktop newer than 0.1.0; existing packages are unchanged. - Fixed: installed apps opened in a workspace without an SSH target (connection adapters, the preview) now finish starting. Their environment event carried an undefined `host.target`, which the event journal rejected; the field is now omitted. +- Fixed: closing ShellCanvas while a local drive is attached no longer hides the window and leaves ShellCanvas and the Drive Bridge running in the background. Quitting is refused with a message until drives are detached, and no drive can start attaching once the window is closing. +- Fixed (Windows): drive letters held by remembered but disconnected network drives are no longer offered or accepted for drive attachment. +- Changed: the built-in Canvas theme (1.1.0) uses neutral white and charcoal surfaces with blue accents in both light and dark, replacing the green-tinted palette. Custom themes are unchanged. ## 0.1.1 diff --git a/docs/filesystem-integration-progress.md b/docs/filesystem-integration-progress.md index fc8069c..3527d0c 100644 --- a/docs/filesystem-integration-progress.md +++ b/docs/filesystem-integration-progress.md @@ -2,6 +2,14 @@ Updated 2026-09-11. Goal remains active. This is not a release/completion claim. +Resumed after the Codex restart. The corrected isolated desktop build completed +successfully (3m 14s; executable timestamp 2026-09-11 01:43:23 local). After the +detach fix, the user confirmed the Windows flow worked end to end. This is manual, +user-reported acceptance of the attach/detach/quit checklist. Further testing is +stopped to conserve their OpenAI weekly quota; no additional CI was triggered. The +disposable test folder is `/tmp/shellcanvas-quit-check-Ijl8MnLz`. The desktop +changes are committed; the detailed local handoff is `.local/drive-bridge-resume.md`. + ## Accepted scope Implement optional core filesystem operations and machinery, with Windows @@ -12,6 +20,44 @@ Include the dependencies' licensing/distribution limitations in that app. ## Current implementation +- Manual Windows testing found detach stuck on two handles on the empty fixture. + The bridge counted passive directory browsing as busy. Read-only directory + contexts now release their detach blocker; all file contexts (including mapped + files) and mutation-capable directory contexts remain blockers. Three focused + bookkeeping tests pass. The native regression now holds a browsing directory + across detach. It is now executed and passes in `native_windows_mount` when + `SHELLCANVAS_NATIVE_WINDOWS_TEST=1` and + `SHELLCANVAS_SKIP_PRIVILEGED_VOLUME_FLUSH=1` are set. Updated bridge binary + built 2026-09-11 01:49:46 local; the old isolated helper was stopped only + verifying its profile, W: target, and empty directory, to recover the stuck + test mount. +- WinFsp is installed on the development PC and its launcher is running. Local + native acceptance passes, including I/O, mapped files, metadata, native copy, + injected failures, busy-detach refusal, ordinary detach and pipe-loss cleanup. + This account cannot open a volume for privileged volume-wide flushing; the + explicit `SHELLCANVAS_SKIP_PRIVILEGED_VOLUME_FLUSH=1` test switch omits only + that subtest. Per-file flush remains exercised and the CI default still runs + the privileged check. No CI was triggered for this verification. +- A separate `dev.shellcanvas.driveprobe` profile exercised the actual Windows + file chooser (including cancellation), hash review, bridge installation, + attachment of a disposable SFTP folder on an authorized test host as W:, and + Open folder in Explorer. Native file creation, directory creation and Unicode-file rename + succeeded. Notepad opened and saved the remote text file; an independent SSH + read confirmed the exact saved text. The normal app profile was not changed. + Detach with open handles was refused with a visible explanation. +- The live quit test exposed a Tauri interaction: JavaScript `onCloseRequested` + destroys the window unless its callback prevents default, even if Rust blocks + close. The frontend now always prevents this automatic destruction and asks + the backend to close. The final backend check is serialized with attachment + reservation and marks closing before destruction so waiting attachments cannot + start afterwards. The subsequent attach/detach/quit checklist was confirmed + successful by the user; no further automated UI retest was run. +- Windows attachment selection and preflight now also reserve remembered, + disconnected network-drive letters via `WNetGetConnectionW`; active mount + checks alone omitted this PC's X: and Y:. The standalone bridge and its native + harness apply the same check. Mount cleanup deliberately still checks active + mounts, so a remembered network connection is not mistaken for a live bridge. + The updated bridge native suite passes locally (10.96 seconds). - Unconfirmed SFTP CLOSE now retains cleanup ownership in a guard until the server acknowledges success. Caller cancellation, rejection, timeout, or a dropped cleanup task retires the mount's dedicated channel, releasing remote @@ -282,9 +328,9 @@ Include the dependencies' licensing/distribution limitations in that app. use isolated temporary profiles; no bridge was installed into the user's profile. Three installer tests pass, including changed-review rejection preserving the previous installation. Frontend production build, desktop clippy and 640px - preview overflow checks pass. Native chooser/approval still needs end-to-end use. - Native chooser/approval and an actual desktop-launched Windows mapping remain - unverified; the implementation is a development preview. + preview overflow checks pass. Native chooser/approval and a desktop-launched + Windows SFTP mapping subsequently passed in the isolated profile above; + the implementation remains a development preview until the other gates close. - Files now offers **Attach to this computer…** for folders/current directories, and an Attach action beside mounted volume locations. The dialog checks optional provider support and installation, defaults to read-only, offers a Windows drive @@ -373,10 +419,11 @@ Include the dependencies' licensing/distribution limitations in that app. disposable FUSE test rather than changing the server's installed toolchain. - Verified the official WinFsp 2.1.25156 MSI signature (Navimatics). Installation failed with Windows Installer 1925 / exit 1603: administrator privileges needed. - No successful driver installation on this PC has been established. CI now + The user subsequently installed WinFsp successfully; local acceptance is recorded + above. CI also installs the official runtime on its disposable Windows runner, checking the - pinned MSI SHA-256 and Authenticode signer before native tests. The local UAC question - is pending. This is an OS privilege issue, not an automatic approval rejection. + pinned MSI SHA-256 and Authenticode signer before native tests. The earlier + installer failure was an OS privilege issue, not an automatic approval rejection. Installer/log are under `.local/bridge-tools`. Workspace-only libclang 18.1.1 is available at `.local/bridge-tools/python/clang/native` for Windows builds. @@ -426,10 +473,11 @@ driver, remote account, normal app profile, or CI run was used by these checks. ## Remaining completion gates -1. Native end-to-end installation verification and an actual desktop-created - mapping, including chooser cancel, missing driver and changed executable. - The manager/UI/source leases are implemented; native user-flow verification - remains, including disconnect/source replacement/quit with busy local files. +1. Native chooser cancel, installation, desktop-created SFTP mapping, Explorer + launch, Notepad save and busy-detach refusal now pass on this PC. Remaining: + changed-executable/source-retirement and source-replacement user flows, plus + full failure recovery. The corrected ordinary detach/quit flow has subsequent + user-reported end-to-end acceptance. 2. Open folder and explicit Retry cleanup controls are implemented. Retry retains the original helper/job ownership and connection reservation. After process shutdown, native OS mount-table checks must confirm the location is unmounted; @@ -454,10 +502,10 @@ driver, remote account, normal app profile, or CI run was used by these checks. chooser/source-retirement race remains to be exercised without touching the normal user profile. 4. Native Windows file API, replacement-save, capacity, basic error and busy-detach - checks now pass in disposable WinFsp CI. Remaining: desktop-created SFTP mapping, - Explorer/ordinary editor acceptance (native directory rename/open-handle - semantics are now verified above), - disconnect behavior and failure recovery. Local driver setup is still pending. + checks pass in disposable WinFsp CI and locally. Local driver setup and the + desktop-created SFTP/Explorer/Notepad path are now verified above. Remaining: + full desktop failure recovery. The corrected Windows lifecycle checklist has + user-reported end-to-end acceptance; wider failure matrices remain unverified. 5. Native Linux FUSE file operations, directory rewind/rename, truncate, flush, mapped files, busy detach and ordinary unmount now pass as root and as an unprivileged user on Linux 6.8. Pipe loss with an open file also passes in both @@ -467,6 +515,8 @@ driver, remote account, normal app profile, or CI run was used by these checks. other supported runtimes. Do not infer those from one kernel/runtime. 6. Verify modern macOS compilation and native runtime as available. fuser's kernel/libfuse backend is implemented; FSKit operation is not established. + On this Windows machine, modern macOS runtime verification cannot be + executed yet, so this gate remains unverified here. Do not claim an OS version/runtime works solely because Linux compiled. 7. Resolve currently documented limits before calling the release ready: Windows cleanup-time deletion warnings and busy-detach control are implemented diff --git a/docs/themes.md b/docs/themes.md index 132632d..57acced 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -1,9 +1,10 @@ # Themes and appearance Open **Desktop settings → Desktop**. Choose a theme and **Light**, **Dark**, or -**Follow system**. Canvas, the built-in theme, has a pale alpine daytime palette -and a deep blue-green evening palette. Changes apply to open windows, menus, -shared dialogs, the editor and the terminal without reconnecting hosts. +**Follow system**. Canvas, the built-in theme, uses neutral white and charcoal +surfaces with blue accents, inspired by macOS and Windows 11. Changes apply to +open windows, menus, shared dialogs, the editor and the terminal without +reconnecting hosts. The same page controls the wallpaper, interface scale (80–150%), top toolbar height and dock icon size. Scaling changes text, controls and spacing through diff --git a/examples/themes/canvas-study/shellcanvas.theme.json b/examples/themes/canvas-study/shellcanvas.theme.json index 34602c7..1a30490 100644 --- a/examples/themes/canvas-study/shellcanvas.theme.json +++ b/examples/themes/canvas-study/shellcanvas.theme.json @@ -2,38 +2,38 @@ "format": 1, "kind": "shellcanvas-theme", "id": "example.canvas-study", - "version": "1.0.0", + "version": "1.1.0", "name": "Canvas Study", "author": "Your name", "description": "A starting point for your own light and dark desktop theme.", "variants": { "dark": { "colors": { - "base": "#122330", - "surface": "#1b2b36", - "raised": "#263943", - "inset": "#15242f", - "text": "#e0e9ea", - "muted": "#a1b8c2", - "accent": "#a3d9c4", - "onAccent": "#173a30", - "border": "#405660", - "hover": "#2b414b", - "selection": "#35564f", + "base": "#19191c", + "surface": "#242427", + "raised": "#303034", + "inset": "#1d1d20", + "text": "#f2f2f5", + "muted": "#b0b0ba", + "accent": "#7ab8ff", + "onAccent": "#102745", + "border": "#48484f", + "hover": "#39393f", + "selection": "#293f5b", "danger": "#f1a7a7", - "dangerSoft": "#452f36", - "warning": "#e5c68a", - "warningSoft": "#3f3a30", - "success": "#a3d9b8", - "shadow": "#07121b", - "scrim": "#07121b", - "terminal": "#111d27", - "terminalText": "#d4e2e7", - "sky": "#193e50", - "glow": "#8cad9f", - "far": "#54757b", - "mid": "#304f5d", - "near": "#18333f" + "dangerSoft": "#462d33", + "warning": "#f2c572", + "warningSoft": "#413527", + "success": "#91d6a3", + "shadow": "#08080d", + "scrim": "#111118", + "terminal": "#161619", + "terminalText": "#e5e5ea", + "sky": "#102655", + "glow": "#536dd4", + "far": "#455b96", + "mid": "#293c72", + "near": "#172345" }, "windowRadius": 14, "windowHeader": 43, @@ -45,31 +45,31 @@ }, "light": { "colors": { - "base": "#e9eeeb", - "surface": "#f8faf7", - "raised": "#edf2ee", - "inset": "#e5ece7", - "text": "#223b3c", - "muted": "#516b6c", - "accent": "#296e60", + "base": "#f0f0f3", + "surface": "#fafafa", + "raised": "#f0f0f3", + "inset": "#e8e8ed", + "text": "#202024", + "muted": "#585861", + "accent": "#005fb8", "onAccent": "#ffffff", - "border": "#c0d0c8", - "hover": "#e1eae3", - "selection": "#c8e1d4", + "border": "#c9c9d1", + "hover": "#e2e2e8", + "selection": "#d6e7fb", "danger": "#a02f3d", "dangerSoft": "#f8e6e5", - "warning": "#795916", - "warningSoft": "#f3ead5", - "success": "#2e7451", - "shadow": "#3c5854", - "scrim": "#304d47", - "terminal": "#f3f7f1", - "terminalText": "#243c36", - "sky": "#d4e4df", - "glow": "#fff0cb", - "far": "#9fbdb8", - "mid": "#799f9b", - "near": "#527e7c" + "warning": "#805400", + "warningSoft": "#fff1d6", + "success": "#26733b", + "shadow": "#252535", + "scrim": "#242432", + "terminal": "#ffffff", + "terminalText": "#242429", + "sky": "#dde8ff", + "glow": "#f4edff", + "far": "#a6b7e2", + "mid": "#748dc3", + "near": "#3b558c" }, "windowRadius": 14, "windowHeader": 43, diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d8effeb..c4afa59 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,7 +37,7 @@ rand = "0.10" [target.'cfg(windows)'.dependencies] windows-core = "0.61.2" -windows = { version = "0.61.3", features = ["Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Memory", "Win32_System_DataExchange", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } +windows = { version = "0.61.3", features = ["Win32_NetworkManagement_WNet", "Win32_Foundation", "Win32_Graphics_Gdi", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_Ole", "Win32_System_Memory", "Win32_System_DataExchange", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } [target.'cfg(target_os = "macos")'.dependencies] libc = "0.2" diff --git a/src-tauri/src/drive_mappings.rs b/src-tauri/src/drive_mappings.rs index 3b29703..95213e1 100644 --- a/src-tauri/src/drive_mappings.rs +++ b/src-tauri/src/drive_mappings.rs @@ -357,9 +357,12 @@ pub async fn attach_drive( // Reserve while holding the registry lock: disconnect/replacement use the same // order and cannot retire this source between validation and grant ownership. let _transition = state.mount_transition.lock().await; + if *_transition { + return Err("ShellCanvas is closing; no new drive can be attached.".into()); + } // Read the local OS table before reserving, without probing filesystem contents. - if crate::local_mounts::occupied(&target)? { - return Err("That local location is already mounted. Choose another location.".into()); + if crate::local_mounts::reserved_for_attachment(&target)? { + return Err("That local location is already mounted or reserved. Choose another location.".into()); } let registry = state.registry.lock().await; let session = registry diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b448832..94936b2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -51,7 +51,9 @@ use workspace_services::WorkspaceServices as ActiveSession; #[derive(Default)] struct DesktopState { mappings: drive_mappings::Mappings, - mount_transition: Mutex<()>, + // True once final window destruction has been requested; attachments waiting + // for this lock must not start after the UI has gone away. + mount_transition: Mutex, directories: directories::DirectoryReaders, registry: Arc>>, next_id: AtomicU64, @@ -730,6 +732,30 @@ async fn close_terminal( Ok(()) } +/// The frontend always prevents Tauri's automatic destroy. Serialize the final +/// close with attachment reservation so a newly starting mount cannot lose its UI. +#[tauri::command] +async fn request_app_close( + window: tauri::Window, + state: State<'_, DesktopState>, +) -> Result<(), String> { + let mut closing = state.mount_transition.lock().await; + if *closing { + return Ok(()); + } + if state.mappings.has_running() { + use tauri::Emitter; + let _ = window.emit("drive-mappings-close-blocked", ()); + return Err("Detach local drives in Settings → Files before quitting ShellCanvas.".into()); + } + *closing = true; + if let Err(error) = window.destroy() { + *closing = false; + return Err(error.to_string()); + } + Ok(()) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -794,6 +820,7 @@ pub fn run() { } let handler: fn(tauri::ipc::Invoke) -> bool = tauri::generate_handler![ client_platform, + request_app_close, drive_mappings::cancel_drive_startup, drive_bridge_install::drive_bridge_installation, drive_bridge_install::review_drive_bridge, diff --git a/src-tauri/src/local_mounts.rs b/src-tauri/src/local_mounts.rs index dccc27e..a821bc1 100644 --- a/src-tauri/src/local_mounts.rs +++ b/src-tauri/src/local_mounts.rs @@ -9,11 +9,58 @@ pub(crate) fn available_drive_letters() -> Result, String> { if mask == 0 { return Err(std::io::Error::last_os_error().to_string()); } - Ok((b'D'..=b'Z') - .rev() - .filter(|letter| mask & (1 << (letter - b'A')) == 0) - .map(|letter| format!("{}:", char::from(letter))) - .collect()) + let mut available = Vec::new(); + for letter in (b'D'..=b'Z').rev() { + let target = format!("{}:", char::from(letter)); + if mask & (1 << (letter - b'A')) == 0 && !network_drive_reserved(&target)? { + available.push(target); + } + } + Ok(available) +} + +#[cfg(windows)] +fn network_drive_reserved(target: &str) -> Result { + use windows::{core::HSTRING, Win32::NetworkManagement::WNet::WNetGetConnectionW}; + let mut remote = [0u16; 256]; + let mut length = remote.len() as u32; + let status = unsafe { + WNetGetConnectionW( + &HSTRING::from(target), + Some(windows::core::PWSTR(remote.as_mut_ptr())), + &mut length, + ) + }; + network_connection_reserved(status.0) +} + +#[cfg(windows)] +fn network_connection_reserved(status: u32) -> Result { + use windows::Win32::Foundation::{ + ERROR_CONNECTION_UNAVAIL, ERROR_MORE_DATA, ERROR_NOT_CONNECTED, NO_ERROR, + }; + match status { + s if s == NO_ERROR.0 || s == ERROR_MORE_DATA.0 || s == ERROR_CONNECTION_UNAVAIL.0 => { + Ok(true) + } + s if s == ERROR_NOT_CONNECTED.0 => Ok(false), + other => Err(format!( + "Unable to check reserved network drives: {}", + std::io::Error::from_raw_os_error(other as i32) + )), + } +} + +/// Reservations include remembered disconnected network drives. Cleanup checks +/// deliberately use occupied() instead: a remembered drive is not our live mount. +pub(crate) fn reserved_for_attachment(target: &Path) -> Result { + if occupied(target)? { + return Ok(true); + } + #[cfg(windows)] + return network_drive_reserved(target.to_str().ok_or("Invalid local drive")?); + #[cfg(not(windows))] + Ok(false) } #[cfg(windows)] @@ -127,6 +174,24 @@ pub(crate) fn occupied(_: &Path) -> Result { #[cfg(test)] mod tests { use super::*; + #[cfg(windows)] + #[test] + fn disconnected_network_connections_still_reserve_the_letter() { + use windows::Win32::Foundation::*; + for status in [NO_ERROR, ERROR_MORE_DATA, ERROR_CONNECTION_UNAVAIL] { + assert_eq!(network_connection_reserved(status.0), Ok(true)); + } + assert_eq!( + network_connection_reserved(ERROR_NOT_CONNECTED.0), + Ok(false) + ); + assert!(network_connection_reserved(ERROR_ACCESS_DENIED.0).is_err()); + let available = available_drive_letters().expect("Read local drive reservations"); + for letter in &available { + assert!(!reserved_for_attachment(Path::new(letter)).unwrap()); + } + println!("Available attachment letters: {available:?}"); + } #[test] fn native_system_volume_is_present_without_filesystem_io() { #[cfg(windows)] diff --git a/src/App.tsx b/src/App.tsx index c016eb4..5cd31eb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -154,12 +154,21 @@ export default function App({ void import("@tauri-apps/api/window") .then(async ({ getCurrentWindow }) => { const stop = await getCurrentWindow().onCloseRequested((event) => { + // Tauri destroys the window after this callback unless we prevent it, + // even when Rust has independently rejected the close request. + event.preventDefault(); if ( !closeAllowed.current && (closeState.current.hasUnsaved || closeState.current.hasBusy) ) { - event.preventDefault(); setCloseApp(true); + } else { + void import("@tauri-apps/api/core") + .then(({ invoke }) => invoke("request_app_close")) + .catch((error) => { + closeAllowed.current = false; + setToast(String(error)); + }); } }); if (disposed) stop(); diff --git a/src/preview.ts b/src/preview.ts index 8f2dc30..f2ebf82 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -134,14 +134,14 @@ export const previewServices: HostServices = { data: Array.from(new TextEncoder().encode(text)), }); output( - "\x1b[38;2;115;205;173m ShellCanvas\x1b[0m / design preview\r\n\r\n Your remote workspace, one connection away.\r\n \x1b[38;2;137;153;166mSample terminal • no commands are executed\x1b[0m\r\n\r\n\x1b[38;2;115;205;173mdemo@atlas\x1b[0m \x1b[38;2;153;183;210m~\x1b[0m $ ", + "\x1b[34m ShellCanvas\x1b[0m / design preview\r\n\r\n Your remote workspace, one connection away.\r\n \x1b[38;2;137;153;166mSample terminal • no commands are executed\x1b[0m\r\n\r\n\x1b[34mdemo@atlas\x1b[0m \x1b[38;2;153;183;210m~\x1b[0m $ ", ); return { resizable: true, write: async (text) => { if (typeof text === "string" ? text.includes("\r") : text.includes(13)) output( - "\r\nPreview only. Connect in the native app to run commands.\r\n\x1b[38;2;115;205;173mdemo@atlas\x1b[0m ~ $ ", + "\r\nPreview only. Connect in the native app to run commands.\r\n\x1b[34mdemo@atlas\x1b[0m ~ $ ", ); }, resize: async () => {}, diff --git a/src/themes/model.test.ts b/src/themes/model.test.ts index d633771..2eb8fb4 100644 --- a/src/themes/model.test.ts +++ b/src/themes/model.test.ts @@ -137,7 +137,7 @@ describe("installed theme collection", () => { createThemeStore(() => storage) .getSnapshot() .themes.map((theme) => theme.version), - ).toEqual(["1.0.0", "1.1.0"]); + ).toEqual([canvasTheme.version, "1.1.0"]); expect(() => store.install(canvasTheme)).toThrow("cannot be replaced"); expect(() => store.remove(canvasTheme.id)).toThrow("built in"); store.remove("studio.paper"); diff --git a/src/themes/model.ts b/src/themes/model.ts index b555f57..c4578a0 100644 --- a/src/themes/model.ts +++ b/src/themes/model.ts @@ -159,39 +159,39 @@ export const canvasTheme: CanvasTheme = { format: 1, kind: "shellcanvas-theme", id: "org.shellcanvas.canvas", - version: "1.0.0", + version: "1.1.0", name: "Canvas", author: "ShellCanvas", description: - "Quiet alpine mornings. Deep, restful evenings. One workspace, in two lights.", + "Clean white. Quiet charcoal. A familiar desktop, with a clear blue accent.", variants: { dark: { colors: { - base: "#122330", - surface: "#1b2b36", - raised: "#263943", - inset: "#15242f", - text: "#e0e9ea", - muted: "#a1b8c2", - accent: "#a3d9c4", - onAccent: "#173a30", - border: "#405660", - hover: "#2b414b", - selection: "#35564f", + base: "#19191c", + surface: "#242427", + raised: "#303034", + inset: "#1d1d20", + text: "#f2f2f5", + muted: "#b0b0ba", + accent: "#7ab8ff", + onAccent: "#102745", + border: "#48484f", + hover: "#39393f", + selection: "#293f5b", danger: "#f1a7a7", - dangerSoft: "#452f36", - warning: "#e5c68a", - warningSoft: "#3f3a30", - success: "#a3d9b8", - shadow: "#07121b", - scrim: "#07121b", - terminal: "#111d27", - terminalText: "#d4e2e7", - sky: "#193e50", - glow: "#8cad9f", - far: "#54757b", - mid: "#304f5d", - near: "#18333f", + dangerSoft: "#462d33", + warning: "#f2c572", + warningSoft: "#413527", + success: "#91d6a3", + shadow: "#08080d", + scrim: "#111118", + terminal: "#161619", + terminalText: "#e5e5ea", + sky: "#102655", + glow: "#536dd4", + far: "#455b96", + mid: "#293c72", + near: "#172345", }, windowRadius: 14, windowHeader: 43, @@ -203,31 +203,31 @@ export const canvasTheme: CanvasTheme = { }, light: { colors: { - base: "#e9eeeb", - surface: "#f8faf7", - raised: "#edf2ee", - inset: "#e5ece7", - text: "#223b3c", - muted: "#516b6c", - accent: "#296e60", + base: "#f0f0f3", + surface: "#fafafa", + raised: "#f0f0f3", + inset: "#e8e8ed", + text: "#202024", + muted: "#585861", + accent: "#005fb8", onAccent: "#ffffff", - border: "#c0d0c8", - hover: "#e1eae3", - selection: "#c8e1d4", + border: "#c9c9d1", + hover: "#e2e2e8", + selection: "#d6e7fb", danger: "#a02f3d", dangerSoft: "#f8e6e5", - warning: "#795916", - warningSoft: "#f3ead5", - success: "#2e7451", - shadow: "#3c5854", - scrim: "#304d47", - terminal: "#f3f7f1", - terminalText: "#243c36", - sky: "#d4e4df", - glow: "#fff0cb", - far: "#9fbdb8", - mid: "#799f9b", - near: "#527e7c", + warning: "#805400", + warningSoft: "#fff1d6", + success: "#26733b", + shadow: "#252535", + scrim: "#242432", + terminal: "#ffffff", + terminalText: "#242429", + sky: "#dde8ff", + glow: "#f4edff", + far: "#a6b7e2", + mid: "#748dc3", + near: "#3b558c", }, windowRadius: 14, windowHeader: 43, diff --git a/src/themes/theme.css b/src/themes/theme.css index 4b6b746..254d792 100644 --- a/src/themes/theme.css +++ b/src/themes/theme.css @@ -90,15 +90,42 @@ :root[data-theme] .bottom-status, :root[data-theme] .desktop-caption, :root[data-theme] .disconnect-button { - color: #e0efec; - text-shadow: 0 1px 3px #122330; + color: #f2f2f7; + text-shadow: 0 1px 3px #171725; } :root[data-theme] .bottom-status svg { - color: #a8d8c0; + color: var(--sc-accent); } :root[data-theme] .dock .running::after { background: var(--sc-accent); } +/* Canvas's default chrome is neutral. App identity colors stay distinct, while + custom themes retain their own palette and the existing icon treatment. */ +:root[data-theme="org.shellcanvas.canvas"] .app-empty { + background: transparent; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon { + background: linear-gradient(145deg, #656873, #383b45); + color: #f5f5f8; + border-color: #ffffff30; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.launcher-icon, +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.connections { + background: linear-gradient(145deg, #70b3ff, #266ace); + color: #ffffff; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.files { + background: linear-gradient(145deg, #ffe39a, #eeb54a); + color: #694711; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.editor { + background: linear-gradient(145deg, #d3c9fa, #9c8acb); + color: #3b305e; +} +:root[data-theme="org.shellcanvas.canvas"] .dock-app-icon.settings { + background: linear-gradient(145deg, #e1e2e9, #a8aab7); + color: #353641; +} :root[data-custom-wallpaper="true"] .wallpaper-custom .landscape { background-color: var(--sc-base); background-image: var(--sc-wallpaper-image);