Skip to content

Commit 90e2010

Browse files
committed
Design: apply unified design system
- Add design tokens (radius, shadow, transition, z-index, spacing, font-size) - Rename border/text tokens to match design system naming - Read macOS accent color via NSColor, inject into webview with live updates - Migrate cursor highlight from hardcoded blue to accent-subtle - Create shared Button component, roll out across all 9 dialogs - Replace ~300 hardcoded values with design tokens across 70+ components - Remove old hover/cursor tokens, clean up hardcoded colors - Website: swap Inter → Geist Sans, warm up dark palette, add light mode - Add empty folder state to file list
1 parent 141a12b commit 90e2010

98 files changed

Lines changed: 1322 additions & 1076 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.interface-design/system.md

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,6 @@ The folder color setting (System Settings > Appearance > Folder color) is separa
3333
accent (theme) color for interactive UI chrome. This matches macOS intent: accent is for controls, folder tint is
3434
cosmetic.
3535

36-
**Migration note:** The existing `--color-button-hover` and `--color-bg-hover` tokens (hardcoded blue-tinted rgba
37-
values) will be removed. Interactive hover states migrate to `--color-accent-subtle` (accent-derived). Neutral hover
38-
states (non-interactive surfaces) migrate to `--color-bg-tertiary`. Affected components: ModalDialog, Notification,
39-
DualPaneExplorer, SortableHeader, ShareBrowser, NetworkBrowser, viewer page, CommandPalette, and settings components
40-
(9 files, ~17 occurrences).
41-
4236
**Website:** Mustard yellow `#ffc206` is the brand accent. Used for CTAs, links, and emphasis. Hover: `#ffd23f`. Glow:
4337
`rgba(255, 194, 6, 0.4)`.
4438

@@ -165,9 +159,6 @@ to anchor the file list below them. Kept as a separate token rather than reusing
165159
--font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace;
166160
```
167161

168-
**Migration note:** The current code includes `'Segoe UI'` in the font stack (a Windows font). Remove it — Cmdr is
169-
macOS-only.
170-
171162
**Type scale.** Fixed `px`, not `rem` — the app scales with macOS display settings, not browser font preferences.
172163
`html { font-size: 16px }` is hardcoded to establish `1rem = 16px`. The body inherits this. The tokens below are for
173164
explicit use on specific elements, not as a global cascade.
@@ -493,9 +484,9 @@ both the UI indicator and the background process.
493484

494485
### Empty states (app)
495486

496-
Currently missing — no visual feedback when a folder is empty. Should show a centered, single-line message in
497-
`--color-text-tertiary` at `--font-size-sm`: "Empty folder". No icon, no illustration — keep it as quiet as the rest of
498-
the chrome.
487+
Centered, single-line message in `--color-text-tertiary` at `--font-size-sm`: "Empty folder". No icon, no
488+
illustration — keep it as quiet as the rest of the chrome. Shown when a directory exists and has loaded successfully but
489+
contains no entries.
499490

500491
### Notifications/toasts (app)
501492

@@ -533,7 +524,3 @@ Not shared tokens — shared _decisions_:
533524
- Fast enough to feel mechanical. Transitions serve confirmation ("I heard your click"), not decoration.
534525
- Dark mode is the assumed default, not a bolt-on. Light mode is tuned independently, not auto-inverted.
535526

536-
---
537-
538-
_This document contains "Migration note" annotations that reference the pre-migration state. Remove them once the
539-
migration plan is fully executed — this file should be a clean reference, not a changelog._

apps/desktop/.stylelintrc.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default {
2020
'declaration-property-value-disallowed-list': {
2121
'/.*/': ['/var\\(--[\\w-]+\\s*,/'],
2222
},
23-
'custom-property-pattern': '^(color|spacing|font)-.+',
23+
'custom-property-pattern': '^(color|spacing|font|radius|shadow|transition|z)-.+',
2424
'declaration-block-no-duplicate-custom-properties': true,
2525
'selector-class-pattern': null,
2626
'no-descending-specificity': null,

apps/desktop/coverage-allowlist.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"$comment": "Files listed here are exempt from coverage thresholds. Each entry should include a reason. Remove entries as you add tests.",
33
"files": {
4+
"accent-color.ts": { "reason": "Depends on Tauri invoke and listen APIs" },
45
"ui/AlertDialog.svelte": { "reason": "Simple UI modal for informational messages" },
56
"ui/dialog-registry.ts": { "reason": "Pure constant and type definition, no logic to test" },
67
"ui/ModalDialog.svelte": { "reason": "Shared modal dialog wrapper, depends on DOM APIs and Tauri commands" },

apps/desktop/src-tauri/Cargo.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@ urlencoding = "2.1.3"
8484
objc2 = { version = "0.6", features = ["std"] }
8585
objc2-foundation = { version = "0.3", features = [
8686
"NSURL", "NSString", "NSDictionary", "NSDate", "NSArray", "NSValue", "NSError",
87-
"NSFileManager",
87+
"NSFileManager", "NSNotification",
8888
] }
8989
mdns-sd = { version = "0.17", features = ["logging"] }
90-
objc2-app-kit = { version = "0.3", features = ["NSDragging", "NSDraggingItem", "NSImage"] }
90+
objc2-app-kit = { version = "0.3", features = [
91+
"NSDragging", "NSDraggingItem", "NSImage", "NSColor", "NSColorSpace",
92+
] }
9193
block2 = "0.6"
9294
smb = "0.11.1"
9395
smb-rpc = "=0.11.1"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//! macOS accent color reader.
2+
//!
3+
//! Reads the user's system accent color via `NSColor.controlAccentColor()` and
4+
//! observes `NSSystemColorsDidChangeNotification` to emit live updates.
5+
6+
use std::ptr::NonNull;
7+
8+
use log::{info, warn};
9+
use objc2_app_kit::{NSColor, NSColorSpace};
10+
use objc2_foundation::{NSNotification, NSNotificationCenter, NSString};
11+
use tauri::{AppHandle, Emitter, Runtime};
12+
13+
/// macOS default blue accent (light mode fallback).
14+
const FALLBACK_ACCENT_HEX: &str = "#007aff";
15+
16+
/// Reads the current system accent color and returns it as a hex string (for example, `#007aff`).
17+
/// Falls back to macOS default blue if the color cannot be read.
18+
fn read_accent_color() -> String {
19+
let accent = NSColor::controlAccentColor();
20+
21+
// Convert to sRGB color space so we get predictable RGB components.
22+
let srgb = NSColorSpace::sRGBColorSpace();
23+
let Some(converted) = accent.colorUsingColorSpace(&srgb) else {
24+
warn!("Could not convert accent color to sRGB, using fallback");
25+
return FALLBACK_ACCENT_HEX.to_owned();
26+
};
27+
28+
let r = converted.redComponent();
29+
let g = converted.greenComponent();
30+
let b = converted.blueComponent();
31+
32+
// Clamp to [0, 1] and convert to 8-bit hex
33+
let r8 = (r.clamp(0.0, 1.0) * 255.0).round() as u8;
34+
let g8 = (g.clamp(0.0, 1.0) * 255.0).round() as u8;
35+
let b8 = (b.clamp(0.0, 1.0) * 255.0).round() as u8;
36+
37+
format!("#{r8:02x}{g8:02x}{b8:02x}")
38+
}
39+
40+
/// Tauri command: returns the current macOS accent color as a hex string.
41+
#[tauri::command]
42+
pub fn get_accent_color() -> String {
43+
read_accent_color()
44+
}
45+
46+
/// Starts observing `NSSystemColorsDidChangeNotification`.
47+
/// Emits `accent-color-changed` with the new hex value whenever the user
48+
/// changes their accent color in System Settings.
49+
pub fn observe_accent_color_changes<R: Runtime>(app_handle: AppHandle<R>) {
50+
let initial = read_accent_color();
51+
info!("System accent color: {initial}");
52+
53+
let center = NSNotificationCenter::defaultCenter();
54+
let name = NSString::from_str("NSSystemColorsDidChangeNotification");
55+
56+
// Use block-based observer so we don't need a selector or ObjC class.
57+
// The block captures the app handle to emit Tauri events.
58+
let block = block2::RcBlock::new(move |_notification: NonNull<NSNotification>| {
59+
let hex = read_accent_color();
60+
info!("Accent color changed: {hex}");
61+
if let Err(e) = app_handle.emit("accent-color-changed", &hex) {
62+
warn!("Failed to emit accent-color-changed event: {e}");
63+
}
64+
});
65+
66+
unsafe {
67+
center.addObserverForName_object_queue_usingBlock(Some(&name), None, None, &block);
68+
}
69+
70+
// The observer is retained by NSNotificationCenter for the lifetime of the app.
71+
// We intentionally never remove it because we want updates for the entire session.
72+
}

apps/desktop/src-tauri/src/lib.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ use nusb as _;
5151
mod ignore_poison;
5252
pub use ignore_poison::IgnorePoison;
5353

54+
#[cfg(target_os = "macos")]
55+
mod accent_color;
5456
mod ai;
5557
pub mod benchmark;
5658
mod commands;
@@ -158,6 +160,10 @@ pub fn run() {
158160
#[cfg(target_os = "macos")]
159161
drag_image_detection::install(app.handle().clone());
160162

163+
// Observe system accent color changes and emit events to frontend
164+
#[cfg(target_os = "macos")]
165+
accent_color::observe_accent_color_changes(app.handle().clone());
166+
161167
// Initialize font metrics for default font (system font at 12px)
162168
font_metrics::init_font_metrics(app.handle(), "system-400-12");
163169

@@ -552,6 +558,11 @@ pub fn run() {
552558
stubs::network::list_shares_with_credentials,
553559
#[cfg(not(target_os = "macos"))]
554560
stubs::network::mount_network_share,
561+
// Accent color command (macOS reads system color, others return fallback)
562+
#[cfg(target_os = "macos")]
563+
accent_color::get_accent_color,
564+
#[cfg(not(target_os = "macos"))]
565+
stubs::accent_color::get_accent_color,
555566
// Permission commands (platform-specific)
556567
#[cfg(target_os = "macos")]
557568
permissions::check_full_disk_access,
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
//! Accent color stub for Linux/non-macOS platforms.
2+
//!
3+
//! Returns the macOS default blue as fallback since system accent color
4+
//! detection is not available outside macOS.
5+
6+
/// Returns the default accent color (macOS blue) on non-macOS platforms.
7+
#[tauri::command]
8+
pub fn get_accent_color() -> String {
9+
"#007aff".to_owned()
10+
}

apps/desktop/src-tauri/src/stubs/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//! on Linux for E2E testing purposes. They return sensible defaults that
55
//! enable the core file manager functionality to work.
66
7+
pub mod accent_color;
78
pub mod mtp;
89
pub mod network;
910
pub mod permissions;

apps/desktop/src/app.css

Lines changed: 59 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
/*
2-
* We use semantic CSS classes with CSS custom properties instead.
3-
*/
4-
5-
/* =============================================================================
6-
Design tokens - CSS Custom Properties
7-
8-
These variables serve as the single source of truth for all colors, spacing,
9-
and typography. They can be used in both scoped CSS and Tailwind classes.
10-
11-
Future: Can be modified at runtime for user theme customization.
12-
============================================================================= */
1+
/* Design tokens for the Cmdr desktop app.
2+
* See /.interface-design/system.md for the design system specification.
3+
*
4+
* These variables serve as the single source of truth for all colors, spacing,
5+
* and typography. They can be used in both scoped CSS and Tailwind classes.
6+
*
7+
* Future: Can be modified at runtime for user theme customization. */
138

149
:root {
1510
/* === Backgrounds === */
@@ -19,24 +14,19 @@
1914
--color-bg-header: #f0f0f0;
2015

2116
/* === Borders === */
22-
--color-border-primary: #ccc;
17+
--color-border-strong: #bbb;
2318
--color-border: #ddd;
24-
--color-border-secondary: #e0e0e0;
19+
--color-border-subtle: #e8e8e8;
2520

2621
/* === Text === */
27-
--color-text-primary: #000000;
22+
--color-text-primary: #1a1a1a;
2823
--color-text-secondary: #666666;
2924
--color-text-tertiary: #888888;
30-
--color-text-muted: #999999;
3125

3226
/* === Interactive States === */
33-
--color-button-hover: rgba(0, 120, 215, 0.08);
34-
--color-bg-hover: rgba(0, 120, 215, 0.08);
35-
--color-cursor-focused-bg: #88cbff;
36-
--color-cursor-focused-fg: #ffffff;
37-
--color-cursor-unfocused-bg: rgba(104, 128, 147, 0.1);
38-
--color-accent: #0078d4;
39-
--color-accent-hover: #005a9e;
27+
--color-accent: #007aff;
28+
--color-accent-hover: color-mix(in oklch, var(--color-accent), white 15%);
29+
--color-accent-subtle: color-mix(in oklch, var(--color-accent), transparent 85%);
4030

4131
/* === Semantic Colors === */
4232
--color-allow: #2e7d32;
@@ -63,14 +53,44 @@
6353
--spacing-xxs: 2px;
6454
--spacing-xs: 4px;
6555
--spacing-sm: 8px;
66-
--spacing-md: 16px;
56+
--spacing-md: 12px;
57+
--spacing-lg: 16px;
58+
--spacing-xl: 24px;
59+
--spacing-2xl: 32px;
6760

6861
/* === Typography === */
69-
--font-system: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
62+
--font-system: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
7063
--font-mono: ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Monaco, Consolas, monospace;
71-
--font-size-xs: 12px;
64+
--font-size-xs: 10px;
7265
--font-size-sm: 12px;
73-
--font-size-base: 16px;
66+
--font-size-md: 14px;
67+
--font-size-lg: 16px;
68+
--font-size-xl: 20px;
69+
70+
/* === Border radius === */
71+
--radius-sm: 4px;
72+
--radius-md: 6px;
73+
--radius-lg: 8px;
74+
--radius-full: 9999px;
75+
76+
/* === Shadows (light) === */
77+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.12);
78+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1);
79+
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.15);
80+
--shadow-focus: 0 0 0 3px var(--color-accent-subtle);
81+
82+
/* === Transitions === */
83+
--transition-fast: 100ms ease;
84+
--transition-base: 150ms ease;
85+
--transition-slow: 200ms ease;
86+
87+
/* === Z-index scale === */
88+
--z-base: 0;
89+
--z-sticky: 10;
90+
--z-dropdown: 100;
91+
--z-overlay: 200;
92+
--z-modal: 300;
93+
--z-notification: 400;
7494
}
7595

7696
/* Dark Mode - automatically applied when user's system prefers dark mode */
@@ -83,24 +103,19 @@
83103
--color-bg-header: #252525;
84104

85105
/* === Borders === */
86-
--color-border-primary: #444444;
87-
--color-border: #555555;
88-
--color-border-secondary: #3a3a3a;
106+
--color-border-strong: #555555;
107+
--color-border: #444444;
108+
--color-border-subtle: #333333;
89109

90110
/* === Text === */
91-
--color-text-primary: #ffffff;
111+
--color-text-primary: #e8e8e8;
92112
--color-text-secondary: #aaaaaa;
93113
--color-text-tertiary: #888888;
94-
--color-text-muted: #808080;
95114

96115
/* === Interactive States === */
97-
--color-button-hover: rgba(100, 180, 255, 0.12);
98-
--color-bg-hover: rgba(100, 180, 255, 0.12);
99-
--color-cursor-focused-bg: #0a50d0;
100-
--color-cursor-focused-fg: #ffffff;
101-
--color-cursor-unfocused-bg: rgba(10, 80, 208, 0.1);
102-
--color-accent: #4da3ff;
103-
--color-accent-hover: #6eb5ff;
116+
--color-accent: #0a84ff;
117+
--color-accent-hover: color-mix(in oklch, var(--color-accent), white 10%);
118+
--color-accent-subtle: color-mix(in oklch, var(--color-accent), transparent 85%);
104119

105120
/* === Semantic Colors === */
106121
--color-allow: #66bb6a;
@@ -116,6 +131,11 @@
116131
/* === Search Highlight === */
117132
--color-highlight: rgba(255, 213, 100, 0.9);
118133
--color-highlight-active: rgba(255, 150, 100, 0.9);
134+
135+
/* === Shadows (dark) === */
136+
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.3);
137+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
138+
--shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.5);
119139
}
120140
}
121141

@@ -353,7 +373,6 @@ body {
353373
-webkit-user-select: none;
354374
/* Force default cursor everywhere */
355375
cursor: default;
356-
font-size: var(--font-size-base);
357376
}
358377

359378
body {

apps/desktop/src/app.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
height: 100%;
3434
background-color: var(--bg);
3535
color: var(--text);
36-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
36+
font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
3737
}
3838

3939
#loading-screen {

0 commit comments

Comments
 (0)