Compare commits

..

5 Commits

Author SHA1 Message Date
zhom e54bc1192d chore: version bump 2026-06-24 08:30:19 +04:00
zhom 9061e4db8f refactor: improve location info generation for fresh profiles 2026-06-24 08:01:46 +04:00
zhom 0da8529e07 docs: cleanup 2026-06-24 08:01:46 +04:00
zhom b3373924e6 style: improve responsiveness 2026-06-24 08:01:46 +04:00
zhom 19e50324c4 refactor: improve profile creation api invalid 'browser' handling 2026-06-24 08:01:46 +04:00
91 changed files with 1043 additions and 1056 deletions
+10 -10
View File
@@ -49,12 +49,12 @@ pnpm format && pnpm lint && pnpm test
This runs:
- **Biome** JS/TS linting and formatting
- **Clippy + rustfmt** Rust linting and formatting
- **typos** Spellcheck (allowlist in `_typos.toml`)
- **CodeQL** Security analysis (JS, Actions, Rust) runs in CI
- **Unit tests** 330+ Rust tests
- **Integration tests** proxy, sync e2e
- **Biome**: JS/TS linting and formatting
- **Clippy + rustfmt**: Rust linting and formatting
- **typos**: Spellcheck (allowlist in `_typos.toml`)
- **CodeQL**: Security analysis (JS, Actions, Rust), runs in CI
- **Unit tests**: 330+ Rust tests
- **Integration tests**: proxy, sync e2e
### Running CodeQL locally
@@ -88,10 +88,10 @@ codeql database analyze /tmp/codeql-rust --format=sarifv2.1.0 --output=/tmp/rust
## Architecture
- **Frontend**: Next.js (React) `src/`
- **Backend**: Tauri (Rust) `src-tauri/src/`
- **Proxy Worker**: Detached process for proxy tunneling `src-tauri/src/bin/proxy_server.rs`
- **Sync**: Cloud sync via S3-compatible storage `src-tauri/src/sync/`, `donut-sync/`
- **Frontend**: Next.js (React), `src/`
- **Backend**: Tauri (Rust), `src-tauri/src/`
- **Proxy Worker**: Detached process for proxy tunneling, `src-tauri/src/bin/proxy_server.rs`
- **Sync**: Cloud sync via S3-compatible storage, `src-tauri/src/sync/`, `donut-sync/`
- **Browsers**: Camoufox (Firefox-based) and Wayfern (Chromium-based)
## Getting Help
+13 -13
View File
@@ -25,19 +25,19 @@
## Features
- **Unlimited browser profiles** each fully isolated with its own fingerprint, cookies, extensions, and data
- **Anti-detect Chromium engine** powered by [Wayfern](https://wayfern.com), which is privacy-focused Chromium fork that comes with advanced fingerprint spoofing which naturally hides information in a way that is not detected by Cloudflare, reCaptcha v3, and other browser fingerprinting and anti-bot services.
- **Unlimited browser profiles**: each fully isolated with its own fingerprint, cookies, extensions, and data
- **Anti-detect Chromium engine**: powered by [Wayfern](https://wayfern.com), which is privacy-focused Chromium fork that comes with advanced fingerprint spoofing which naturally hides information in a way that is not detected by Cloudflare, reCaptcha v3, and other browser fingerprinting and anti-bot services.
- **DNS AdBlocker** - block ads, trackers, and other unwanted content with per-profile DNS blocking
- **Proxy support** HTTP, HTTPS, SOCKS4, SOCKS5 per profile, with dynamic proxy URLs
- **VPN support** WireGuard configs per profile
- **Local API & MCP** REST API and [Model Context Protocol](https://modelcontextprotocol.io) server for integration with Claude, automation tools, and custom workflows
- **Profile groups** organize profiles and apply bulk settings
- **Import profiles** migrate from Chrome, Firefox, Edge, Brave, or other Chromium browsers
- **Cookie & extension management** import/export cookies, manage extensions per profile
- **Default browser** set Donut as your default browser and choose which profile opens each link
- **Cloud sync** sync profiles, proxies, and groups across devices (self-hostable)
- **E2E encryption** optional end-to-end encrypted sync with a password only you know
- **Zero telemetry** no tracking or device fingerprinting
- **Proxy support**: HTTP, HTTPS, SOCKS4, SOCKS5 per profile, with dynamic proxy URLs
- **VPN support**: WireGuard configs per profile
- **Local API & MCP**: REST API and [Model Context Protocol](https://modelcontextprotocol.io) server for integration with Claude, automation tools, and custom workflows
- **Profile groups**: organize profiles and apply bulk settings
- **Import profiles**: migrate from Chrome, Firefox, Edge, Brave, or other Chromium browsers
- **Cookie & extension management**: import/export cookies, manage extensions per profile
- **Default browser**: set Donut as your default browser and choose which profile opens each link
- **Cloud sync**: sync profiles, proxies, and groups across devices (self-hostable)
- **E2E encryption**: optional end-to-end encrypted sync with a password only you know
- **Zero telemetry**: no tracking or device fingerprinting
## Install
@@ -94,7 +94,7 @@ nix run github:zhom/donutbrowser#release-start
## Self-Hosting Sync
Donut Browser supports syncing profiles, proxies, and groups across devices via a self-hosted sync server. See the [Self-Hosting Guide](docs/self-hosting-donut-sync.md) for Docker-based setup instructions.
Donut Browser supports syncing profiles, proxies, and groups across devices via a self-hosted sync server, which makes sync completely free. See the [Self-Hosting Donut Sync guide](https://donutbrowser.com/docs/self-hosting) for Docker-based setup instructions.
## Development
-177
View File
@@ -1,177 +0,0 @@
# Self-Hosting Donut Sync
Donut Sync is the synchronization server for Donut Browser. It allows you to sync your profiles, proxies, and groups across multiple devices. This guide covers how to self-host it using Docker.
## Prerequisites
- [Docker](https://docs.docker.com/get-docker/) and [Docker Compose](https://docs.docker.com/compose/install/)
- An S3-compatible object storage (MinIO included by default, or use AWS S3, Cloudflare R2, etc.)
## Quick Start
### 1. Create a `docker-compose.yml`
```yaml
services:
donut-sync:
image: donutbrowser/donut-sync:latest
ports:
- "3929:3929"
environment:
- SYNC_TOKEN=your-secret-token-here
- PORT=3929
- S3_ENDPOINT=http://minio:9000
- S3_REGION=us-east-1
- S3_ACCESS_KEY_ID=minioadmin
- S3_SECRET_ACCESS_KEY=minioadmin
- S3_BUCKET=donut-sync
- S3_FORCE_PATH_STYLE=true
depends_on:
minio:
condition: service_healthy
minio:
image: minio/minio:latest
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 5s
retries: 5
volumes:
- minio_data:/data
volumes:
minio_data:
```
### 2. Start the services
```bash
docker compose up -d
```
### 3. Verify the server is running
```bash
# Health check
curl http://localhost:3929/health
# Expected: {"status":"ok"}
# Readiness check (verifies S3 connectivity)
curl http://localhost:3929/readyz
# Expected: {"status":"ready","s3":true}
```
## Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| `SYNC_TOKEN` | Yes | - | Bearer token used to authenticate requests from Donut Browser clients |
| `PORT` | No | `3929` | Port the sync server listens on |
| `S3_ENDPOINT` | No | - | S3-compatible endpoint URL (e.g., `http://minio:9000` or `https://s3.amazonaws.com`) |
| `S3_REGION` | No | `us-east-1` | S3 region |
| `S3_ACCESS_KEY_ID` | Yes | - | S3 access key |
| `S3_SECRET_ACCESS_KEY` | Yes | - | S3 secret key |
| `S3_BUCKET` | No | `donut-sync` | S3 bucket name for storing sync data |
| `S3_FORCE_PATH_STYLE` | No | `false` | Set to `true` for MinIO and other S3-compatible services that use path-style URLs |
## Using External S3 Storage
Instead of running MinIO, you can use any S3-compatible storage service. Remove the `minio` service from `docker-compose.yml` and update the environment variables:
### AWS S3
```yaml
services:
donut-sync:
image: donutbrowser/donut-sync:latest
ports:
- "3929:3929"
environment:
- SYNC_TOKEN=your-secret-token-here
- S3_REGION=us-east-1
- S3_ACCESS_KEY_ID=your-aws-access-key
- S3_SECRET_ACCESS_KEY=your-aws-secret-key
- S3_BUCKET=your-bucket-name
```
### Cloudflare R2
```yaml
services:
donut-sync:
image: donutbrowser/donut-sync:latest
ports:
- "3929:3929"
environment:
- SYNC_TOKEN=your-secret-token-here
- S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
- S3_REGION=auto
- S3_ACCESS_KEY_ID=your-r2-access-key
- S3_SECRET_ACCESS_KEY=your-r2-secret-key
- S3_BUCKET=your-bucket-name
- S3_FORCE_PATH_STYLE=true
```
### Other S3-Compatible Services
Any service that implements the S3 API (e.g., Backblaze B2, DigitalOcean Spaces, Wasabi) can be used. Set `S3_ENDPOINT` to the service's endpoint URL and `S3_FORCE_PATH_STYLE=true` if required by the provider.
## Configuring the Donut Browser Client
1. Open Donut Browser
2. Click the sync icon in the header to open the Sync Configuration dialog
3. Enter the **Server URL** (e.g., `http://your-server:3929`)
4. Enter the **Sync Token** (the value you set for `SYNC_TOKEN`)
5. Click **Save**
Once configured, you can enable sync on individual profiles, proxies, and groups.
## Health Check Endpoints
| Endpoint | Description |
|---|---|
| `GET /health` | Basic health check. Returns `{"status":"ok"}` if the server is running. |
| `GET /readyz` | Readiness check. Verifies S3 connectivity. Returns `{"status":"ready","s3":true}` or HTTP 503 if S3 is unreachable. |
## Security Considerations
- **Use a strong `SYNC_TOKEN`**: Generate a random token (e.g., `openssl rand -hex 32`) and keep it secret.
- **HTTPS**: In production, place a reverse proxy (e.g., Nginx, Caddy, Traefik) in front of Donut Sync to terminate TLS. The sync token is sent as a Bearer token in the `Authorization` header and should not be transmitted over plain HTTP.
- **Network isolation**: If running on a VPS, consider restricting access to the sync port using firewall rules or binding only to localhost behind a reverse proxy.
- **S3 credentials**: Use dedicated IAM credentials with minimal permissions (read/write to the sync bucket only).
### Example: Caddy Reverse Proxy
```
sync.yourdomain.com {
reverse_proxy localhost:3929
}
```
### Example: Nginx Reverse Proxy
```nginx
server {
listen 443 ssl;
server_name sync.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3929;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.27.0",
"version": "0.27.1",
"type": "module",
"scripts": {
"dev": "next dev --turbopack -p 12341",
+3 -18
View File
@@ -871,15 +871,6 @@ dependencies = [
"bzip2-sys",
]
[[package]]
name = "bzip2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
dependencies = [
"libbz2-rs-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
@@ -1792,7 +1783,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.27.0"
version = "0.27.1"
dependencies = [
"aes 0.9.1",
"aes-gcm",
@@ -1803,7 +1794,7 @@ dependencies = [
"base64 0.22.1",
"blake3",
"boringtun",
"bzip2 0.6.1",
"bzip2",
"cbc",
"chrono",
"chrono-tz",
@@ -3603,12 +3594,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "libbz2-rs-sys"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
[[package]]
name = "libc"
version = "0.2.186"
@@ -9074,7 +9059,7 @@ checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
dependencies = [
"aes 0.8.4",
"arbitrary",
"bzip2 0.5.2",
"bzip2",
"constant_time_eq 0.3.1",
"crc32fast",
"crossbeam-utils",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.27.0"
version = "0.27.1"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
+36 -7
View File
@@ -57,6 +57,9 @@ pub struct ApiProfileResponse {
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct CreateProfileRequest {
pub name: String,
/// Browser engine. Must be `"wayfern"` (anti-detect Chromium) or `"camoufox"`
/// (anti-detect Firefox). Any other value (e.g. `"chromium"`) is rejected with
/// 400.
pub browser: String,
/// Optional. Omit (or pass `"latest"`) to use the newest already-downloaded
/// version of the chosen browser. A concrete version must already be
@@ -815,7 +818,7 @@ async fn get_profile(
async fn create_profile(
State(state): State<ApiServerState>,
Json(request): Json<CreateProfileRequest>,
) -> Result<Json<ApiProfileResponse>, StatusCode> {
) -> Result<Json<ApiProfileResponse>, (StatusCode, String)> {
let profile_manager = ProfileManager::instance();
// Only Wayfern and Camoufox profiles are launchable; the rest of the system
@@ -824,7 +827,13 @@ async fn create_profile(
// unrecognized browser, then crashes with a 500 on /run. Mirrors the MCP
// create_profile validation.
if request.browser != "wayfern" && request.browser != "camoufox" {
return Err(StatusCode::BAD_REQUEST);
return Err((
StatusCode::BAD_REQUEST,
format!(
"Invalid browser \"{}\". Must be \"wayfern\" (anti-detect Chromium) or \"camoufox\" (anti-detect Firefox).",
request.browser
),
));
}
// Resolve the version. Omitted, empty, or "latest" means "newest version
@@ -841,7 +850,15 @@ async fn create_profile(
versions.sort_by(|a, b| crate::api_client::compare_versions(b, a));
match versions.into_iter().next() {
Some(v) => v,
None => return Err(StatusCode::BAD_REQUEST),
None => {
return Err((
StatusCode::BAD_REQUEST,
format!(
"No downloaded version of \"{}\" is available. Download the browser in Donut Browser first — this endpoint does not download browsers.",
request.browser
),
));
}
}
}
};
@@ -866,9 +883,15 @@ async fn create_profile(
crate::validate_profile_network(request.proxy_id.as_deref(), request.vpn_id.as_deref()).await
{
return Err(if err.contains("PROXY_PAYMENT_REQUIRED") {
StatusCode::PAYMENT_REQUIRED
(
StatusCode::PAYMENT_REQUIRED,
"The selected proxy requires an active subscription.".to_string(),
)
} else {
StatusCode::BAD_REQUEST
(
StatusCode::BAD_REQUEST,
format!("Profile network validation failed: {err}"),
)
});
}
@@ -898,7 +921,10 @@ async fn create_profile(
.update_profile_tags(&state.app_handle, &profile.name, tags.clone())
.is_err()
{
return Err(StatusCode::INTERNAL_SERVER_ERROR);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
"Profile created but failed to apply tags.".to_string(),
));
}
profile.tags = tags.clone();
}
@@ -930,7 +956,10 @@ async fn create_profile(
},
}))
}
Err(_) => Err(StatusCode::BAD_REQUEST),
Err(e) => Err((
StatusCode::BAD_REQUEST,
format!("Failed to create profile: {e}"),
)),
}
}
+64
View File
@@ -597,6 +597,14 @@ impl BrowserRunner {
if wayfern_config.os.is_some() {
updated_wayfern_config.os = wayfern_config.os.clone();
}
// The fresh fingerprint's location matches the current routing; record
// its signature so launches keep it in sync with the non-randomize path.
updated_wayfern_config.geo_proxy_signature =
Some(crate::wayfern_manager::WayfernManager::geo_signature(
upstream_proxy.as_ref(),
profile.vpn_id.as_deref(),
wayfern_config.geoip.as_ref(),
));
updated_profile.wayfern_config = Some(updated_wayfern_config.clone());
log::info!(
@@ -604,6 +612,62 @@ impl BrowserRunner {
profile.name,
updated_wayfern_config.fingerprint.as_ref().map(|f| f.len()).unwrap_or(0)
);
} else {
// Safety net: the stored fingerprint's timezone and geolocation were
// computed for whatever proxy was set when the fingerprint was
// generated. If the profile's proxy or VPN has changed since (the
// common case being a user who forgot to set a proxy at creation and
// added one afterwards), that location data is stale and the user would
// see the wrong timezone on first launch. When the routing signature no
// longer matches, refresh just the location fields of the stored
// fingerprint through the current proxy. Wayfern only; the randomize
// path above already regenerates the whole fingerprint each launch.
let current_geo_sig = crate::wayfern_manager::WayfernManager::geo_signature(
upstream_proxy.as_ref(),
profile.vpn_id.as_deref(),
wayfern_config.geoip.as_ref(),
);
let geo_enabled = !matches!(
wayfern_config.geoip.as_ref(),
Some(serde_json::Value::Bool(false))
);
if geo_enabled
&& wayfern_config.geo_proxy_signature.as_deref() != Some(current_geo_sig.as_str())
{
if let Some(stored_fp) = wayfern_config.fingerprint.clone() {
log::info!(
"Routing changed for Wayfern profile {} since its fingerprint was generated (was {:?}, now {}); refreshing timezone and geolocation",
profile.name,
wayfern_config.geo_proxy_signature,
current_geo_sig
);
match crate::wayfern_manager::WayfernManager::refresh_fingerprint_geolocation(
&stored_fp,
wayfern_config.proxy.as_deref(),
wayfern_config.geoip.as_ref(),
)
.await
{
Some(refreshed) => {
// Use the refreshed fingerprint for this launch...
wayfern_config.fingerprint = Some(refreshed.clone());
wayfern_config.geo_proxy_signature = Some(current_geo_sig.clone());
// ...and persist it so the corrected location sticks and we do
// not refresh again on the next launch with the same proxy.
let mut cfg = updated_profile.wayfern_config.clone().unwrap_or_default();
cfg.fingerprint = Some(refreshed);
cfg.geo_proxy_signature = Some(current_geo_sig);
updated_profile.wayfern_config = Some(cfg);
}
None => {
log::warn!(
"Could not refresh geolocation for Wayfern profile {} (proxy unreachable?); launching with existing location and will retry next launch",
profile.name
);
}
}
}
}
}
// Create ephemeral dir for ephemeral or password-protected profiles
+13
View File
@@ -326,6 +326,19 @@ impl ProfileManager {
log::info!("Using provided fingerprint for Wayfern profile: {name}");
}
// Record which proxy/geoip the fingerprint's location data was computed
// for. On launch this is compared against the profile's current routing
// so a proxy that was changed after creation triggers a location refresh
// instead of showing a stale timezone.
config.geo_proxy_signature = Some(crate::wayfern_manager::WayfernManager::geo_signature(
proxy_id
.as_ref()
.and_then(|id| PROXY_MANAGER.get_proxy_settings_by_id(id))
.as_ref(),
None,
config.geoip.as_ref(),
));
// Clear the proxy from config after fingerprint generation
config.proxy = None;
+138 -64
View File
@@ -39,6 +39,12 @@ pub struct WayfernConfig {
pub block_webgl: Option<bool>,
#[serde(default, skip_serializing)]
pub proxy: Option<String>,
/// Stable signature of the proxy/VPN/geoip the fingerprint's location data
/// (timezone, latitude/longitude, language) was last computed for. Compared
/// on launch to detect that the routing changed since creation, so the
/// location can be refreshed instead of showing stale data.
#[serde(default)]
pub geo_proxy_signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -263,6 +269,130 @@ impl WayfernManager {
Err("No response received from CDP".into())
}
/// Stable signature describing what determines this profile's geolocation
/// (timezone, latitude/longitude, language): the geoip mode first, then the
/// VPN, the proxy, or a direct connection. Compared across creation and
/// launch to detect a change. The VPN case keys off `vpn_id` rather than the
/// per-launch local port, and the proxy case off type/host/port/username so
/// that editing the proxy is also caught.
pub fn geo_signature(
proxy: Option<&crate::browser::ProxySettings>,
vpn_id: Option<&str>,
geoip: Option<&serde_json::Value>,
) -> String {
match geoip {
Some(serde_json::Value::Bool(false)) => "off".to_string(),
Some(serde_json::Value::String(ip)) if !ip.is_empty() => format!("ip:{ip}"),
_ => {
if let Some(id) = vpn_id {
format!("vpn:{id}")
} else if let Some(p) = proxy {
format!(
"proxy:{}://{}@{}:{}",
p.proxy_type.to_lowercase(),
p.username.as_deref().unwrap_or(""),
p.host,
p.port
)
} else {
"direct".to_string()
}
}
}
}
/// Apply timezone/geolocation fields to a fingerprint object from the proxy's
/// exit IP (or a fixed geoip IP). Mutates `fingerprint` in place. Returns true
/// if fresh geolocation was fetched and applied, false if geolocation is
/// disabled or could not be resolved (in which case only safe defaults are
/// filled in). Shared by fingerprint generation and the launch-time refresh
/// so both produce identical location data.
async fn apply_geolocation(
fingerprint: &mut serde_json::Value,
proxy: Option<&str>,
geoip: Option<&serde_json::Value>,
) -> bool {
// Default to auto-detect; only an explicit `false` disables geolocation.
let should_geolocate = !matches!(geoip, Some(serde_json::Value::Bool(false)));
if !should_geolocate {
return false;
}
let geo_result = async {
let ip = match geoip {
Some(serde_json::Value::String(ip_str)) => ip_str.clone(),
_ => crate::ip_utils::fetch_public_ip(proxy)
.await
.map_err(|e| format!("Failed to fetch public IP: {e}"))?,
};
crate::camoufox::geolocation::get_geolocation(&ip)
.map_err(|e| format!("Failed to get geolocation for IP {ip}: {e}"))
}
.await;
match geo_result {
Ok(geo) => {
if let Some(obj) = fingerprint.as_object_mut() {
obj.insert("timezone".to_string(), json!(geo.timezone));
// Calculate timezone offset from IANA timezone name
if let Ok(tz) = geo.timezone.parse::<chrono_tz::Tz>() {
use chrono::Offset;
let now = chrono::Utc::now().with_timezone(&tz);
let offset_seconds = now.offset().fix().local_minus_utc();
let offset_minutes = -(offset_seconds / 60);
obj.insert("timezoneOffset".to_string(), json!(offset_minutes));
}
obj.insert("latitude".to_string(), json!(geo.latitude));
obj.insert("longitude".to_string(), json!(geo.longitude));
let locale_str = geo.locale.as_string();
obj.insert("language".to_string(), json!(&locale_str));
obj.insert(
"languages".to_string(),
json!([&locale_str, &geo.locale.language]),
);
}
log::info!(
"Applied geolocation to Wayfern fingerprint: {} ({})",
geo.locale.as_string(),
geo.timezone
);
true
}
Err(e) => {
log::warn!("Geolocation failed, using defaults: {e}");
if let Some(obj) = fingerprint.as_object_mut() {
if !obj.contains_key("timezone") {
obj.insert("timezone".to_string(), json!("America/New_York"));
}
if !obj.contains_key("timezoneOffset") {
obj.insert("timezoneOffset".to_string(), json!(300));
}
}
false
}
}
}
/// Refresh ONLY the location fields (timezone, offset, latitude/longitude,
/// language) of an already-generated fingerprint to match the current proxy,
/// leaving every other fingerprint field untouched. `proxy` is the local
/// proxy URL the browser will use. Returns the updated fingerprint JSON on
/// success, or None if geolocation is disabled or could not be resolved, in
/// which case the caller keeps the existing fingerprint and retries on the
/// next launch.
pub async fn refresh_fingerprint_geolocation(
fingerprint_json: &str,
proxy: Option<&str>,
geoip: Option<&serde_json::Value>,
) -> Option<String> {
let mut fp: serde_json::Value = serde_json::from_str(fingerprint_json).ok()?;
if Self::apply_geolocation(&mut fp, proxy, geoip).await {
serde_json::to_string(&fp).ok()
} else {
None
}
}
pub async fn generate_fingerprint_config(
&self,
_app_handle: &AppHandle,
@@ -424,70 +554,14 @@ impl WayfernManager {
// Normalize the fingerprint: convert JSON string fields to proper types
let mut normalized = Self::normalize_fingerprint(fp);
// Apply geolocation based on proxy IP or geoip config
let geoip_option = config.geoip.as_ref();
let should_geolocate = match geoip_option {
Some(serde_json::Value::Bool(false)) => false,
_ => true, // Default to auto-detect
};
if should_geolocate {
let geo_result = async {
let ip = match geoip_option {
Some(serde_json::Value::String(ip_str)) => ip_str.clone(),
_ => {
// Auto-detect IP, optionally through proxy
crate::ip_utils::fetch_public_ip(config.proxy.as_deref())
.await
.map_err(|e| format!("Failed to fetch public IP: {e}"))?
}
};
crate::camoufox::geolocation::get_geolocation(&ip)
.map_err(|e| format!("Failed to get geolocation for IP {ip}: {e}"))
}
.await;
match geo_result {
Ok(geo) => {
if let Some(obj) = normalized.as_object_mut() {
obj.insert("timezone".to_string(), json!(geo.timezone));
// Calculate timezone offset from IANA timezone name
if let Ok(tz) = geo.timezone.parse::<chrono_tz::Tz>() {
use chrono::Offset;
let now = chrono::Utc::now().with_timezone(&tz);
let offset_seconds = now.offset().fix().local_minus_utc();
let offset_minutes = -(offset_seconds / 60);
obj.insert("timezoneOffset".to_string(), json!(offset_minutes));
}
obj.insert("latitude".to_string(), json!(geo.latitude));
obj.insert("longitude".to_string(), json!(geo.longitude));
let locale_str = geo.locale.as_string();
obj.insert("language".to_string(), json!(&locale_str));
obj.insert(
"languages".to_string(),
json!([&locale_str, &geo.locale.language]),
);
}
log::info!(
"Applied geolocation to Wayfern fingerprint: {} ({})",
geo.locale.as_string(),
geo.timezone
);
}
Err(e) => {
log::warn!("Geolocation failed, using defaults: {e}");
if let Some(obj) = normalized.as_object_mut() {
if !obj.contains_key("timezone") {
obj.insert("timezone".to_string(), json!("America/New_York"));
}
if !obj.contains_key("timezoneOffset") {
obj.insert("timezoneOffset".to_string(), json!(300));
}
}
}
}
}
// Apply timezone/geolocation for the proxy this fingerprint is being
// generated against. Shared with the launch-time location refresh.
Self::apply_geolocation(
&mut normalized,
config.proxy.as_deref(),
config.geoip.as_ref(),
)
.await;
normalized
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.27.0",
"version": "0.27.1",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
+4 -4
View File
@@ -1637,7 +1637,7 @@ export default function Home() {
: t(`pageTitle.${currentPage}`);
return (
<div className="flex flex-col h-screen bg-background font-(family-name:--font-geist-sans)">
<div className="flex h-dvh flex-col bg-background font-(family-name:--font-geist-sans)">
<CloseConfirmDialog />
<CamoufoxDeprecationDialog profiles={profiles} />
<HomeHeader
@@ -1650,11 +1650,11 @@ export default function Home() {
onGroupSelect={handleSelectGroup}
pageTitle={subPageTitle}
/>
<div className="flex flex-1 min-h-0">
<div className="flex min-h-0 flex-1">
<RailNav currentPage={currentPage} onNavigate={handleRailNavigate} />
<main className="flex-1 min-w-0 flex flex-col overflow-hidden">
<main className="flex min-w-0 flex-1 flex-col overflow-hidden">
{currentPage === "profiles" && (
<div className="px-3 pt-2.5 flex flex-col flex-1 min-h-0">
<div className="flex min-h-0 flex-1 flex-col px-3 pt-2.5">
{isLoading && groupsData.length === 0 ? null : null}
<ProfilesDataTable
profiles={filteredProfiles}
+24 -24
View File
@@ -198,11 +198,11 @@ export function AccountPage({
return (
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
<DialogContent className="max-w-2xl max-h-[calc(100vh-4rem)] flex flex-col">
<DialogContent className="flex max-h-[calc(100vh-4rem)] max-w-2xl flex-col">
<div
className={cn(
"flex flex-col gap-4 p-4 overflow-y-auto flex-1 min-h-0",
subPage && "w-full max-w-2xl mx-auto",
"flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto p-4",
subPage && "mx-auto w-full max-w-2xl",
)}
>
<AnimatedTabs defaultValue="account">
@@ -226,16 +226,16 @@ export function AccountPage({
<AnimatedTabsContent value="account" className="mt-4">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<div className="grid place-items-center size-12 rounded-full bg-accent text-foreground shrink-0">
<div className="grid size-12 shrink-0 place-items-center rounded-full bg-accent text-foreground">
<LuUser className="size-6" />
</div>
<div className="min-w-0 flex-1">
{isLoggedIn && user ? (
<>
<h2 className="text-base font-semibold truncate">
<h2 className="truncate text-base font-semibold">
{user.email}
</h2>
<p className="text-xs text-muted-foreground mt-0.5">
<p className="mt-0.5 text-xs text-muted-foreground">
{t("account.plan", {
plan: user.plan,
period: user.planPeriod ?? "—",
@@ -247,7 +247,7 @@ export function AccountPage({
<h2 className="text-base font-semibold">
{t("account.signedOut")}
</h2>
<p className="text-xs text-muted-foreground mt-0.5">
<p className="mt-0.5 text-xs text-muted-foreground">
{t("account.signedOutDescription")}
</p>
</>
@@ -257,39 +257,39 @@ export function AccountPage({
{isLoggedIn && user && (
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("account.fields.plan")}
</p>
<p className="mt-0.5 font-medium uppercase">
{user.plan}
</p>
</div>
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("account.fields.status")}
</p>
<p className="mt-0.5">{user.subscriptionStatus ?? "—"}</p>
</div>
{user.teamRole && (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("account.fields.teamRole")}
</p>
<p className="mt-0.5">{user.teamRole}</p>
</div>
)}
{user.planPeriod && (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("account.fields.period")}
</p>
<p className="mt-0.5">{user.planPeriod}</p>
</div>
)}
{typeof user.deviceOrdinal === "number" && (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("account.fields.device")}
</p>
<p className="mt-0.5">
@@ -321,7 +321,7 @@ export function AccountPage({
</p>
)}
<div className="flex flex-wrap gap-2 mt-2">
<div className="mt-2 flex flex-wrap gap-2">
{isLoggedIn ? (
<>
<Button
@@ -331,7 +331,7 @@ export function AccountPage({
void handleRefresh();
}}
disabled={isRefreshing}
className="h-8 text-xs gap-1.5"
className="h-8 gap-1.5 text-xs"
>
<LuRefreshCw className="size-3" />
{t("account.refresh")}
@@ -344,7 +344,7 @@ export function AccountPage({
onClick={() => {
void handleLogout();
}}
className="h-8 text-xs gap-1.5"
className="h-8 gap-1.5 text-xs"
>
<LuLogOut className="size-3" />
{t("account.logout")}
@@ -354,7 +354,7 @@ export function AccountPage({
<Button
size="sm"
onClick={onOpenSignIn}
className="h-8 text-xs gap-1.5"
className="h-8 gap-1.5 text-xs"
>
<LuCloud className="size-3" />
{t("account.signIn")}
@@ -380,7 +380,7 @@ export function AccountPage({
<p className="text-sm font-medium">
{t("account.selfHosted.title")}
</p>
<p className="text-xs text-muted-foreground mt-0.5">
<p className="mt-0.5 text-xs text-muted-foreground">
{t("account.selfHosted.description")}
</p>
</div>
@@ -431,7 +431,7 @@ export function AccountPage({
? t("common.aria.hideToken")
: t("common.aria.showToken")
}
className="absolute right-2 top-1/2 -translate-y-1/2 p-1 text-muted-foreground hover:text-foreground"
className="absolute top-1/2 right-2 -translate-y-1/2 p-1 text-muted-foreground hover:text-foreground"
>
{showToken ? (
<LuEyeOff className="size-3.5" />
@@ -449,7 +449,7 @@ export function AccountPage({
{connectionStatus === "connected" && (
<Badge
variant="default"
className="text-success-foreground bg-success"
className="bg-success text-success-foreground"
>
{t("sync.status.connected")}
</Badge>
+9 -9
View File
@@ -35,13 +35,13 @@ export function AppUpdateToast({
};
return (
<div className="flex items-start p-4 w-full max-w-md rounded-lg border shadow-lg bg-card border-border text-card-foreground">
<div className="mr-3 mt-0.5">
<LuCheckCheck className="shrink-0 size-5" />
<div className="flex w-full max-w-md items-start rounded-lg border border-border bg-card p-4 text-card-foreground shadow-lg">
<div className="mt-0.5 mr-3">
<LuCheckCheck className="size-5 shrink-0" />
</div>
<div className="flex-1 min-w-0">
<div className="flex gap-2 justify-between items-start">
<div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2">
<div className="flex flex-col gap-1">
<span className="text-sm font-semibold text-foreground">
{updateReady
@@ -59,18 +59,18 @@ export function AppUpdateToast({
variant="ghost"
size="sm"
onClick={onDismiss}
className="p-0 size-6 shrink-0"
className="size-6 shrink-0 p-0"
>
<FaTimes className="size-3" />
</Button>
</div>
<div className="flex gap-2 items-center mt-3">
<div className="mt-3 flex items-center gap-2">
{updateReady ? (
<RippleButton
onClick={() => void handleRestartClick()}
size="sm"
className="flex gap-2 items-center text-xs"
className="flex items-center gap-2 text-xs"
>
<LuCheckCheck className="size-3" />
{t("appUpdate.toast.restartNow")}
@@ -81,7 +81,7 @@ export function AppUpdateToast({
<RippleButton
onClick={handleViewRelease}
size="sm"
className="flex gap-2 items-center text-xs"
className="flex items-center gap-2 text-xs"
>
<FaExternalLinkAlt className="size-3" />
{t("appUpdate.toast.viewRelease")}
+3 -3
View File
@@ -63,11 +63,11 @@ export function BandwidthMiniChart({
type="button"
onClick={onClick}
className={cn(
"relative flex items-center gap-1.5 px-2 rounded cursor-pointer hover:bg-accent/50 transition-colors w-full min-w-0 border-none bg-transparent",
"relative flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded border-none bg-transparent px-2 transition-colors hover:bg-accent/50",
className,
)}
>
<div className="flex-1 min-w-0 h-3 pointer-events-none">
<div className="pointer-events-none h-3 min-w-0 flex-1">
<ResponsiveContainer
width="100%"
height="100%"
@@ -111,7 +111,7 @@ export function BandwidthMiniChart({
</AreaChart>
</ResponsiveContainer>
</div>
<span className="text-xs text-muted-foreground whitespace-nowrap shrink-0 min-w-[60px] text-right">
<span className="min-w-[60px] shrink-0 text-right text-xs whitespace-nowrap text-muted-foreground">
{formatBytes(currentBandwidth)}
</span>
</button>
+3 -3
View File
@@ -149,7 +149,7 @@ export function CamoufoxConfigDialog({
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl h-[min(85vh,52rem)] flex flex-col">
<DialogContent className="flex h-[min(85vh,52rem)] max-w-3xl flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>
{isRunning
@@ -164,7 +164,7 @@ export function CamoufoxConfigDialog({
</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 min-h-0">
<ScrollArea className="min-h-0 flex-1">
<div className="py-4">
{profile.browser === "wayfern" ? (
<WayfernConfigForm
@@ -193,7 +193,7 @@ export function CamoufoxConfigDialog({
</div>
</ScrollArea>
<DialogFooter className="shrink-0 pt-4 border-t">
<DialogFooter className="shrink-0 border-t pt-4">
<RippleButton variant="outline" onClick={handleClose}>
{isRunning ? t("common.buttons.close") : t("common.buttons.cancel")}
</RippleButton>
+1 -1
View File
@@ -74,7 +74,7 @@ function Tokens({ tokens }: { tokens: string[] }) {
{tokens.map((tok, i) => (
<kbd
key={i}
className="inline-flex items-center justify-center min-w-[1.25rem] h-5 px-1 rounded border border-border bg-muted text-[10px] font-medium text-muted-foreground"
className="inline-flex h-5 min-w-5 items-center justify-center rounded border border-border bg-muted px-1 text-[10px] font-medium text-muted-foreground"
>
{tok}
</kbd>
+16 -16
View File
@@ -332,7 +332,7 @@ export function CookieCopyDialog({
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-[min(48rem,calc(100%-4rem))] max-h-[80vh] flex flex-col">
<DialogContent className="flex max-h-[80vh] max-w-[min(48rem,calc(100%-4rem))] flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<LuCookie className="size-5" />
@@ -349,7 +349,7 @@ export function CookieCopyDialog({
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto space-y-4">
<div className="flex-1 space-y-4 overflow-y-auto">
<div className="space-y-2">
<Label>{t("cookies.copy.sourceProfile")}</Label>
<Select
@@ -393,7 +393,7 @@ export function CookieCopyDialog({
count: targetProfiles.length,
})}
</Label>
<div className="p-2 bg-muted rounded-md max-h-20 overflow-y-auto">
<div className="max-h-20 overflow-y-auto rounded-md bg-muted p-2">
{targetProfiles.length === 0 ? (
<p className="text-sm text-muted-foreground">
{sourceProfileId
@@ -405,7 +405,7 @@ export function CookieCopyDialog({
{targetProfiles.map((p) => (
<span
key={p.id}
className="inline-flex items-center gap-1 px-2 py-0.5 bg-background rounded text-sm"
className="inline-flex items-center gap-1 rounded bg-background px-2 py-0.5 text-sm"
>
{p.name}
{runningProfiles.has(p.id) && (
@@ -437,7 +437,7 @@ export function CookieCopyDialog({
</div>
<div className="relative">
<LuSearch className="absolute left-2 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<LuSearch className="absolute top-1/2 left-2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder={t("cookies.copy.searchPlaceholder")}
value={searchQuery}
@@ -449,11 +449,11 @@ export function CookieCopyDialog({
</div>
{isLoadingCookies ? (
<div className="flex items-center justify-center h-40">
<div className="animate-spin size-6 border-2 border-primary border-t-transparent rounded-full" />
<div className="flex h-40 items-center justify-center">
<div className="size-6 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
) : error ? (
<div className="p-4 text-center text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-4 text-center text-destructive">
{error}
</div>
) : filteredDomains.length === 0 ? (
@@ -463,8 +463,8 @@ export function CookieCopyDialog({
: t("cookies.copy.noFound")}
</div>
) : (
<ScrollArea className="h-[clamp(150px,35vh,450px)] border rounded-md">
<div className="p-2 space-y-1">
<ScrollArea className="h-[clamp(150px,35vh,450px)] rounded-md border">
<div className="space-y-1 p-2">
{filteredDomains.map((domain) => (
<DomainRow
key={domain.domain}
@@ -549,7 +549,7 @@ function DomainRow({
return (
<div>
<div className="flex items-center gap-2 p-2 hover:bg-accent/50 rounded">
<div className="flex items-center gap-2 rounded p-2 hover:bg-accent/50">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => {
@@ -559,7 +559,7 @@ function DomainRow({
/>
<button
type="button"
className="flex items-center gap-1 flex-1 min-w-0 text-left bg-transparent border-none cursor-pointer"
className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 border-none bg-transparent text-left"
onClick={() => {
onToggleExpand(domain.domain);
}}
@@ -569,21 +569,21 @@ function DomainRow({
) : (
<LuChevronRight className="size-4" />
)}
<span className="font-medium truncate">{domain.domain}</span>
<span className="text-xs text-muted-foreground shrink-0">
<span className="truncate font-medium">{domain.domain}</span>
<span className="shrink-0 text-xs text-muted-foreground">
({domain.cookie_count})
</span>
</button>
</div>
{isExpanded && (
<div className="ml-8 pl-2 border-l space-y-1">
<div className="ml-8 space-y-1 border-l pl-2">
{domain.cookies.map((cookie) => {
const isSelected =
domainSelection?.cookies.has(cookie.name) ?? false;
return (
<div
key={`${domain.domain}-${cookie.name}`}
className="flex items-center gap-2 p-1 text-sm hover:bg-accent/30 rounded"
className="flex items-center gap-2 rounded p-1 text-sm hover:bg-accent/30"
>
<Checkbox
checked={isSelected || isAllSelected}
+19 -19
View File
@@ -409,7 +409,7 @@ export function CookieManagementDialog({
</TabsTrigger>
</TabsList>
<TabsContent value="import" className="space-y-4 mt-4">
<TabsContent value="import" className="mt-4 space-y-4">
{!fileContent && (
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
@@ -418,7 +418,7 @@ export function CookieManagementDialog({
<div
role="button"
tabIndex={0}
className="flex flex-col items-center justify-center border-2 border-dashed rounded-lg p-8 transition-colors cursor-pointer border-muted-foreground/25 hover:border-muted-foreground/50"
className="flex cursor-pointer flex-col items-center justify-center rounded-lg border-2 border-dashed border-muted-foreground/25 p-8 transition-colors hover:border-muted-foreground/50"
onClick={() =>
document.getElementById("cookie-file-input")?.click()
}
@@ -429,8 +429,8 @@ export function CookieManagementDialog({
}
}}
>
<LuUpload className="size-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
<LuUpload className="mb-4 size-10 text-muted-foreground" />
<p className="text-center text-sm text-muted-foreground">
{t("cookies.management.dropPrompt")}
<br />
<span className="text-xs">
@@ -454,7 +454,7 @@ export function CookieManagementDialog({
{fileContent && !importResult && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3 rounded-lg bg-muted/30 p-4">
<div>
<div className="font-medium">{fileName}</div>
<div className="text-sm text-muted-foreground">
@@ -481,7 +481,7 @@ export function CookieManagementDialog({
{importResult && (
<div className="space-y-4">
<div className="p-4 rounded-lg bg-success/10">
<div className="rounded-lg bg-success/10 p-4">
<div className="font-medium text-success">
{t("cookies.management.importedSuccess", {
imported: importResult.cookies_imported,
@@ -505,7 +505,7 @@ export function CookieManagementDialog({
)}
</TabsContent>
<TabsContent value="export" className="space-y-3 mt-4">
<TabsContent value="export" className="mt-4 space-y-3">
<div className="space-y-2">
<Label>{t("cookies.export.formatLabel")}</Label>
<Select
@@ -533,7 +533,7 @@ export function CookieManagementDialog({
<Label>
{t("cookies.management.cookiesLabel")}{" "}
{exportCookieData && (
<span className="text-muted-foreground font-normal">
<span className="font-normal text-muted-foreground">
{t("cookies.management.selectionStatus", {
selected: selectedExportCount,
total: exportCookieData.total_count,
@@ -544,7 +544,7 @@ export function CookieManagementDialog({
{exportCookieData && exportCookieData.total_count > 0 && (
<button
type="button"
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
className="text-xs text-muted-foreground transition-colors hover:text-foreground"
onClick={toggleSelectAll}
>
{selectedExportCount === exportCookieData.total_count
@@ -555,16 +555,16 @@ export function CookieManagementDialog({
</div>
{isLoadingExportCookies ? (
<div className="flex items-center justify-center h-24">
<div className="animate-spin size-5 border-2 border-primary border-t-transparent rounded-full" />
<div className="flex h-24 items-center justify-center">
<div className="size-5 animate-spin rounded-full border-2 border-primary border-t-transparent" />
</div>
) : !exportCookieData || exportCookieData.domains.length === 0 ? (
<div className="p-4 text-center text-sm text-muted-foreground border rounded-md">
<div className="rounded-md border p-4 text-center text-sm text-muted-foreground">
{t("cookies.management.noCookies")}
</div>
) : (
<FadingScrollArea className="h-[clamp(140px,30vh,420px)]">
<div className="p-2 space-y-1">
<div className="space-y-1 p-2">
{exportCookieData.domains.map((domain) => (
<ExportDomainRow
key={domain.domain}
@@ -629,7 +629,7 @@ function ExportDomainRow({
return (
<div>
<div className="flex items-center gap-2 p-1.5 hover:bg-accent/50 rounded">
<div className="flex items-center gap-2 rounded p-1.5 hover:bg-accent/50">
<Checkbox
checked={isAllSelected || isPartial}
onCheckedChange={() => {
@@ -639,7 +639,7 @@ function ExportDomainRow({
/>
<button
type="button"
className="flex items-center gap-1 flex-1 text-left text-sm bg-transparent border-none cursor-pointer"
className="flex min-w-0 flex-1 cursor-pointer items-center gap-1 border-none bg-transparent text-left text-sm"
onClick={() => {
onToggleExpand(domain.domain);
}}
@@ -649,21 +649,21 @@ function ExportDomainRow({
) : (
<LuChevronRight className="size-3.5" />
)}
<span className="font-medium truncate">{domain.domain}</span>
<span className="text-xs text-muted-foreground shrink-0">
<span className="truncate font-medium">{domain.domain}</span>
<span className="shrink-0 text-xs text-muted-foreground">
({domain.cookie_count})
</span>
</button>
</div>
{isExpanded && (
<div className="ml-7 pl-2 border-l space-y-0.5">
<div className="ml-7 space-y-0.5 border-l pl-2">
{domain.cookies.map((cookie) => {
const isSelected =
domainSelection?.cookies.has(cookie.name) ?? false;
return (
<div
key={`${domain.domain}-${cookie.name}`}
className="flex items-center gap-2 p-1 text-sm hover:bg-accent/30 rounded"
className="flex items-center gap-2 rounded p-1 text-sm hover:bg-accent/30"
>
<Checkbox
checked={isSelected || isAllSelected}
+1 -1
View File
@@ -93,7 +93,7 @@ export function CreateGroupDialog({
</div>
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
+39 -39
View File
@@ -534,7 +534,7 @@ export function CreateProfileDialog({
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-[min(48rem,calc(100%-4rem))] max-h-[90vh] flex flex-col">
<DialogContent className="flex max-h-[90vh] max-w-[min(48rem,calc(100%-4rem))] flex-col">
<DialogHeader className="shrink-0">
<DialogTitle>
{currentStep === "browser-selection"
@@ -551,13 +551,13 @@ export function CreateProfileDialog({
<Tabs
value={activeTab}
onValueChange={handleTabChange}
className="flex flex-col flex-1 w-full min-h-0"
className="flex min-h-0 w-full flex-1 flex-col"
>
{/* Tab list hidden - only anti-detect browsers are supported */}
<ScrollArea className="overflow-y-auto flex-1">
<div className="flex flex-col justify-center items-center w-full">
<div className="py-4 space-y-6 w-full">
<ScrollArea className="flex-1 overflow-y-auto">
<div className="flex w-full flex-col items-center justify-center">
<div className="w-full space-y-6 py-4">
{currentStep === "browser-selection" ? (
<>
<TabsContent value="anti-detect" className="mt-0 space-y-6">
@@ -569,10 +569,10 @@ export function CreateProfileDialog({
handleBrowserSelect("wayfern");
}}
disabled={!getCreatableVersion("wayfern")}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
className="flex h-16 w-full items-center justify-start gap-3 border-2 p-4 transition-colors hover:border-primary/50"
variant="outline"
>
<div className="flex justify-center items-center size-8">
<div className="flex size-8 items-center justify-center">
{isBrowserCurrentlyDownloading("wayfern") ? (
<LuLoaderCircle className="size-6 animate-spin" />
) : (
@@ -600,7 +600,7 @@ export function CreateProfileDialog({
profiles. Only Wayfern can be created. */}
{!getCreatableVersion("wayfern") && (
<p className="pt-2 text-sm text-center text-muted-foreground">
<p className="pt-2 text-center text-sm text-muted-foreground">
{t("createProfile.browsersDownloading")}
</p>
)}
@@ -629,10 +629,10 @@ export function CreateProfileDialog({
onClick={() => {
handleBrowserSelect(browser.value);
}}
className="flex gap-3 justify-start items-center p-4 w-full h-16 border-2 transition-colors hover:border-primary/50"
className="flex h-16 w-full items-center justify-start gap-3 border-2 p-4 transition-colors hover:border-primary/50"
variant="outline"
>
<div className="flex justify-center items-center size-8">
<div className="flex size-8 items-center justify-center">
{IconComponent && (
<IconComponent className="size-6" />
)}
@@ -684,7 +684,7 @@ export function CreateProfileDialog({
</div>
{/* Ephemeral Option */}
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex items-center gap-x-2">
<Checkbox
id="ephemeral"
@@ -697,14 +697,14 @@ export function CreateProfileDialog({
{t("profiles.ephemeral")}
</Label>
</div>
<p className="text-sm text-muted-foreground ml-6">
<p className="ml-6 text-sm text-muted-foreground">
{t("profiles.ephemeralDescription")}
</p>
</div>
{/* Password Option */}
{!ephemeral && (
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex items-center gap-x-2">
<Checkbox
id="enable-password"
@@ -725,7 +725,7 @@ export function CreateProfileDialog({
{t("createProfile.passwordProtect.label")}
</Label>
</div>
<p className="text-sm text-muted-foreground ml-6">
<p className="ml-6 text-sm text-muted-foreground">
{t("createProfile.passwordProtect.description")}
</p>
{enablePassword && (
@@ -769,15 +769,15 @@ export function CreateProfileDialog({
<div className="space-y-6">
{/* Wayfern Download Status */}
{isLoadingReleaseTypes && (
<div className="flex gap-3 items-center p-3 rounded-md border">
<div className="size-4 rounded-full border-2 animate-spin border-muted/40 border-t-primary" />
<div className="flex items-center gap-3 rounded-md border p-3">
<div className="size-4 animate-spin rounded-full border-2 border-muted/40 border-t-primary" />
<p className="text-sm text-muted-foreground">
{t("createProfile.version.fetching")}
</p>
</div>
)}
{!isLoadingReleaseTypes && releaseTypesError && (
<div className="flex gap-3 items-center p-3 rounded-md border border-destructive/50 bg-destructive/10">
<div className="flex items-center gap-3 rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="flex-1 text-sm text-destructive">
{releaseTypesError}
</p>
@@ -796,7 +796,7 @@ export function CreateProfileDialog({
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-warning/50 bg-warning/10">
<div className="flex items-center gap-3 rounded-md border border-warning/50 bg-warning/10 p-3">
<p className="text-sm text-warning">
{t("createProfile.platformUnavailable", {
browser: "Wayfern",
@@ -809,7 +809,7 @@ export function CreateProfileDialog({
!isBrowserCurrentlyDownloading("wayfern") &&
!getCreatableVersion("wayfern") &&
getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border">
<div className="flex items-center gap-3 rounded-md border p-3">
<p className="text-sm text-muted-foreground">
{t("createProfile.version.needsDownload", {
browser: "Wayfern",
@@ -840,7 +840,7 @@ export function CreateProfileDialog({
!releaseTypesError &&
!isBrowserCurrentlyDownloading("wayfern") &&
getCreatableVersion("wayfern") && (
<div className="p-3 text-sm rounded-md border text-muted-foreground">
<div className="rounded-md border p-3 text-sm text-muted-foreground">
{" "}
{t("createProfile.version.available", {
browser: "Wayfern",
@@ -855,7 +855,7 @@ export function CreateProfileDialog({
getCreatableVersion("wayfern") &&
!isBrowserVersionAvailable("wayfern") &&
getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border">
<div className="flex items-center gap-3 rounded-md border p-3">
<p className="flex-1 text-sm text-muted-foreground">
{t(
"createProfile.version.upgradeAvailable",
@@ -887,7 +887,7 @@ export function CreateProfileDialog({
</div>
)}
{isBrowserCurrentlyDownloading("wayfern") && (
<div className="p-3 text-sm rounded-md border text-muted-foreground">
<div className="rounded-md border p-3 text-sm text-muted-foreground">
{t("createProfile.version.downloading", {
browser: "Wayfern",
version:
@@ -915,8 +915,8 @@ export function CreateProfileDialog({
{selectedBrowser && (
<div className="space-y-3">
{isLoadingReleaseTypes && (
<div className="flex gap-3 items-center">
<div className="size-4 rounded-full border-2 animate-spin border-muted/40 border-t-primary" />
<div className="flex items-center gap-3">
<div className="size-4 animate-spin rounded-full border-2 border-muted/40 border-t-primary" />
<p className="text-sm text-muted-foreground">
{t("createProfile.version.fetching")}
</p>
@@ -924,7 +924,7 @@ export function CreateProfileDialog({
)}
{!isLoadingReleaseTypes &&
releaseTypesError && (
<div className="flex gap-3 items-center p-3 rounded-md border border-destructive/50 bg-destructive/10">
<div className="flex items-center gap-3 rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="flex-1 text-sm text-destructive">
{releaseTypesError}
</p>
@@ -947,7 +947,7 @@ export function CreateProfileDialog({
) &&
!getCreatableVersion(selectedBrowser) &&
getBestAvailableVersion(selectedBrowser) && (
<div className="flex gap-3 items-center">
<div className="flex items-center gap-3">
<p className="text-sm text-muted-foreground">
{t(
"createProfile.version.latestNeedsDownload",
@@ -1016,7 +1016,7 @@ export function CreateProfileDialog({
{/* Proxy / VPN Selection - Always visible */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<Label>{t("createProfile.proxy.title")}</Label>
<RippleButton
size="sm"
@@ -1024,7 +1024,7 @@ export function CreateProfileDialog({
onClick={() => {
setShowProxyForm(true);
}}
className="px-2 h-7 text-xs"
className="h-7 px-2 text-xs"
>
<GoPlus className="mr-1 size-3" />{" "}
{t("createProfile.proxy.addProxy")}
@@ -1144,7 +1144,7 @@ export function CreateProfileDialog({
/>
<Badge
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
className="mr-1 px-1 py-0 text-[10px] leading-tight"
>
WG
</Badge>
@@ -1158,7 +1158,7 @@ export function CreateProfileDialog({
</PopoverContent>
</Popover>
) : (
<div className="flex gap-3 items-center p-3 text-sm rounded-md border text-muted-foreground">
<div className="flex items-center gap-3 rounded-md border p-3 text-sm text-muted-foreground">
{t("createProfile.proxy.noProxiesAvailable")}
</div>
)}
@@ -1285,15 +1285,15 @@ export function CreateProfileDialog({
{selectedBrowser && (
<div className="space-y-3">
{isLoadingReleaseTypes && (
<div className="flex gap-3 items-center">
<div className="size-4 rounded-full border-2 animate-spin border-muted/40 border-t-primary" />
<div className="flex items-center gap-3">
<div className="size-4 animate-spin rounded-full border-2 border-muted/40 border-t-primary" />
<p className="text-sm text-muted-foreground">
{t("createProfile.version.fetching")}
</p>
</div>
)}
{!isLoadingReleaseTypes && releaseTypesError && (
<div className="flex gap-3 items-center p-3 rounded-md border border-destructive/50 bg-destructive/10">
<div className="flex items-center gap-3 rounded-md border border-destructive/50 bg-destructive/10 p-3">
<p className="flex-1 text-sm text-destructive">
{releaseTypesError}
</p>
@@ -1316,7 +1316,7 @@ export function CreateProfileDialog({
) &&
!getCreatableVersion(selectedBrowser) &&
getBestAvailableVersion(selectedBrowser) && (
<div className="flex gap-3 items-center">
<div className="flex items-center gap-3">
<p className="text-sm text-muted-foreground">
{t(
"createProfile.version.latestNeedsDownload",
@@ -1383,7 +1383,7 @@ export function CreateProfileDialog({
{/* Proxy / VPN Selection - Always visible */}
<div className="space-y-3">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<Label>{t("createProfile.proxy.title")}</Label>
<RippleButton
size="sm"
@@ -1391,7 +1391,7 @@ export function CreateProfileDialog({
onClick={() => {
setShowProxyForm(true);
}}
className="px-2 h-7 text-xs"
className="h-7 px-2 text-xs"
>
<GoPlus className="mr-1 size-3" />{" "}
{t("createProfile.proxy.addProxy")}
@@ -1511,7 +1511,7 @@ export function CreateProfileDialog({
/>
<Badge
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
className="mr-1 px-1 py-0 text-[10px] leading-tight"
>
WG
</Badge>
@@ -1525,7 +1525,7 @@ export function CreateProfileDialog({
</PopoverContent>
</Popover>
) : (
<div className="flex gap-3 items-center p-3 text-sm rounded-md border text-muted-foreground">
<div className="flex items-center gap-3 rounded-md border p-3 text-sm text-muted-foreground">
{t("createProfile.proxy.noProxiesAvailable")}
</div>
)}
@@ -1556,7 +1556,7 @@ export function CreateProfileDialog({
</ScrollArea>
</Tabs>
<DialogFooter className="shrink-0 pt-4 border-t">
<DialogFooter className="shrink-0 border-t pt-4">
{currentStep === "browser-config" ? (
<>
<RippleButton variant="outline" onClick={handleBack}>
+23 -25
View File
@@ -162,34 +162,34 @@ function formatEtaCompact(seconds: number): string {
function getToastIcon(type: ToastProps["type"], stage?: string) {
switch (type) {
case "success":
return <LuCheckCheck className="shrink-0 size-4 text-foreground" />;
return <LuCheckCheck className="size-4 shrink-0 text-foreground" />;
case "error":
return <LuTriangleAlert className="shrink-0 size-4 text-foreground" />;
return <LuTriangleAlert className="size-4 shrink-0 text-foreground" />;
case "download":
if (stage === "completed") {
return <LuCheckCheck className="shrink-0 size-4 text-foreground" />;
return <LuCheckCheck className="size-4 shrink-0 text-foreground" />;
}
return <LuDownload className="shrink-0 size-4 text-foreground" />;
return <LuDownload className="size-4 shrink-0 text-foreground" />;
case "version-update":
return (
<LuRefreshCw className="shrink-0 size-4 animate-spin text-foreground" />
<LuRefreshCw className="size-4 shrink-0 animate-spin text-foreground" />
);
case "fetching":
return (
<LuRefreshCw className="shrink-0 size-4 animate-spin text-foreground" />
<LuRefreshCw className="size-4 shrink-0 animate-spin text-foreground" />
);
case "sync-progress":
return (
<LuRefreshCw className="shrink-0 size-4 animate-spin text-foreground" />
<LuRefreshCw className="size-4 shrink-0 animate-spin text-foreground" />
);
case "loading":
return (
<div className="shrink-0 size-4 rounded-full border-2 animate-spin border-foreground border-t-transparent" />
<div className="size-4 shrink-0 animate-spin rounded-full border-2 border-foreground border-t-transparent" />
);
default:
return (
<div className="shrink-0 size-4 rounded-full border-2 animate-spin border-foreground border-t-transparent" />
<div className="size-4 shrink-0 animate-spin rounded-full border-2 border-foreground border-t-transparent" />
);
}
}
@@ -201,18 +201,16 @@ export function UnifiedToast(props: ToastProps) {
const progress = "progress" in props ? props.progress : undefined;
return (
<div className="flex items-start p-3 w-full max-w-md rounded-lg border shadow-lg bg-card border-border text-card-foreground">
<div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div>
<div className="flex-1 min-w-0">
<div className="flex w-full max-w-md items-start rounded-lg border border-border bg-card p-3 text-card-foreground shadow-lg">
<div className="mt-0.5 mr-3">{getToastIcon(type, stage)}</div>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold leading-tight text-foreground">
{title}
</p>
<p className="text-sm/tight font-semibold text-foreground">{title}</p>
{onCancel && (
<button
type="button"
onClick={onCancel}
className="ml-2 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors shrink-0"
className="ml-2 shrink-0 rounded p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label={t("common.buttons.cancel")}
>
<LuX className="size-3" />
@@ -226,17 +224,17 @@ export function UnifiedToast(props: ToastProps) {
"percentage" in progress &&
stage === "downloading" && (
<div className="mt-2 space-y-1">
<div className="flex justify-between items-center">
<p className="flex-1 min-w-0 text-xs text-muted-foreground">
<div className="flex items-center justify-between">
<p className="min-w-0 flex-1 text-xs text-muted-foreground">
{progress.percentage.toFixed(1)}%
{progress.speed && `${progress.speed} MB/s`}
{progress.eta &&
`${t("toasts.progress.remaining", { time: progress.eta })}`}
</p>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div className="h-1.5 w-full rounded-full bg-muted">
<div
className="bg-foreground h-1.5 rounded-full transition-all duration-150"
className="h-1.5 rounded-full bg-foreground transition-all duration-150"
style={{ width: `${progress.percentage}%` }}
/>
</div>
@@ -255,15 +253,15 @@ export function UnifiedToast(props: ToastProps) {
})}
</p>
<div className="flex items-center gap-x-2">
<div className="flex-1 bg-muted rounded-full h-1.5 min-w-0">
<div className="h-1.5 min-w-0 flex-1 rounded-full bg-muted">
<div
className="bg-foreground h-1.5 rounded-full transition-all duration-150"
className="h-1.5 rounded-full bg-foreground transition-all duration-150"
style={{
width: `${(progress.current / progress.total) * 100}%`,
}}
/>
</div>
<span className="w-8 text-xs text-right whitespace-nowrap text-muted-foreground shrink-0">
<span className="w-8 shrink-0 text-right text-xs whitespace-nowrap text-muted-foreground">
{progress.current}/{progress.total}
</span>
</div>
@@ -299,7 +297,7 @@ export function UnifiedToast(props: ToastProps) {
})}`}
</p>
{progress.failed_count > 0 && (
<p className="text-xs text-destructive mt-0.5">
<p className="mt-0.5 text-xs text-destructive">
{t("toasts.progress.filesFailed", {
count: progress.failed_count,
})}
@@ -310,7 +308,7 @@ export function UnifiedToast(props: ToastProps) {
{/* Description */}
{description && (
<p className="mt-1 text-xs leading-tight text-muted-foreground">
<p className="mt-1 text-xs/tight text-muted-foreground">
{description}
</p>
)}
+3 -3
View File
@@ -106,7 +106,7 @@ function DataTableActionBarAction({
{...props}
>
{isPending ? (
<div className="size-3.5 rounded-full border border-current animate-spin border-t-transparent" />
<div className="size-3.5 animate-spin rounded-full border border-current border-t-transparent" />
) : (
children
)}
@@ -142,7 +142,7 @@ function DataTableActionBarSelection<TData>({
return (
<div className="flex h-7 items-center rounded-md border pr-1 pl-2.5">
<span className="whitespace-nowrap text-xs">
<span className="text-xs whitespace-nowrap">
{t("dataTableActionBar.selected", {
count: table.getFilteredSelectedRowModel().rows.length,
})}
@@ -164,7 +164,7 @@ function DataTableActionBarSelection<TData>({
className="flex items-center gap-2 border bg-accent px-2 py-1 font-semibold text-foreground dark:bg-card [&>span]:hidden"
>
<p>{t("dataTableActionBar.clearSelection")}</p>
<kbd className="select-none rounded border bg-background px-1.5 py-px font-mono font-normal text-[0.7rem] text-foreground shadow-xs">
<kbd className="rounded border bg-background px-1.5 py-px font-mono text-[0.7rem] font-normal text-foreground shadow-xs select-none">
<abbr title={t("common.keys.escape")} className="no-underline">
Esc
</abbr>
@@ -55,10 +55,10 @@ export function DeleteConfirmationDialog({
<DialogDescription>{description}</DialogDescription>
{profileIds && profileIds.length > 0 && (
<div className="mt-4">
<p className="text-sm font-medium mb-2">
<p className="mb-2 text-sm font-medium">
{t("deleteDialog.profilesToDelete")}
</p>
<div className="bg-muted rounded-md p-3 max-h-32 overflow-y-auto">
<div className="max-h-32 overflow-y-auto rounded-md bg-muted p-3">
<ul className="space-y-1">
{profileIds.map((id) => {
const profile = profiles.find((p) => p.id === id);
@@ -66,7 +66,7 @@ export function DeleteConfirmationDialog({
return (
<li
key={id}
className="text-sm text-muted-foreground truncate"
className="truncate text-sm text-muted-foreground"
>
{displayName}
</li>
+3 -3
View File
@@ -136,10 +136,10 @@ export function DeleteGroupDialog({
count: associatedProfiles.length,
})}
</Label>
<ScrollArea className="max-h-[min(8rem,25vh)] overflow-y-auto w-full border rounded-md p-3">
<ScrollArea className="max-h-[min(8rem,25vh)] w-full overflow-y-auto rounded-md border p-3">
<div className="space-y-1">
{associatedProfiles.map((profile) => (
<div key={profile.id} className="text-sm truncate">
<div key={profile.id} className="truncate text-sm">
{profile.name}
</div>
))}
@@ -184,7 +184,7 @@ export function DeleteGroupDialog({
)}
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
+4 -4
View File
@@ -87,7 +87,7 @@ export function DnsBlocklistDialog({
{t("dnsBlocklist.settingsDescription")}
</p>
<div className="space-y-3 overflow-y-auto min-h-0 max-h-[40vh]">
<div className="max-h-[40vh] min-h-0 space-y-3 overflow-y-auto">
{statuses.map((status) => (
<div
key={status.level}
@@ -100,18 +100,18 @@ export function DnsBlocklistDialog({
</span>
{status.is_cached ? (
status.is_fresh ? (
<Badge variant="default" className="text-[10px] px-1.5">
<Badge variant="default" className="px-1.5 text-[10px]">
{t("dnsBlocklist.fresh")}
</Badge>
) : (
<Badge variant="secondary" className="text-[10px] px-1.5">
<Badge variant="secondary" className="px-1.5 text-[10px]">
{t("dnsBlocklist.stale")}
</Badge>
)
) : (
<Badge
variant="outline"
className="text-[10px] px-1.5 text-muted-foreground"
className="px-1.5 text-[10px] text-muted-foreground"
>
{t("dnsBlocklist.notCached")}
</Badge>
+1 -1
View File
@@ -103,7 +103,7 @@ export function EditGroupDialog({
</div>
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
@@ -110,8 +110,8 @@ export function ExtensionGroupAssignmentDialog({
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("extensions.assignTitle")}:</Label>
<div className="p-3 bg-muted rounded-md max-h-[min(8rem,20vh)] overflow-y-auto">
<ul className="text-sm space-y-1">
<div className="max-h-[min(8rem,20vh)] overflow-y-auto rounded-md bg-muted p-3">
<ul className="space-y-1 text-sm">
{selectedProfiles.map((profileId) => {
const profile = profiles.find(
(p: BrowserProfile) => p.id === profileId,
@@ -160,7 +160,7 @@ export function ExtensionGroupAssignmentDialog({
</div>
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
+47 -47
View File
@@ -655,7 +655,7 @@ export function ExtensionManagementDialog({
const hasFirefox = compat.includes("firefox");
if (!hasChromium && !hasFirefox) return null;
return (
<div className="flex items-center gap-1 shrink-0">
<div className="flex shrink-0 items-center gap-1">
{hasChromium && (
<Tooltip>
<TooltipTrigger asChild>
@@ -753,7 +753,7 @@ export function ExtensionManagementDialog({
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
className="h-auto cursor-pointer justify-start p-0 text-left font-semibold"
>
{t("common.labels.name")}
{column.getIsSorted() === "asc" ? (
@@ -764,7 +764,7 @@ export function ExtensionManagementDialog({
</Button>
),
cell: ({ row }) => (
<span className="text-sm font-medium truncate min-w-0 block">
<span className="block min-w-0 truncate text-sm font-medium">
{row.original.name}
</span>
),
@@ -786,7 +786,7 @@ export function ExtensionManagementDialog({
const ext = row.original;
const syncDot = getSyncStatusDot(ext, extSyncStatus[ext.id], t);
return (
<div className="flex items-center gap-2 shrink-0">
<div className="flex shrink-0 items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -801,7 +801,7 @@ export function ExtensionManagementDialog({
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center shrink-0">
<span className="inline-flex shrink-0 items-center">
<AnimatedSwitch
checked={ext.sync_enabled}
onCheckedChange={() => void handleToggleExtSync(ext)}
@@ -829,7 +829,7 @@ export function ExtensionManagementDialog({
cell: ({ row }) => {
const ext = row.original;
return (
<div className="flex gap-0.5 justify-end shrink-0">
<div className="flex shrink-0 justify-end gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -927,7 +927,7 @@ export function ExtensionManagementDialog({
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
className="h-auto cursor-pointer justify-start p-0 text-left font-semibold"
>
{t("common.labels.name")}
{column.getIsSorted() === "asc" ? (
@@ -938,7 +938,7 @@ export function ExtensionManagementDialog({
</Button>
),
cell: ({ row }) => (
<span className="font-medium text-sm truncate min-w-0 block">
<span className="block min-w-0 truncate text-sm font-medium">
{row.original.name}
</span>
),
@@ -956,7 +956,7 @@ export function ExtensionManagementDialog({
const visibleExts = groupExts.slice(0, MAX_VISIBLE_ICONS);
const overflowCount = groupExts.length - MAX_VISIBLE_ICONS;
return (
<div className="flex items-center gap-1 min-w-0">
<div className="flex min-w-0 items-center gap-1">
{visibleExts.map((ext) => (
<Tooltip key={ext.id}>
<TooltipTrigger asChild>
@@ -972,7 +972,7 @@ export function ExtensionManagementDialog({
<TooltipTrigger asChild>
<Badge
variant="secondary"
className="text-xs h-5 px-1.5 shrink-0"
className="h-5 shrink-0 px-1.5 text-xs"
>
+{overflowCount}
</Badge>
@@ -989,7 +989,7 @@ export function ExtensionManagementDialog({
</Tooltip>
)}
{groupExts.length === 0 && (
<span className="text-xs text-muted-foreground truncate min-w-0">
<span className="min-w-0 truncate text-xs text-muted-foreground">
{t("extensions.noExtensionsInGroup")}
</span>
)}
@@ -1010,7 +1010,7 @@ export function ExtensionManagementDialog({
t,
);
return (
<div className="flex items-center gap-2 shrink-0">
<div className="flex shrink-0 items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -1025,7 +1025,7 @@ export function ExtensionManagementDialog({
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center shrink-0">
<span className="inline-flex shrink-0 items-center">
<AnimatedSwitch
checked={group.sync_enabled}
onCheckedChange={() => void handleToggleGroupSync(group)}
@@ -1053,7 +1053,7 @@ export function ExtensionManagementDialog({
cell: ({ row }) => {
const group = row.original;
return (
<div className="flex gap-0.5 justify-end shrink-0">
<div className="flex shrink-0 justify-end gap-0.5">
<Tooltip>
<TooltipTrigger asChild>
<Button
@@ -1116,7 +1116,7 @@ export function ExtensionManagementDialog({
return (
<>
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
<DialogContent className="max-w-[min(80rem,calc(100%-4rem))] max-h-[90vh] flex flex-col">
<DialogContent className="flex max-h-[90vh] max-w-[min(80rem,calc(100%-4rem))] flex-col">
{!subPage && (
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
@@ -1130,15 +1130,15 @@ export function ExtensionManagementDialog({
</DialogHeader>
)}
<div className="@container relative w-full flex-1 min-h-0 flex flex-col">
<div className="@container relative flex min-h-0 w-full flex-1 flex-col">
{limitedMode && (
<>
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
<div className="absolute inset-y-0 left-0 w-6 bg-linear-to-r from-background to-transparent z-[2]" />
<div className="absolute inset-y-0 right-0 w-6 bg-linear-to-l from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 top-0 h-6 bg-linear-to-b from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background to-transparent z-[2]" />
<div className="absolute inset-0 flex items-center justify-center z-[3]">
<div className="absolute inset-0 z-1 bg-background/30 backdrop-blur-[6px]" />
<div className="absolute inset-y-0 left-0 z-2 w-6 bg-linear-to-r from-background to-transparent" />
<div className="absolute inset-y-0 right-0 z-2 w-6 bg-linear-to-l from-background to-transparent" />
<div className="absolute inset-x-0 top-0 z-2 h-6 bg-linear-to-b from-background to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-2 h-6 bg-linear-to-t from-background to-transparent" />
<div className="absolute inset-0 z-3 flex items-center justify-center">
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
<ProBadge />
<span className="text-sm font-medium text-muted-foreground">
@@ -1153,9 +1153,9 @@ export function ExtensionManagementDialog({
key={initialTab}
value={activeTab}
onValueChange={(v) => setActiveTab(v as "extensions" | "groups")}
className="flex-1 min-h-0 flex flex-col"
className="flex min-h-0 flex-1 flex-col"
>
<div className="flex flex-wrap items-center justify-between gap-2 shrink-0">
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2">
<AnimatedTabsList>
<AnimatedTabsTrigger
value="extensions"
@@ -1219,15 +1219,15 @@ export function ExtensionManagementDialog({
</div>
{/* Notice */}
<div className="rounded-md bg-muted/50 p-3 text-sm text-muted-foreground mt-4 shrink-0">
<div className="mt-4 shrink-0 rounded-md bg-muted/50 p-3 text-sm text-muted-foreground">
{t("extensions.managedNotice")}
</div>
<AnimatedTabsContent
value="extensions"
className="mt-4 flex-1 min-h-0 data-[state=active]:flex flex-col"
className="mt-4 min-h-0 flex-1 flex-col data-[state=active]:flex"
>
<div className="flex flex-col gap-4 flex-1 min-h-0">
<div className="flex min-h-0 flex-1 flex-col gap-4">
<Input
id="ext-file-input"
type="file"
@@ -1291,7 +1291,7 @@ export function ExtensionManagementDialog({
) : (
<FadingScrollArea
className={cn(
"flex-1 min-h-0",
"min-h-0 flex-1",
selectedExtensions.length > 0 && "pb-16",
)}
style={
@@ -1367,12 +1367,12 @@ export function ExtensionManagementDialog({
<AnimatedTabsContent
value="groups"
className="mt-4 flex-1 min-h-0 data-[state=active]:flex flex-col"
className="mt-4 min-h-0 flex-1 flex-col data-[state=active]:flex"
>
<div className="flex flex-col gap-4 flex-1 min-h-0">
<div className="flex min-h-0 flex-1 flex-col gap-4">
{/* Create group form */}
{showCreateGroup && (
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<Input
value={newGroupName}
onChange={(e) => {
@@ -1412,7 +1412,7 @@ export function ExtensionManagementDialog({
) : (
<FadingScrollArea
className={cn(
"flex-1 min-h-0",
"min-h-0 flex-1",
selectedGroups.length > 0 && "pb-16",
)}
style={
@@ -1509,7 +1509,7 @@ export function ExtensionManagementDialog({
}
}}
>
<DialogContent className="max-w-lg max-h-[90vh] flex flex-col">
<DialogContent className="flex max-h-[90vh] max-w-lg flex-col">
<DialogHeader>
<DialogTitle>{t("extensions.editGroup")}</DialogTitle>
<DialogDescription>
@@ -1517,7 +1517,7 @@ export function ExtensionManagementDialog({
</DialogDescription>
</DialogHeader>
<ScrollArea className="overflow-y-auto flex-1 -mx-6 px-6">
<ScrollArea className="-mx-6 flex-1 overflow-y-auto px-6">
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("common.labels.name")}</Label>
@@ -1562,11 +1562,11 @@ export function ExtensionManagementDialog({
<div className="space-y-2">
<Label>{t("extensions.groupExtensions")}</Label>
{editGroupExtensionIds.length === 0 ? (
<div className="text-sm text-muted-foreground py-2">
<div className="py-2 text-sm text-muted-foreground">
{t("extensions.noExtensionsInGroup")}
</div>
) : (
<div className="space-y-1 max-h-[min(40vh,320px)] overflow-y-auto">
<div className="max-h-[min(40vh,320px)] space-y-1 overflow-y-auto">
{editGroupExtensionIds.map((extId) => {
const ext = extensions.find((e) => e.id === extId);
if (!ext) return null;
@@ -1576,14 +1576,14 @@ export function ExtensionManagementDialog({
className="flex items-center gap-2 rounded-md border px-2 py-1.5"
>
{renderExtensionIcon(ext, "sm")}
<span className="text-sm flex-1 truncate min-w-0">
<span className="min-w-0 flex-1 truncate text-sm">
{ext.name}
</span>
{renderCompatIcons(ext.browser_compatibility)}
<Button
variant="ghost"
size="sm"
className="size-6 p-0 shrink-0"
className="size-6 shrink-0 p-0"
onClick={() => {
setEditGroupExtensionIds((prev) =>
prev.filter((id) => id !== extId),
@@ -1633,7 +1633,7 @@ export function ExtensionManagementDialog({
}
}}
>
<DialogContent className="max-w-lg max-h-[90vh] flex flex-col">
<DialogContent className="flex max-h-[90vh] max-w-lg flex-col">
<DialogHeader>
<DialogTitle>{t("extensions.editExtension")}</DialogTitle>
<DialogDescription>
@@ -1641,7 +1641,7 @@ export function ExtensionManagementDialog({
</DialogDescription>
</DialogHeader>
<ScrollArea className="overflow-y-auto flex-1 -mx-6 px-6">
<ScrollArea className="-mx-6 flex-1 overflow-y-auto px-6">
{editingExtension && (
<div className="space-y-4">
<div className="space-y-2">
@@ -1659,8 +1659,8 @@ export function ExtensionManagementDialog({
</div>
{/* Metadata from manifest.json */}
<div className="rounded-md border p-3 space-y-2">
<Label className="text-xs text-muted-foreground uppercase tracking-wide">
<div className="space-y-2 rounded-md border p-3">
<Label className="text-xs tracking-wide text-muted-foreground uppercase">
{t("extensions.metadata")}
</Label>
<div className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1.5 text-sm">
@@ -1711,7 +1711,7 @@ export function ExtensionManagementDialog({
href={editingExtension.homepage_url}
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline flex items-center gap-1 min-w-0"
className="flex min-w-0 items-center gap-1 text-primary hover:underline"
>
<span className="truncate">
{editingExtension.homepage_url}
@@ -1724,7 +1724,7 @@ export function ExtensionManagementDialog({
!editingExtension.author &&
!editingExtension.description &&
!editingExtension.homepage_url && (
<span className="col-span-2 text-muted-foreground text-xs">
<span className="col-span-2 text-xs text-muted-foreground">
{t("extensions.noMetadata")}
</span>
)}
@@ -1734,7 +1734,7 @@ export function ExtensionManagementDialog({
{/* Re-upload */}
<div className="space-y-2">
<Label>{t("extensions.reupload")}</Label>
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<RippleButton
size="sm"
variant="outline"
@@ -1742,7 +1742,7 @@ export function ExtensionManagementDialog({
document.getElementById("ext-edit-file-input")?.click()
}
>
<LuUpload className="size-3 mr-1" />
<LuUpload className="mr-1 size-3" />
{t("extensions.selectFile")}
</RippleButton>
<input
@@ -1753,7 +1753,7 @@ export function ExtensionManagementDialog({
onChange={handleEditFileSelect}
/>
{pendingUpdateFile && (
<span className="text-xs text-muted-foreground truncate max-w-[200px]">
<span className="max-w-[200px] truncate text-xs text-muted-foreground">
{pendingUpdateFile.name}
</span>
)}
+4 -4
View File
@@ -134,8 +134,8 @@ export function GroupAssignmentDialog({
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("groupAssignment.selectedProfilesLabel")}</Label>
<div className="p-3 bg-muted rounded-md max-h-[min(8rem,20vh)] overflow-y-auto">
<ul className="text-sm space-y-1">
<div className="max-h-[min(8rem,20vh)] overflow-y-auto rounded-md bg-muted p-3">
<ul className="space-y-1 text-sm">
{selectedProfiles.map((profileId) => {
// Find the profile name for display
const profile = profiles.find(
@@ -153,7 +153,7 @@ export function GroupAssignmentDialog({
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<Label htmlFor="group-select">
{t("groupAssignment.assignGroupLabel")}
</Label>
@@ -198,7 +198,7 @@ export function GroupAssignmentDialog({
</div>
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
+7 -7
View File
@@ -327,7 +327,7 @@ export function GroupManagementDialog({
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
className="h-auto cursor-pointer justify-start p-0 text-left font-semibold"
>
{t("common.labels.name")}
{column.getIsSorted() === "asc" ? (
@@ -346,7 +346,7 @@ export function GroupManagementDialog({
groupSyncErrors[group.id],
);
return (
<div className="flex items-center gap-2 font-medium min-w-0">
<div className="flex min-w-0 items-center gap-2 font-medium">
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -557,7 +557,7 @@ export function GroupManagementDialog({
return (
<>
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
<DialogContent className="max-w-[min(60rem,calc(100%-4rem))] max-h-[90vh] flex flex-col">
<DialogContent className="flex max-h-[90vh] max-w-[min(60rem,calc(100%-4rem))] flex-col">
{!subPage && (
<DialogHeader>
<DialogTitle>{t("groups.management")}</DialogTitle>
@@ -567,7 +567,7 @@ export function GroupManagementDialog({
</DialogHeader>
)}
<div className="w-full flex flex-col gap-4 flex-1 min-h-0">
<div className="flex min-h-0 w-full flex-1 flex-col gap-4">
<div className="flex items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<h2 className="text-base font-semibold">
@@ -582,7 +582,7 @@ export function GroupManagementDialog({
onClick={() => {
setCreateDialogOpen(true);
}}
className="flex gap-2 items-center shrink-0"
className="flex shrink-0 items-center gap-2"
>
<GoPlus className="size-4" />
{t("proxies.management.create")}
@@ -590,7 +590,7 @@ export function GroupManagementDialog({
</div>
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
@@ -607,7 +607,7 @@ export function GroupManagementDialog({
) : (
<FadingScrollArea
className={cn(
"flex-1 min-h-0",
"min-h-0 flex-1",
selectedGroupsForBulk.length > 0 && "pb-16",
)}
style={
+18 -18
View File
@@ -175,7 +175,7 @@ const HomeHeader = ({
onPointerCancel={handlePointerEnd}
onDoubleClick={handleDoubleClick}
className={cn(
"flex items-center gap-2 h-11 pl-3 border-b border-border bg-card select-none",
"flex h-11 items-center gap-2 border-b border-border bg-card pl-3 select-none",
// Windows: WindowDragArea renders three 44px native-style controls
// (minimize + maximize/restore + close) fixed at top-right with
// z-50, total 132px wide. Reserve 144px on the right edge so the
@@ -187,24 +187,24 @@ const HomeHeader = ({
{isMacOS && (
<div
aria-hidden="true"
className="flex items-center gap-[7px] mr-1 shrink-0"
className="mr-1 flex shrink-0 items-center gap-[7px]"
>
{/* Reserve space for the macOS native traffic lights — the OS draws
the colored buttons here through the transparent titlebar. */}
<div className="w-[11px] h-[11px] rounded-full" />
<div className="w-[11px] h-[11px] rounded-full" />
<div className="w-[11px] h-[11px] rounded-full" />
<div className="size-[11px] rounded-full" />
<div className="size-[11px] rounded-full" />
<div className="size-[11px] rounded-full" />
</div>
)}
{pageTitle ? (
<span className="text-xs font-semibold text-card-foreground ml-2">
<span className="ml-2 text-xs font-semibold text-card-foreground">
{pageTitle}
</span>
) : null}
{showProfileToolbar && (
<div className="relative flex-1 min-w-0 flex items-center">
<div className="relative flex min-w-0 flex-1 items-center">
{groupsFadeLeft && (
<button
type="button"
@@ -217,14 +217,14 @@ const HomeHeader = ({
behavior: "smooth",
});
}}
className="absolute left-0 top-1/2 -translate-y-1/2 z-10 grid place-items-center size-5 rounded-full bg-card/90 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors shadow-sm"
className="absolute top-1/2 left-0 z-10 grid size-5 -translate-y-1/2 place-items-center rounded-full bg-card/90 text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground"
>
<LuChevronLeft className="size-3" />
</button>
)}
<div
ref={groupsScrollRef}
className="flex items-center gap-3 ml-2 overflow-x-auto scroll-smooth [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
className="ml-2 flex scrollbar-none items-center gap-3 overflow-x-auto scroll-smooth [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
style={{
paddingLeft: groupsFadeLeft ? 22 : 0,
paddingRight: groupsFadeRight ? 22 : 0,
@@ -241,9 +241,9 @@ const HomeHeader = ({
onGroupSelect(ALL_FILTER_ID);
}}
className={cn(
"flex items-center gap-1.5 h-7 px-1 text-xs transition-colors duration-100 shrink-0",
"flex h-7 shrink-0 items-center gap-1.5 px-1 text-xs transition-colors duration-100",
active
? "text-foreground font-medium"
? "font-medium text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
@@ -265,9 +265,9 @@ const HomeHeader = ({
onGroupSelect(active ? ALL_FILTER_ID : group.id);
}}
className={cn(
"flex items-center gap-1.5 h-7 px-1 text-xs transition-colors duration-100 shrink-0",
"flex h-7 shrink-0 items-center gap-1.5 px-1 text-xs transition-colors duration-100",
active
? "text-foreground font-medium"
? "font-medium text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
@@ -291,7 +291,7 @@ const HomeHeader = ({
behavior: "smooth",
});
}}
className="absolute right-0 top-1/2 -translate-y-1/2 z-10 grid place-items-center size-5 rounded-full bg-card/90 hover:bg-accent text-muted-foreground hover:text-foreground transition-colors shadow-sm"
className="absolute top-1/2 right-0 z-10 grid size-5 -translate-y-1/2 place-items-center rounded-full bg-card/90 text-muted-foreground shadow-sm transition-colors hover:bg-accent hover:text-foreground"
>
<LuChevronRight className="size-3" />
</button>
@@ -310,16 +310,16 @@ const HomeHeader = ({
onChange={(e) => {
onSearchQueryChange(e.target.value);
}}
className="pr-7 pl-8 w-36 min-[860px]:w-52 h-7 text-xs"
className="h-7 w-36 pr-7 pl-8 text-xs min-[860px]:w-52"
/>
<LuSearch className="absolute left-2.5 top-1/2 size-3.5 transform -translate-y-1/2 text-muted-foreground pointer-events-none" />
<LuSearch className="pointer-events-none absolute top-1/2 left-2.5 size-3.5 -translate-y-1/2 transform text-muted-foreground" />
{searchQuery ? (
<button
type="button"
onClick={() => {
onSearchQueryChange("");
}}
className="absolute right-1.5 top-1/2 p-0.5 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
className="absolute top-1/2 right-1.5 -translate-y-1/2 transform rounded-sm p-0.5 transition-colors hover:bg-accent"
aria-label={t("header.clearSearch")}
>
<LuX className="size-3.5 text-muted-foreground hover:text-foreground" />
@@ -338,7 +338,7 @@ const HomeHeader = ({
onClick={() => {
onCreateProfileDialogOpen(true);
}}
className="flex gap-1.5 items-center h-7 px-2.5 text-xs"
className="flex h-7 items-center gap-1.5 px-2.5 text-xs"
>
<GoPlus className="size-3.5" />
{t("header.newProfile")}
+8 -8
View File
@@ -306,7 +306,7 @@ export function ImportProfileDialog({
return (
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
<DialogContent className="max-w-[min(48rem,calc(100%-4rem))] max-h-[80vh] flex flex-col">
<DialogContent className="flex max-h-[80vh] max-w-[min(48rem,calc(100%-4rem))] flex-col">
{!subPage && (
<DialogHeader className="shrink-0">
<DialogTitle>{t("importProfile.title")}</DialogTitle>
@@ -315,7 +315,7 @@ export function ImportProfileDialog({
<div
className={cn(
"overflow-y-auto flex-1 space-y-6 min-h-0",
"min-h-0 flex-1 space-y-6 overflow-y-auto",
subPage && "mx-auto w-full max-w-2xl",
)}
>
@@ -389,7 +389,7 @@ export function ImportProfileDialog({
key={profile.path}
value={profile.path}
>
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
{IconComponent && (
<IconComponent className="size-4" />
)}
@@ -413,7 +413,7 @@ export function ImportProfileDialog({
</div>
{selectedProfile && (
<div className="p-3 rounded-lg bg-muted">
<div className="rounded-lg bg-muted p-3">
<p className="text-sm break-all">
<span className="font-medium">
{t("importProfile.pathLabel")}
@@ -481,7 +481,7 @@ export function ImportProfileDialog({
const IconComponent = getBrowserIcon(browser);
return (
<SelectItem key={browser} value={browser}>
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
{IconComponent && (
<IconComponent className="size-4" />
)}
@@ -518,7 +518,7 @@ export function ImportProfileDialog({
<FaFolder className="size-4" />
</Button>
</div>
<p className="mt-2 text-xs text-muted-foreground break-all">
<p className="mt-2 text-xs break-all text-muted-foreground">
{t("importProfile.examplePaths")}
<br />
macOS: ~/Library/Application
@@ -604,9 +604,9 @@ export function ImportProfileDialog({
<div
className={cn(
"shrink-0 flex gap-2 items-center justify-end",
"flex shrink-0 items-center justify-end gap-2",
subPage
? "pt-2 border-t border-border mx-auto w-full max-w-2xl"
? "mx-auto w-full max-w-2xl border-t border-border pt-2"
: undefined,
)}
>
+30 -30
View File
@@ -308,7 +308,7 @@ export function IntegrationsDialog({
}}
subPage={subPage}
>
<DialogContent className="max-w-3xl max-h-[calc(100vh-5rem)] flex flex-col">
<DialogContent className="flex max-h-[calc(100vh-5rem)] max-w-3xl flex-col">
{!subPage && (
<DialogHeader className="shrink-0">
<DialogTitle>{t("integrations.title")}</DialogTitle>
@@ -317,8 +317,8 @@ export function IntegrationsDialog({
<div
className={cn(
"overflow-y-auto flex-1 min-h-0",
subPage && "w-full max-w-3xl mx-auto",
"min-h-0 flex-1 overflow-y-auto",
subPage && "mx-auto w-full max-w-3xl",
)}
>
<AnimatedTabs key={initialTab} defaultValue={initialTab}>
@@ -333,12 +333,12 @@ export function IntegrationsDialog({
<AnimatedTabsContent
value="api"
className="mt-4 flex flex-col gap-4 @container"
className="@container mt-4 flex flex-col gap-4"
>
<div className="rounded-md border bg-card p-4 flex flex-col gap-4">
<div className="flex flex-col gap-4 rounded-md border bg-card p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3">
<LuPlug className="size-5 mt-0.5 text-muted-foreground" />
<LuPlug className="mt-0.5 size-5 text-muted-foreground" />
<div className="flex flex-col gap-1">
<Label className="text-sm font-medium">
{t("integrations.apiEnableLabel")}
@@ -370,9 +370,9 @@ export function IntegrationsDialog({
{settings.api_enabled && (
<>
<div className="grid grid-cols-1 @2xl:grid-cols-2 gap-4">
<div className="rounded-md border bg-card p-4 flex flex-col gap-2">
<Label className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="grid grid-cols-1 gap-4 @2xl:grid-cols-2">
<div className="flex flex-col gap-2 rounded-md border bg-card p-4">
<Label className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("integrations.apiPortLabel")}
</Label>
<div className="flex items-center gap-2">
@@ -463,9 +463,9 @@ export function IntegrationsDialog({
</div>
</div>
<div className="rounded-md border bg-card p-4 flex flex-col gap-2">
<div className="flex flex-col gap-2 rounded-md border bg-card p-4">
<div className="flex items-center justify-between">
<Label className="text-[10px] uppercase tracking-wide text-muted-foreground">
<Label className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("integrations.apiTokenLabel")}
</Label>
</div>
@@ -475,13 +475,13 @@ export function IntegrationsDialog({
type={showApiToken ? "text" : "password"}
value={settings.api_token ?? ""}
readOnly
className="font-mono pr-10"
className="pr-10 font-mono"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
className="absolute top-0 right-0 h-full px-3 hover:bg-transparent"
onClick={() => {
setShowApiToken(!showApiToken);
}}
@@ -501,9 +501,9 @@ export function IntegrationsDialog({
</div>
</div>
<div className="rounded-md border bg-card p-4 flex flex-col gap-2">
<div className="flex flex-col gap-2 rounded-md border bg-card p-4">
<div className="flex items-center justify-between">
<Label className="text-[10px] uppercase tracking-wide text-muted-foreground">
<Label className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("integrations.apiExampleRequest")}
</Label>
<CopyToClipboard
@@ -511,7 +511,7 @@ export function IntegrationsDialog({
successMessage={t("common.buttons.copied")}
/>
</div>
<pre className="font-mono text-[11px] whitespace-pre overflow-x-auto bg-background rounded p-3">
<pre className="overflow-x-auto rounded bg-background p-3 font-mono text-[11px] whitespace-pre">
{`curl -H "Authorization: Bearer \${TOKEN}" \\
http://127.0.0.1:${apiServerPort ?? settings.api_port}/v1/profiles`}
</pre>
@@ -524,10 +524,10 @@ export function IntegrationsDialog({
value="mcp"
className="mt-4 flex flex-col gap-5"
>
<div className="rounded-md border bg-card p-4 flex flex-col gap-4">
<div className="flex flex-col gap-4 rounded-md border bg-card p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex items-start gap-3">
<LuZap className="size-5 mt-0.5 text-muted-foreground" />
<LuZap className="mt-0.5 size-5 text-muted-foreground" />
<div className="flex flex-col gap-1">
<Label className="text-sm font-medium">
{t("integrations.mcpEnableLabel")}
@@ -552,8 +552,8 @@ export function IntegrationsDialog({
{mcpConfig && (
<>
<div className="rounded-md border bg-card p-4 flex flex-col gap-2">
<Label className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="flex flex-col gap-2 rounded-md border bg-card p-4">
<Label className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("integrations.mcp.url")}
</Label>
<div className="flex items-center gap-x-2">
@@ -562,13 +562,13 @@ export function IntegrationsDialog({
type={showMcpUrl ? "text" : "password"}
value={mcpUrl}
readOnly
className="font-mono text-xs pr-10"
className="pr-10 font-mono text-xs"
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 hover:bg-transparent"
className="absolute top-0 right-0 h-full px-3 hover:bg-transparent"
onClick={() => {
setShowMcpUrl(!showMcpUrl);
}}
@@ -587,32 +587,32 @@ export function IntegrationsDialog({
</div>
</div>
<div className="flex flex-col gap-3 @container">
<Label className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="@container flex flex-col gap-3">
<Label className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("integrations.mcp.clientsLabel")}
</Label>
<div className="grid grid-cols-1 @2xl:grid-cols-2 gap-3">
<div className="grid grid-cols-1 gap-3 @2xl:grid-cols-2">
{agents.map((agent) => {
const busy = busyAgentIds.has(agent.id);
return (
<div
key={agent.id}
className="rounded-md border bg-card px-3 py-2.5 flex items-center gap-3"
className="flex items-center gap-3 rounded-md border bg-card px-3 py-2.5"
>
<div className="grid place-items-center size-8 rounded-md bg-muted shrink-0">
<div className="grid size-8 shrink-0 place-items-center rounded-md bg-muted">
<AgentIcon category={agent.category} />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium truncate">
<p className="truncate text-sm font-medium">
{agent.display_name}
</p>
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{categoryLabel(t, agent.category)}
</p>
</div>
{agent.connected ? (
<div className="flex items-center gap-1">
<span className="inline-flex items-center gap-1 rounded-md border bg-muted px-2 py-1 text-[10px] font-medium uppercase tracking-wide text-foreground">
<span className="inline-flex items-center gap-1 rounded-md border bg-muted px-2 py-1 text-[10px] font-medium tracking-wide text-foreground uppercase">
<LuCheck className="size-3" />
{t("integrations.mcp.connected")}
</span>
+1 -1
View File
@@ -233,7 +233,7 @@ export function LocationProxyDialog({
</DialogDescription>
</DialogHeader>
<div className="space-y-4 overflow-y-auto min-h-0 max-h-[calc(100vh-16rem)] pr-1">
<div className="max-h-[calc(100vh-16rem)] min-h-0 space-y-4 overflow-y-auto pr-1">
{/* Country - always visible */}
<div className="space-y-2">
<Label className="flex items-center gap-2">
+7 -7
View File
@@ -152,7 +152,7 @@ const CommandEmpty = forwardRef<
return (
<div
ref={forwardedRef}
className={cn("py-6 text-sm text-center", className)}
className={cn("py-6 text-center text-sm", className)}
cmdk-empty=""
role="presentation"
{...props}
@@ -428,8 +428,8 @@ const MultipleSelector = React.forwardRef<
<Badge
key={option.value}
className={cn(
"data-[disabled]:bg-muted-foreground data-[disabled]:text-muted data-[disabled]:hover:bg-muted-foreground",
"data-[fixed]:bg-muted-foreground data-[fixed]:text-muted data-[fixed]:hover:bg-muted-foreground",
"data-disabled:bg-muted-foreground data-disabled:text-muted data-disabled:hover:bg-muted-foreground",
"data-fixed:bg-muted-foreground data-fixed:text-muted data-fixed:hover:bg-muted-foreground",
badgeClassName,
)}
data-fixed={option.fixed}
@@ -439,7 +439,7 @@ const MultipleSelector = React.forwardRef<
<button
type="button"
className={cn(
"cursor-pointer ml-1 rounded-full outline-none ring-offset-background focus:ring-2 focus:ring-ring focus:ring-offset-2",
"ml-1 cursor-pointer rounded-full ring-offset-background outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
(disabled ?? option.fixed) && "hidden",
)}
onKeyDown={(e) => {
@@ -525,7 +525,7 @@ const MultipleSelector = React.forwardRef<
"flex-1 bg-transparent outline-none placeholder:text-muted-foreground",
{
"w-full": hidePlaceholderWhenSelected,
"px-3 mt-1": selected.length === 0,
"mt-1 px-3": selected.length === 0,
"ml-1": selected.length !== 0,
},
inputProps?.className,
@@ -537,7 +537,7 @@ const MultipleSelector = React.forwardRef<
{open && hasAvailableOptions && (
<CommandList
className={cn(
"absolute z-10 w-full rounded-md border shadow-md outline-none bg-popover text-popover-foreground animate-in",
"absolute z-10 w-full animate-in rounded-md border bg-popover text-popover-foreground shadow-md outline-none",
dropUp ? "bottom-full mb-1" : "top-full mt-1",
)}
>
@@ -554,7 +554,7 @@ const MultipleSelector = React.forwardRef<
<CommandGroup
key={key}
heading={key}
className="overflow-auto max-h-48"
className="max-h-48 overflow-auto"
>
{dropdowns.map((option) => {
return (
+11 -11
View File
@@ -28,26 +28,26 @@ export function OnboardingCard({
const requiresAction = step.selector === '[data-onborda="create-profile"]';
return (
<div className="relative p-4 w-80 max-w-[90vw] rounded-lg border shadow-lg bg-popover text-popover-foreground">
<div className="flex gap-2 items-start justify-between">
<h3 className="text-sm font-semibold leading-tight">{step.title}</h3>
<span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
<div className="relative w-80 max-w-[90vw] rounded-lg border bg-popover p-4 text-popover-foreground shadow-lg">
<div className="flex items-start justify-between gap-2">
<h3 className="text-sm/tight font-semibold">{step.title}</h3>
<span className="shrink-0 text-[11px] text-muted-foreground tabular-nums">
{currentStep + 1}/{totalSteps}
</span>
</div>
<div className="mt-2 text-xs leading-relaxed text-muted-foreground">
<div className="mt-2 text-xs/relaxed text-muted-foreground">
{step.content}
</div>
<div className="flex gap-2 items-center justify-between mt-4">
<div className="mt-4 flex items-center justify-between gap-2">
{isLast ? (
<span />
) : (
<Button
variant="ghost"
size="sm"
className="text-xs h-7 px-2 text-muted-foreground hover:text-foreground"
className="h-7 px-2 text-xs text-muted-foreground hover:text-foreground"
onClick={() => {
closeOnborda();
}}
@@ -56,12 +56,12 @@ export function OnboardingCard({
</Button>
)}
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
{!isFirst && !isLast && (
<Button
variant="outline"
size="sm"
className="text-xs h-7 px-2.5"
className="h-7 px-2.5 text-xs"
onClick={() => {
prevStep();
}}
@@ -72,7 +72,7 @@ export function OnboardingCard({
{isLast ? (
<Button
size="sm"
className="text-xs h-7 px-3"
className="h-7 px-3 text-xs"
onClick={() => {
closeOnborda();
window.dispatchEvent(new Event(ONBOARDING_TOUR_FINISHED_EVENT));
@@ -83,7 +83,7 @@ export function OnboardingCard({
) : requiresAction ? null : (
<Button
size="sm"
className="text-xs h-7 px-3"
className="h-7 px-3 text-xs"
onClick={() => {
nextStep();
}}
+1 -1
View File
@@ -206,7 +206,7 @@ export function PermissionDialog({
<div className="space-y-4">
{!isCurrentPermissionGranted && (
<div className="p-3 bg-warning/10 rounded-lg">
<div className="rounded-lg bg-warning/10 p-3">
<p className="text-sm text-warning">
{permissionType === "microphone"
? t("permissionDialog.notGrantedMicrophone")
+61 -61
View File
@@ -365,10 +365,10 @@ function ExtCell({
<button
type="button"
disabled={isSaving}
className="flex items-center gap-1.5 h-7 px-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded transition-colors duration-100 w-full text-left disabled:opacity-50"
className="flex h-7 w-full items-center gap-1.5 rounded px-1.5 text-left text-xs text-muted-foreground transition-colors duration-100 hover:bg-accent/50 hover:text-foreground disabled:opacity-50"
>
<LuPuzzle className="size-3 shrink-0" />
<span className="truncate flex-1" title={label}>
<span className="flex-1 truncate" title={label}>
{label}
</span>
<LuChevronDown className="size-3 shrink-0 text-muted-foreground" />
@@ -460,7 +460,7 @@ function DnsCell({
type="button"
data-onborda="dns-blocklist"
disabled={isSaving}
className="flex items-center gap-1.5 h-7 px-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50 rounded transition-colors duration-100 w-full text-left disabled:opacity-50"
className="flex h-7 w-full items-center gap-1.5 rounded px-1.5 text-left text-xs text-muted-foreground transition-colors duration-100 hover:bg-accent/50 hover:text-foreground disabled:opacity-50"
title={
level
? meta.t("profiles.table.dnsLevel", { level })
@@ -681,9 +681,9 @@ const TagsCell = React.memo<{
type="button"
ref={containerRef as unknown as React.RefObject<HTMLButtonElement>}
className={cn(
"flex overflow-hidden gap-1 items-center px-2 py-1 h-6 w-full bg-transparent rounded border-none cursor-pointer",
"flex h-6 w-full cursor-pointer items-center gap-1 overflow-hidden rounded border-none bg-transparent px-2 py-1",
isDisabled
? "opacity-60 cursor-not-allowed"
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
)}
onClick={() => {
@@ -709,7 +709,7 @@ const TagsCell = React.memo<{
);
return (
<div className="w-full h-6 cursor-pointer">
<div className="h-6 w-full cursor-pointer">
<Tooltip>
<TooltipTrigger asChild>{ButtonContent}</TooltipTrigger>
{hiddenCount > 0 && (
@@ -735,13 +735,13 @@ const TagsCell = React.memo<{
return (
<div
className={cn(
"w-full h-6 relative",
isDisabled && "opacity-60 pointer-events-none",
"relative h-6 w-full",
isDisabled && "pointer-events-none opacity-60",
)}
>
<div
ref={editorRef}
className="absolute top-0 left-0 z-50 w-40 min-h-6 bg-popover rounded-md shadow-md"
className="absolute top-0 left-0 z-50 min-h-6 w-40 rounded-md bg-popover shadow-md"
>
<MultipleSelector
value={valueOptions}
@@ -755,11 +755,11 @@ const TagsCell = React.memo<{
: ""
}
className={cn(
"bg-transparent border-0! focus-within:ring-0!",
"border-0! bg-transparent focus-within:ring-0!",
"[&_div:first-child]:border-0! [&_div:first-child]:ring-0! [&_div:first-child]:focus-within:ring-0!",
"[&_div:first-child]:min-h-6! [&_div:first-child]:px-2! [&_div:first-child]:py-1!",
"[&_div:first-child>div]:items-center [&_div:first-child>div]:h-6!",
"[&_input]:ml-0! [&_input]:mt-0! [&_input]:px-0!",
"[&_div:first-child>div]:h-6! [&_div:first-child>div]:items-center",
"[&_input]:mt-0! [&_input]:ml-0! [&_input]:px-0!",
!isFocused && "[&_div:first-child>div]:justify-center",
)}
badgeClassName="shrink-0"
@@ -859,7 +859,7 @@ const OverflowTooltipText = React.memo<{
<TooltipTrigger asChild>
<span
ref={textRef}
className={cn("block min-w-0 max-w-full truncate", className)}
className={cn("block max-w-full min-w-0 truncate", className)}
>
{text}
</span>
@@ -894,16 +894,16 @@ const ProxyCellTrigger = React.memo<{
<PopoverTrigger asChild>
<span
className={cn(
"flex gap-2 items-center px-2 py-1 rounded min-w-0 max-w-full",
"flex max-w-full min-w-0 items-center gap-2 rounded px-2 py-1",
isDisabled
? "opacity-60 cursor-not-allowed pointer-events-none"
? "pointer-events-none cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
)}
>
{vpnBadge && (
<Badge
variant="outline"
className="text-[10px] px-1 py-0 leading-tight shrink-0"
className="shrink-0 px-1 py-0 text-[10px] leading-tight"
>
{vpnBadge}
</Badge>
@@ -911,7 +911,7 @@ const ProxyCellTrigger = React.memo<{
<span
ref={textRef}
className={cn(
"text-sm min-w-0 truncate",
"min-w-0 truncate text-sm",
!hasAssignment && "text-muted-foreground",
)}
>
@@ -1037,15 +1037,15 @@ const NoteCell = React.memo<{
if (openNoteEditorFor !== profile.id) {
return (
<div className="w-full min-h-6">
<div className="min-h-6 w-full">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className={cn(
"flex items-center px-2 py-1 min-h-6 w-full min-w-0 bg-transparent rounded border-none text-left",
"flex min-h-6 w-full min-w-0 items-center rounded border-none bg-transparent px-2 py-1 text-left",
isDisabled
? "opacity-60 cursor-not-allowed"
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
)}
onClick={() => {
@@ -1057,7 +1057,7 @@ const NoteCell = React.memo<{
>
<span
className={cn(
"text-sm truncate block w-full",
"block w-full truncate text-sm",
!effectiveNote && "text-muted-foreground",
)}
>
@@ -1067,7 +1067,7 @@ const NoteCell = React.memo<{
</TooltipTrigger>
{showTooltip && (
<TooltipContent className="max-w-[320px]">
<p className="whitespace-pre-wrap wrap-break-word">
<p className="wrap-break-word whitespace-pre-wrap">
{effectiveNote ?? t("profiles.note.empty")}
</p>
</TooltipContent>
@@ -1080,13 +1080,13 @@ const NoteCell = React.memo<{
return (
<div
className={cn(
"w-full relative",
isDisabled && "opacity-60 pointer-events-none",
"relative w-full",
isDisabled && "pointer-events-none opacity-60",
)}
>
<div
ref={editorRef}
className="absolute -top-[15px] -left-px z-50 w-60 min-h-6 bg-popover rounded-md shadow-md border"
className="absolute top-[-15px] -left-px z-50 min-h-6 w-60 rounded-md border bg-popover shadow-md"
>
<textarea
ref={textareaRef}
@@ -1106,7 +1106,7 @@ const NoteCell = React.memo<{
setOpenNoteEditorFor(null);
}}
placeholder={t("profiles.note.placeholder")}
className="w-full min-h-6 max-h-[200px] px-2 py-1 text-sm bg-transparent border-0 resize-none focus:outline-none focus:ring-0"
className="max-h-[200px] min-h-6 w-full resize-none border-0 bg-transparent px-2 py-1 text-sm focus:ring-0 focus:outline-none"
style={{
overflow: "auto",
}}
@@ -2103,18 +2103,18 @@ export function ProfilesDataTable({
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center size-4">
<span className="flex size-4 items-center justify-center">
<button
type="button"
className="flex justify-center items-center p-0 border-none cursor-pointer"
className="flex cursor-pointer items-center justify-center border-none p-0"
onClick={() => {
meta.handleIconClick(profile.id);
}}
aria-label={t("common.aria.selectProfile")}
>
<span className="size-4 group">
<span className="group size-4">
<OsIcon className="size-4 text-muted-foreground group-hover:hidden" />
<span className="peer border-input dark:bg-input/30 dark:data-[state=checked]:bg-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none size-4 hidden group-hover:block pointer-events-none items-center justify-center duration-150" />
<span className="peer pointer-events-none hidden size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-xs transition-shadow duration-150 outline-none group-hover:block dark:bg-input/30 dark:data-[state=checked]:bg-primary" />
</span>
</button>
</span>
@@ -2142,7 +2142,7 @@ export function ProfilesDataTable({
sideOffset={4}
horizontalOffset={8}
>
<span className="flex justify-center items-center size-4">
<span className="flex size-4 items-center justify-center">
<Checkbox
checked={isSelected}
onCheckedChange={(value) => {
@@ -2168,7 +2168,7 @@ export function ProfilesDataTable({
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center size-4 cursor-not-allowed">
<span className="flex size-4 cursor-not-allowed items-center justify-center">
{IconComponent && (
<IconComponent className="size-4 opacity-50" />
)}
@@ -2190,7 +2190,7 @@ export function ProfilesDataTable({
sideOffset={4}
horizontalOffset={8}
>
<span className="flex justify-center items-center size-4">
<span className="flex size-4 items-center justify-center">
<Checkbox
checked={isSelected}
onCheckedChange={(value) => {
@@ -2210,20 +2210,20 @@ export function ProfilesDataTable({
sideOffset={4}
horizontalOffset={8}
>
<span className="flex relative justify-center items-center size-4">
<span className="relative flex size-4 items-center justify-center">
<button
type="button"
className="flex justify-center items-center p-0 border-none cursor-pointer"
className="flex cursor-pointer items-center justify-center border-none p-0"
onClick={() => {
meta.handleIconClick(profile.id);
}}
aria-label={t("common.aria.selectProfile")}
>
<span className="size-4 group">
<span className="group size-4">
{IconComponent && (
<IconComponent className="size-4 group-hover:hidden" />
)}
<span className="peer border-input dark:bg-input/30 dark:data-[state=checked]:bg-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none size-4 hidden group-hover:block pointer-events-none items-center justify-center duration-150" />
<span className="peer pointer-events-none hidden size-4 shrink-0 items-center justify-center rounded-[4px] border border-input shadow-xs transition-shadow duration-150 outline-none group-hover:block dark:bg-input/30 dark:data-[state=checked]:bg-primary" />
</span>
</button>
</span>
@@ -2332,7 +2332,7 @@ export function ProfilesDataTable({
: "default";
return (
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
{isDesynced && (
<Tooltip>
<TooltipTrigger asChild>
@@ -2360,8 +2360,8 @@ export function ProfilesDataTable({
: meta.t("profiles.actions.launch")
}
className={cn(
"size-7 p-0 grid place-items-center",
!canLaunch && "opacity-50 cursor-not-allowed",
"grid size-7 place-items-center p-0",
!canLaunch && "cursor-not-allowed opacity-50",
canLaunch && "cursor-pointer",
isFollower && "border-accent",
isRunning &&
@@ -2374,7 +2374,7 @@ export function ProfilesDataTable({
}
>
{isLaunching || isStopping ? (
<div className="size-3 rounded-full border border-current animate-spin border-t-transparent" />
<div className="size-3 animate-spin rounded-full border border-current border-t-transparent" />
) : isRunning ? (
<LuSquare className="size-3.5 fill-current" />
) : (
@@ -2423,7 +2423,7 @@ export function ProfilesDataTable({
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
className="h-auto cursor-pointer justify-start p-0 text-left font-semibold"
>
{meta.t("common.labels.name")}
{isActive("name", false) ? (
@@ -2491,7 +2491,7 @@ export function ProfilesDataTable({
return (
<div
ref={renameContainerRef}
className="overflow-visible relative"
className="relative overflow-visible"
>
<Input
autoFocus
@@ -2523,7 +2523,7 @@ export function ProfilesDataTable({
meta.setRenameError(null);
}
}}
className="w-full min-w-0 max-w-full h-6 px-2 py-1 text-sm font-medium leading-none border-0 shadow-none focus-visible:ring-0"
className="h-6 w-full max-w-full min-w-0 border-0 px-2 py-1 text-sm leading-none font-medium shadow-none focus-visible:ring-0"
/>
</div>
);
@@ -2532,7 +2532,7 @@ export function ProfilesDataTable({
const display = (
<OverflowTooltipText
text={name}
className="font-medium text-left leading-none"
className="text-left leading-none font-medium"
/>
);
@@ -2548,13 +2548,13 @@ export function ProfilesDataTable({
const isLocked = meta.isProfileLockedByAnother(profile.id);
return (
<div className="flex items-center gap-1.5 min-w-0 max-w-full overflow-hidden">
<div className="flex max-w-full min-w-0 items-center gap-1.5 overflow-hidden">
<button
type="button"
className={cn(
"px-2 py-1 mr-auto text-left bg-transparent rounded border-none h-6 min-w-0 max-w-full overflow-hidden",
"mr-auto h-6 max-w-full min-w-0 overflow-hidden rounded border-none bg-transparent px-2 py-1 text-left",
isDisabled
? "opacity-60 cursor-not-allowed"
? "cursor-not-allowed opacity-60"
: "cursor-pointer hover:bg-accent/50",
)}
onClick={() => {
@@ -2715,7 +2715,7 @@ export function ProfilesDataTable({
(snapshot?.current_bytes_received ?? 0);
return (
<div className="overflow-hidden min-w-0">
<div className="min-w-0 overflow-hidden">
<BandwidthMiniChart
key={`${profile.id}-${snapshot?.last_update ?? 0}-${bandwidthData.length}`}
data={bandwidthData}
@@ -2727,7 +2727,7 @@ export function ProfilesDataTable({
}
return (
<div className="flex overflow-hidden gap-2 items-center min-w-0">
<div className="flex min-w-0 items-center gap-2 overflow-hidden">
<Popover
open={isSelectorOpen}
onOpenChange={(open) => {
@@ -2833,7 +2833,7 @@ export function ProfilesDataTable({
/>
<Badge
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
className="mr-1 px-1 py-0 text-[10px] leading-tight"
>
WG
</Badge>
@@ -2956,7 +2956,7 @@ export function ProfilesDataTable({
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center h-9 w-full">
<span className="flex h-9 w-full items-center justify-center">
{dot.encrypted ? (
<LuLock
className={`size-3 ${dot.color.replace("bg-", "text-")}${dot.animate ? " animate-pulse" : ""}`}
@@ -2981,10 +2981,10 @@ export function ProfilesDataTable({
const profile = row.original;
return (
<div className="flex justify-end items-center h-9 w-full">
<div className="flex h-9 w-full items-center justify-end">
<Button
variant="ghost"
className="p-0 size-7"
className="size-7 p-0"
disabled={!meta.isClient}
onClick={() => {
setProfileForInfoDialog(profile);
@@ -3108,11 +3108,11 @@ export function ProfilesDataTable({
return (
<>
<div className="relative flex-1 min-h-0 flex flex-col">
<div className="relative flex min-h-0 flex-1 flex-col">
<div
ref={scrollParentRef}
className={cn(
"overflow-auto relative flex-1 min-h-0 scroll-fade",
"scroll-fade relative min-h-0 flex-1 overflow-auto",
// Clearance for the floating selection action bar (bottom-6 +
// ~46px tall) so the last rows can scroll out from behind it.
// Same predicate DataTableActionBar uses for its visibility.
@@ -3128,11 +3128,11 @@ export function ProfilesDataTable({
}
>
<Table className="table-fixed" containerClassName="overflow-visible">
<TableHeader className="overflow-visible sticky top-0 z-10 bg-background [&_tr]:border-0">
<TableHeader className="sticky top-0 z-10 overflow-visible bg-background [&_tr]:border-0">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow
key={headerGroup.id}
className="overflow-visible !border-0"
className="overflow-visible border-0!"
>
{headerGroup.headers.map((header) => {
return (
@@ -3196,7 +3196,7 @@ export function ProfilesDataTable({
title={crossOsTitle}
style={{ height: `${ROW_HEIGHT}px` }}
className={cn(
"overflow-visible hover:bg-accent/50 !border-0",
"overflow-visible border-0! hover:bg-accent/50",
rowIsCrossOs && "opacity-60",
)}
>
@@ -3320,7 +3320,7 @@ export function ProfilesDataTable({
<LuPlay className="fill-current" />
</DataTableActionBarAction>
{!bulkActionsUnlocked && (
<ProBadge className="absolute -top-2 -right-2 pointer-events-none" />
<ProBadge className="pointer-events-none absolute -top-2 -right-2" />
)}
</span>
)}
@@ -3339,7 +3339,7 @@ export function ProfilesDataTable({
<LuSquare className="fill-current" />
</DataTableActionBarAction>
{!bulkActionsUnlocked && (
<ProBadge className="absolute -top-2 -right-2 pointer-events-none" />
<ProBadge className="pointer-events-none absolute -top-2 -right-2" />
)}
</span>
)}
+69 -69
View File
@@ -116,9 +116,9 @@ function _OSIcon({ os }: { os: string }) {
function InfoCard({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-md bg-muted/50 border px-3 py-2.5">
<div className="rounded-md border bg-muted/50 px-3 py-2.5">
<p className="text-xs text-muted-foreground">{label}</p>
<p className="text-sm mt-0.5 truncate">{value}</p>
<p className="mt-0.5 truncate text-sm">{value}</p>
</div>
);
}
@@ -503,7 +503,7 @@ export function ProfileInfoDialog({
>
<DialogContent
hideClose
className="max-w-[min(60rem,calc(100%-4rem))] h-[min(clamp(30rem,80vh,48rem),calc(100vh-3rem))] flex flex-col p-0 gap-0 overflow-hidden"
className="flex h-[min(clamp(30rem,80vh,48rem),calc(100vh-3rem))] max-w-[min(60rem,calc(100%-4rem))] flex-col gap-0 overflow-hidden p-0"
>
{/* The dialog renders its own custom header, so the accessible title is
visually hidden but present for screen readers (Radix requires it). */}
@@ -720,20 +720,20 @@ function ProfileInfoLayout({
return (
<>
{/* Top bar */}
<div className="flex items-center gap-2 h-11 px-3 border-b border-border shrink-0">
<LuUsers className="size-3.5 text-muted-foreground shrink-0" />
<div className="flex items-center gap-1.5 text-xs min-w-0 flex-1">
<div className="flex h-11 shrink-0 items-center gap-2 border-b border-border px-3">
<LuUsers className="size-3.5 shrink-0 text-muted-foreground" />
<div className="flex min-w-0 flex-1 items-center gap-1.5 text-xs">
<span className="font-semibold">
{t("profileInfo.breadcrumbRoot")}
</span>
<span className="text-muted-foreground">/</span>
<span className="text-muted-foreground truncate">{profile.name}</span>
<span className="truncate text-muted-foreground">{profile.name}</span>
</div>
{onCloneProfile && (
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs gap-1.5"
className="h-7 gap-1.5 px-2 text-xs"
disabled={isDisabled}
onClick={() => onCloneProfile(profile)}
>
@@ -745,16 +745,16 @@ function ProfileInfoLayout({
type="button"
aria-label={t("common.buttons.close")}
onClick={onClose}
className="grid place-items-center size-7 rounded-md text-muted-foreground hover:text-foreground hover:bg-accent/50 transition-colors duration-100"
className="grid size-7 place-items-center rounded-md text-muted-foreground transition-colors duration-100 hover:bg-accent/50 hover:text-foreground"
>
<LuX className="size-3.5" />
</button>
</div>
{/* Body */}
<div className="flex flex-1 min-h-0">
<div className="flex min-h-0 flex-1">
{/* Sidebar */}
<nav className="w-44 shrink-0 border-r border-border p-2 flex flex-col gap-0.5 overflow-y-auto">
<nav className="flex w-44 shrink-0 flex-col gap-0.5 overflow-y-auto border-r border-border p-2">
{sidebarItems
.filter((it) => !it.hidden)
.map((it) => {
@@ -765,16 +765,16 @@ function ProfileInfoLayout({
type="button"
onClick={() => setSection(it.id)}
className={cn(
"flex items-center gap-2 h-7 px-2 rounded-md text-xs transition-colors duration-100 text-left",
"flex h-7 items-center gap-2 rounded-md px-2 text-left text-xs transition-colors duration-100",
active
? "bg-accent text-accent-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-accent/50",
: "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
<span className="shrink-0">{it.icon}</span>
<span className="flex-1 truncate">{it.label}</span>
{it.badge && (
<span className="text-[9px] uppercase text-muted-foreground tracking-wide truncate max-w-[60px]">
<span className="max-w-[60px] truncate text-[9px] tracking-wide text-muted-foreground uppercase">
{it.badge}
</span>
)}
@@ -788,7 +788,7 @@ function ProfileInfoLayout({
type="button"
onClick={deleteAction.onClick}
disabled={deleteAction.disabled}
className="flex items-center gap-2 h-7 px-2 rounded-md text-xs transition-colors duration-100 text-destructive hover:bg-destructive/10 disabled:opacity-50 disabled:pointer-events-none"
className="flex h-7 items-center gap-2 rounded-md px-2 text-xs text-destructive transition-colors duration-100 hover:bg-destructive/10 disabled:pointer-events-none disabled:opacity-50"
>
<LuTrash2 className="size-3.5 shrink-0" />
<span className="flex-1 text-left">
@@ -800,21 +800,21 @@ function ProfileInfoLayout({
</nav>
{/* Main */}
<div className="flex-1 min-w-0 overflow-y-auto scroll-fade p-4">
<div className="scroll-fade min-w-0 flex-1 overflow-y-auto p-4">
{section === "overview" && (
<div className="flex flex-col gap-3">
{/* Hero */}
<div className="flex items-center gap-3">
<div className="rounded-lg bg-muted p-2.5 shrink-0">
<div className="shrink-0 rounded-lg bg-muted p-2.5">
<ProfileIcon className="size-7 text-foreground" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<h3 className="text-base font-semibold truncate">
<h3 className="truncate text-base font-semibold">
{profile.name}
</h3>
</div>
<div className="flex flex-wrap items-center gap-1.5 mt-1 text-[11px]">
<div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px]">
<span className="font-mono text-muted-foreground">
{profile.version}
</span>
@@ -823,17 +823,17 @@ function ProfileInfoLayout({
</div>
{/* ID */}
<div className="flex items-center gap-2 rounded-md bg-muted/40 px-3 py-2 border border-border">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground shrink-0">
<div className="flex items-center gap-2 rounded-md border border-border bg-muted/40 px-3 py-2">
<span className="shrink-0 text-[10px] tracking-wide text-muted-foreground uppercase">
ID
</span>
<span className="font-mono text-xs truncate flex-1">
<span className="flex-1 truncate font-mono text-xs">
{profile.id}
</span>
<button
type="button"
onClick={() => void handleCopyId()}
className="text-muted-foreground hover:text-foreground transition-colors shrink-0"
className="shrink-0 text-muted-foreground transition-colors hover:text-foreground"
aria-label={t("common.buttons.copy")}
>
{copied ? (
@@ -874,7 +874,7 @@ function ProfileInfoLayout({
{/* Activity */}
<div className="mt-1 flex flex-col gap-1.5">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground">
<span className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.sections.activity")}
</span>
<div className="grid grid-cols-2 gap-2">
@@ -904,11 +904,11 @@ function ProfileInfoLayout({
</div>
{profile.created_by_email && (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("sync.team.title")}
</p>
<p className="text-sm mt-0.5">
<p className="mt-0.5 text-sm">
{t("sync.team.createdBy", {
email: profile.created_by_email,
})}
@@ -1014,7 +1014,7 @@ function _SectionPlaceholder({
</div>
<p className="text-xs text-muted-foreground">{description}</p>
{hint && (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2 text-xs">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2 text-xs">
{hint}
</div>
)}
@@ -1022,7 +1022,7 @@ function _SectionPlaceholder({
size="sm"
onClick={onAction}
disabled={disabled}
className="self-start h-7 text-xs"
className="h-7 self-start text-xs"
>
{actionLabel}
</Button>
@@ -1049,11 +1049,11 @@ function _SectionAction({
disabled={disabled}
onClick={onClick}
className={cn(
"flex items-center gap-2 h-9 px-3 rounded-md text-xs transition-colors text-left",
"flex h-9 items-center gap-2 rounded-md px-3 text-left text-xs transition-colors",
destructive
? "text-destructive hover:bg-destructive/10"
: "hover:bg-accent",
"disabled:opacity-50 disabled:pointer-events-none",
"disabled:pointer-events-none disabled:opacity-50",
)}
>
{icon}
@@ -1121,7 +1121,7 @@ function LaunchHookEditor({
setValue(e.target.value);
}}
placeholder={t("profiles.launchHook.placeholder")}
className="text-xs font-mono"
className="font-mono text-xs"
/>
{showInvalidHint && (
<p className="text-xs text-warning">
@@ -1199,7 +1199,7 @@ function SyncSectionInline({
{t("profileInfo.sectionDesc.sync")}
</p>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground shrink-0">
<span className="shrink-0 text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.fields.syncMode")}
</span>
<Select
@@ -1209,7 +1209,7 @@ function SyncSectionInline({
void onChangeMode(v);
}}
>
<SelectTrigger className="h-7 text-xs flex-1">
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1222,15 +1222,15 @@ function SyncSectionInline({
</Select>
</div>
{syncStatus && (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.fields.syncStatus")}
</p>
<p className="text-sm mt-0.5">
<p className="mt-0.5 text-sm">
{t(`profileInfo.syncStatusValue.${syncStatus.status}`)}
</p>
{syncStatus.error && (
<p className="text-xs text-destructive mt-1">{syncStatus.error}</p>
<p className="mt-1 text-xs text-destructive">{syncStatus.error}</p>
)}
</div>
)}
@@ -1317,7 +1317,7 @@ function NetworkSectionInline({
</p>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground shrink-0 w-12">
<span className="w-12 shrink-0 text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.fields.proxy")}
</span>
<Select
@@ -1327,7 +1327,7 @@ function NetworkSectionInline({
void onProxyChange(v);
}}
>
<SelectTrigger className="h-7 text-xs flex-1">
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1344,7 +1344,7 @@ function NetworkSectionInline({
</div>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground shrink-0 w-12">
<span className="w-12 shrink-0 text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.fields.vpn")}
</span>
<Select
@@ -1354,7 +1354,7 @@ function NetworkSectionInline({
void onVpnChange(v);
}}
>
<SelectTrigger className="h-7 text-xs flex-1">
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1449,7 +1449,7 @@ function ExtensionsSectionInline({
{t("profileInfo.sectionDesc.extensions")}
</p>
<div className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-wide text-muted-foreground shrink-0 w-16">
<span className="w-16 shrink-0 text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.fields.extensionGroup")}
</span>
<Select
@@ -1459,7 +1459,7 @@ function ExtensionsSectionInline({
void onChange(v);
}}
>
<SelectTrigger className="h-7 text-xs flex-1">
<SelectTrigger className="h-7 flex-1 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -1569,7 +1569,7 @@ function CookiesSectionInline({
const domains = stats?.domains ?? [];
return (
<div className="flex flex-col gap-3 min-h-0 flex-1">
<div className="flex min-h-0 flex-1 flex-col gap-3">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 text-sm font-semibold">
<LuCookie className="size-4" />
@@ -1641,18 +1641,18 @@ function CookiesSectionInline({
{t("profileInfo.sectionDesc.cookies")}
</p>
{isRunning ? (
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-xs text-muted-foreground">
{t("profileInfo.cookies.runningNotice")}
</p>
</div>
) : (
<>
<div className="rounded-md bg-muted/40 border border-border px-3 py-2">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground">
<div className="rounded-md border border-border bg-muted/40 px-3 py-2">
<p className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.fields.cookieCount")}
</p>
<p className="text-sm mt-0.5">
<p className="mt-0.5 text-sm">
{isLoading
? t("profileInfo.values.loading")
: stats
@@ -1661,13 +1661,13 @@ function CookiesSectionInline({
</p>
</div>
{domains.length > 0 && (
<div className="rounded-md bg-muted/40 border border-border flex flex-col min-h-0 flex-1 overflow-hidden">
<p className="text-[10px] uppercase tracking-wide text-muted-foreground px-3 py-2 border-b border-border shrink-0">
<div className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border bg-muted/40">
<p className="shrink-0 border-b border-border px-3 py-2 text-[10px] tracking-wide text-muted-foreground uppercase">
{t("profileInfo.cookies.domainsHeader", {
count: domains.length,
})}
</p>
<ul className="text-xs px-3 py-2 overflow-y-auto flex-1 space-y-1">
<ul className="flex-1 space-y-1 overflow-y-auto px-3 py-2 text-xs">
{domains.map((d) => (
<li
key={d.domain}
@@ -1842,7 +1842,7 @@ function FingerprintSectionInline({
{error && <p className="text-xs text-destructive">{error}</p>}
{success && !error && <p className="text-xs text-success">{success}</p>}
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border">
<div className="mt-3 flex items-center gap-2 border-t border-border pt-3">
<Button
size="sm"
className="h-7 text-xs"
@@ -2013,8 +2013,8 @@ function SecuritySectionInline({
setIsVerifyOpen(true);
}}
className={cn(
"flex-1 h-7 px-2 text-xs rounded-md border transition-colors",
"border-border text-muted-foreground hover:text-foreground hover:bg-accent/50",
"h-7 flex-1 rounded-md border px-2 text-xs transition-colors",
"border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
{t("profilePassword.modes.validate")}
@@ -2026,10 +2026,10 @@ function SecuritySectionInline({
reset();
}}
className={cn(
"flex-1 h-7 px-2 text-xs rounded-md border transition-colors",
"h-7 flex-1 rounded-md border px-2 text-xs transition-colors",
mode === "change"
? "bg-accent text-accent-foreground border-transparent"
: "border-border text-muted-foreground hover:text-foreground hover:bg-accent/50",
? "border-transparent bg-accent text-accent-foreground"
: "border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
{t("profilePassword.modes.change")}
@@ -2041,10 +2041,10 @@ function SecuritySectionInline({
reset();
}}
className={cn(
"flex-1 h-7 px-2 text-xs rounded-md border transition-colors",
"h-7 flex-1 rounded-md border px-2 text-xs transition-colors",
mode === "remove"
? "bg-destructive/10 text-destructive border-transparent"
: "border-border text-muted-foreground hover:text-foreground hover:bg-accent/50",
? "border-transparent bg-destructive/10 text-destructive"
: "border-border text-muted-foreground hover:bg-accent/50 hover:text-foreground",
)}
>
{t("profilePassword.modes.remove")}
@@ -2106,7 +2106,7 @@ function SecuritySectionInline({
<Button
size="sm"
variant={mode === "remove" ? "destructive" : "default"}
className="self-start h-7 text-xs"
className="h-7 self-start text-xs"
disabled={isRunning || isSubmitting}
onClick={() => {
void onSubmit();
@@ -2397,11 +2397,11 @@ export function ProfileBypassRulesDialog({
if (!open) onClose();
}}
>
<DialogContent className="sm:max-w-lg max-h-[80vh] flex flex-col">
<DialogContent className="flex max-h-[80vh] flex-col sm:max-w-lg">
<DialogHeader className="shrink-0">
<DialogTitle>{t("profileInfo.network.bypassRulesTitle")}</DialogTitle>
</DialogHeader>
<ScrollArea className="flex-1 min-h-0">
<ScrollArea className="min-h-0 flex-1">
<div className="flex flex-col gap-3 py-2">
<p className="text-sm text-muted-foreground">
{t("profileInfo.network.bypassRulesDescription")}
@@ -2423,12 +2423,12 @@ export function ProfileBypassRulesDialog({
onClick={handleAddRule}
disabled={!newRule.trim()}
>
<LuPlus className="size-4 mr-1" />
<LuPlus className="mr-1 size-4" />
{t("profileInfo.network.addRule")}
</Button>
</div>
{bypassRules.length === 0 ? (
<p className="text-sm text-muted-foreground py-2">
<p className="py-2 text-sm text-muted-foreground">
{t("profileInfo.network.noRules")}
</p>
) : (
@@ -2436,15 +2436,15 @@ export function ProfileBypassRulesDialog({
{bypassRules.map((rule) => (
<div
key={rule}
className="flex items-center justify-between gap-2 px-3 py-1.5 rounded-md bg-muted text-sm"
className="flex items-center justify-between gap-2 rounded-md bg-muted px-3 py-1.5 text-sm"
>
<span className="font-mono text-xs truncate">{rule}</span>
<span className="truncate font-mono text-xs">{rule}</span>
<button
type="button"
onClick={() => {
handleRemoveRule(rule);
}}
className="text-muted-foreground hover:text-destructive transition-colors shrink-0"
className="shrink-0 text-muted-foreground transition-colors hover:text-destructive"
>
<LuX className="size-3.5" />
</button>
+4 -4
View File
@@ -171,7 +171,7 @@ export function ProfileSelectorDialog({
<div className="grid gap-4 py-4">
{url && (
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<Label className="text-sm font-medium">
{t("profileSelector.openingUrl")}
</Label>
@@ -180,7 +180,7 @@ export function ProfileSelectorDialog({
successMessage={t("profileSelector.urlCopied")}
/>
</div>
<div className="p-2 text-sm break-all rounded bg-muted max-h-24 overflow-y-auto">
<div className="max-h-24 overflow-y-auto rounded bg-muted p-2 text-sm break-all">
{url}
</div>
</div>
@@ -230,8 +230,8 @@ export function ProfileSelectorDialog({
!canUseForLinks ? "opacity-50" : ""
}`}
>
<div className="flex gap-3 items-center px-2 py-1 rounded-lg">
<div className="flex gap-2 items-center">
<div className="flex items-center gap-3 rounded-lg px-2 py-1">
<div className="flex items-center gap-2">
{(() => {
const IconComponent = getBrowserIcon(
profile.browser,
+6 -6
View File
@@ -172,7 +172,7 @@ export function ProfileSyncDialog({
return (
<Dialog open={isOpen} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md flex flex-col overflow-hidden">
<DialogContent className="flex max-w-md flex-col overflow-hidden">
<DialogHeader className="shrink-0">
<DialogTitle>{t("sync.mode.title")}</DialogTitle>
<DialogDescription>
@@ -183,15 +183,15 @@ export function ProfileSyncDialog({
</DialogDescription>
</DialogHeader>
<div className="flex-1 min-h-0 overflow-y-auto">
<div className="min-h-0 flex-1 overflow-y-auto">
{isCheckingConfig ? (
<div className="flex justify-center py-8">
<div className="size-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
<div className="size-6 animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
{!hasConfig && (
<div className="p-3 text-sm rounded-md bg-muted">
<div className="rounded-md bg-muted p-3 text-sm">
<p className="mb-2">{t("sync.mode.notConfigured")}</p>
<Button
variant="outline"
@@ -267,14 +267,14 @@ export function ProfileSyncDialog({
{syncMode === "Encrypted" &&
!hasE2ePassword &&
userChangedMode && (
<div className="p-3 text-sm rounded-md bg-destructive/10 text-destructive">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{t("sync.mode.noPasswordWarning")}
</div>
)}
<div className="space-y-2">
<Label>{t("sync.mode.lastSynced")}</Label>
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<Badge variant="outline">
{formatLastSync(profile.last_sync)}
</Badge>
+5 -5
View File
@@ -157,8 +157,8 @@ export function ProxyAssignmentDialog({
<div className="space-y-4">
<div className="space-y-2">
<Label>{t("proxyAssignment.selectedProfilesLabel")}</Label>
<div className="p-3 bg-muted rounded-md max-h-[min(8rem,20vh)] overflow-y-auto">
<ul className="text-sm space-y-1">
<div className="max-h-[min(8rem,20vh)] overflow-y-auto rounded-md bg-muted p-3">
<ul className="space-y-1 text-sm">
{selectedProfiles.map((profileId) => {
const profile = profiles.find(
(p: BrowserProfile) => p.id === profileId,
@@ -206,7 +206,7 @@ export function ProxyAssignmentDialog({
</PopoverTrigger>
<PopoverContent
id={proxyListboxId}
className="w-[var(--radix-popover-trigger-width)] p-0"
className="w-(--radix-popover-trigger-width) p-0"
sideOffset={8}
>
<Command>
@@ -283,7 +283,7 @@ export function ProxyAssignmentDialog({
/>
<Badge
variant="outline"
className="text-[10px] px-1 py-0 leading-tight mr-1"
className="mr-1 px-1 py-0 text-[10px] leading-tight"
>
WG
</Badge>
@@ -299,7 +299,7 @@ export function ProxyAssignmentDialog({
</div>
{error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
<div className="rounded-md bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
)}
+3 -3
View File
@@ -123,14 +123,14 @@ export function ProxyCheckButton({
disabled={isCurrentlyChecking || disabled}
>
{isCurrentlyChecking ? (
<div className="size-3 rounded-full border border-current animate-spin border-t-transparent" />
<div className="size-3 animate-spin rounded-full border border-current border-t-transparent" />
) : result?.is_valid && result.country_code ? (
<span className="relative inline-flex items-center justify-center">
<FlagIcon countryCode={result.country_code} className="h-2.5" />
<FiCheck className="absolute bottom-[-6px] right-[-4px]" />
<FiCheck className="absolute right-[-4px] bottom-[-6px]" />
</span>
) : result && !result.is_valid ? (
<span className="text-destructive text-sm"></span>
<span className="text-sm text-destructive"></span>
) : (
<FiCheck className="size-3" />
)}
+7 -7
View File
@@ -125,17 +125,17 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) {
<div className="space-y-2">
<Label>{t("proxies.exportDialog.preview")}</Label>
<ScrollArea className="h-[clamp(120px,30vh,400px)] border rounded-md bg-muted/30">
<ScrollArea className="h-[clamp(120px,30vh,400px)] rounded-md border bg-muted/30">
{isLoading ? (
<div className="flex items-center justify-center h-full p-4 text-sm text-muted-foreground">
<div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
{t("common.buttons.loading")}
</div>
) : exportContent ? (
<pre className="p-3 text-xs font-mono whitespace-pre-wrap break-all">
<pre className="p-3 font-mono text-xs break-all whitespace-pre-wrap">
{exportContent}
</pre>
) : (
<div className="flex items-center justify-center h-full p-4 text-sm text-muted-foreground">
<div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
{t("proxies.exportDialog.noProxies")}
</div>
)}
@@ -143,7 +143,7 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) {
</div>
</div>
<DialogFooter className="flex-col sm:flex-row gap-2">
<DialogFooter className="flex-col gap-2 sm:flex-row">
<RippleButton variant="outline" onClick={handleClose}>
{t("common.buttons.close")}
</RippleButton>
@@ -151,7 +151,7 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) {
variant="outline"
onClick={() => void handleCopyToClipboard()}
disabled={!exportContent || isLoading}
className="flex gap-2 items-center"
className="flex items-center gap-2"
>
{copied ? (
<LuCheck className="size-4" />
@@ -165,7 +165,7 @@ export function ProxyExportDialog({ isOpen, onClose }: ProxyExportDialogProps) {
<RippleButton
onClick={handleDownload}
disabled={!exportContent || isLoading}
className="flex gap-2 items-center"
className="flex items-center gap-2"
>
<LuDownload className="size-4" />
{t("common.buttons.download")}
+2 -2
View File
@@ -161,7 +161,7 @@ export function ProxyFormDialog({
</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4 @container">
<div className="@container grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="proxy-name">{t("proxies.form.name")}</Label>
<Input
@@ -231,7 +231,7 @@ export function ProxyFormDialog({
</div>
</div>
<div className="grid grid-cols-1 @sm:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @sm:grid-cols-2">
<div className="grid gap-2">
<Label htmlFor="proxy-username">
{form.proxy_type === "ss"
+15 -15
View File
@@ -315,8 +315,8 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
}
}}
>
<LuUpload className="size-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
<LuUpload className="mb-4 size-10 text-muted-foreground" />
<p className="text-center text-sm text-muted-foreground">
{t("proxies.importDialog.dropzonePrompt")}
<br />
<span className="text-xs">
@@ -335,7 +335,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
}}
/>
</div>
<p className="text-xs text-muted-foreground text-center">
<p className="text-center text-xs text-muted-foreground">
{t("proxies.importDialog.pasteHint", { modKey })}
</p>
</div>
@@ -369,19 +369,19 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
count: parsedProxies.length,
})}
{invalidProxies.length > 0 && (
<span className="text-muted-foreground ml-2">
<span className="ml-2 text-muted-foreground">
{t("proxies.importDialog.invalidCount", {
count: invalidProxies.length,
})}
</span>
)}
</Label>
<ScrollArea className="h-[clamp(120px,30vh,400px)] border rounded-md">
<div className="p-2 space-y-1">
<ScrollArea className="h-[clamp(120px,30vh,400px)] rounded-md border">
<div className="space-y-1 p-2">
{parsedProxies.map((proxy, i) => (
<div
key={`${proxy.original_line}-${i}`}
className="text-xs font-mono p-2 bg-muted/30 rounded break-all"
className="rounded bg-muted/30 p-2 font-mono text-xs break-all"
>
<span className="text-primary">
{proxy.proxy_type}://
@@ -407,21 +407,21 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
<p className="text-sm text-muted-foreground">
{t("proxies.importDialog.ambiguousIntro")}
</p>
<ScrollArea className="h-[clamp(150px,35vh,450px)] border rounded-md">
<div className="p-3 space-y-4">
<ScrollArea className="h-[clamp(150px,35vh,450px)] rounded-md border">
<div className="space-y-4 p-3">
{ambiguousProxies.map((proxy, i) => (
<div
key={`${proxy.line}-${i}`}
className="space-y-2 pb-3 border-b last:border-0"
className="space-y-2 border-b pb-3 last:border-0"
>
<code className="text-xs bg-muted px-2 py-1 rounded block break-all">
<code className="block rounded bg-muted px-2 py-1 text-xs break-all">
{proxy.line}
</code>
<div className="flex flex-col gap-2">
{proxy.possible_formats.map((format) => (
<label
key={format}
className="flex items-center gap-2 cursor-pointer"
className="flex cursor-pointer items-center gap-2"
>
<input
type="radio"
@@ -445,7 +445,7 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
{step === "result" && importResult && (
<div className="space-y-4">
<div className="p-4 bg-muted/30 rounded-lg space-y-2">
<div className="space-y-2 rounded-lg bg-muted/30 p-4">
<div className="flex justify-between">
<span className="text-sm">
{t("proxies.importDialog.imported")}
@@ -479,8 +479,8 @@ export function ProxyImportDialog({ isOpen, onClose }: ProxyImportDialogProps) {
{importResult.errors.length > 0 && (
<div className="space-y-2">
<Label>{t("proxies.importDialog.errors")}</Label>
<ScrollArea className="h-[100px] border rounded-md">
<div className="p-2 space-y-1">
<ScrollArea className="h-[100px] rounded-md border">
<div className="space-y-1 p-2">
{importResult.errors.map((error, i) => (
<div
key={`error-${i}`}
+25 -25
View File
@@ -541,7 +541,7 @@ export function ProxyManagementDialog({
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
className="h-auto cursor-pointer justify-start p-0 text-left font-semibold"
>
{t("common.labels.name")}
{column.getIsSorted() === "asc" ? (
@@ -552,7 +552,7 @@ export function ProxyManagementDialog({
</Button>
),
cell: ({ row }) => (
<span className="font-medium block truncate">
<span className="block truncate font-medium">
{row.original.name}
</span>
),
@@ -563,7 +563,7 @@ export function ProxyManagementDialog({
enableSorting: false,
header: () => t("proxies.management.protocolCol"),
cell: ({ row }) => (
<span className="font-mono text-[10px] uppercase tracking-wider text-muted-foreground">
<span className="font-mono text-[10px] tracking-wider text-muted-foreground uppercase">
{row.original.proxy_settings.proxy_type}
</span>
),
@@ -573,7 +573,7 @@ export function ProxyManagementDialog({
enableSorting: false,
header: () => t("proxies.management.hostPort"),
cell: ({ row }) => (
<span className="font-mono text-xs text-muted-foreground block truncate">
<span className="block truncate font-mono text-xs text-muted-foreground">
{row.original.proxy_settings.host}:
{row.original.proxy_settings.port}
</span>
@@ -774,7 +774,7 @@ export function ProxyManagementDialog({
onClick={() => {
column.toggleSorting(column.getIsSorted() === "asc");
}}
className="justify-start p-0 h-auto font-semibold text-left cursor-pointer"
className="h-auto cursor-pointer justify-start p-0 text-left font-semibold"
>
{t("common.labels.name")}
{column.getIsSorted() === "asc" ? (
@@ -793,7 +793,7 @@ export function ProxyManagementDialog({
vpnSyncErrors[vpn.id],
);
return (
<div className="flex items-center gap-2 font-medium min-w-0">
<div className="flex min-w-0 items-center gap-2 font-medium">
<Tooltip>
<TooltipTrigger asChild>
<div
@@ -1090,7 +1090,7 @@ export function ProxyManagementDialog({
return (
<>
<Dialog open={isOpen} onOpenChange={onClose} subPage={subPage}>
<DialogContent className="max-w-[min(80rem,calc(100%-4rem))] max-h-[85vh] flex flex-col">
<DialogContent className="flex max-h-[85vh] max-w-[min(80rem,calc(100%-4rem))] flex-col">
{!subPage && (
<DialogHeader>
<DialogTitle>{t("proxies.management.title")}</DialogTitle>
@@ -1100,14 +1100,14 @@ export function ProxyManagementDialog({
</DialogHeader>
)}
<div className="@container w-full flex-1 min-h-0 flex flex-col">
<div className="@container flex min-h-0 w-full flex-1 flex-col">
<AnimatedTabs
key={initialTab}
defaultValue={initialTab}
onValueChange={(v) => setActiveTab(v as "proxies" | "vpns")}
className="flex-1 min-h-0 flex flex-col"
className="flex min-h-0 flex-1 flex-col"
>
<div className="flex flex-wrap items-center justify-between gap-2 shrink-0">
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2">
<AnimatedTabsList>
<AnimatedTabsTrigger value="proxies">
<span>{t("proxies.management.tabProxies")}</span>
@@ -1133,7 +1133,7 @@ export function ProxyManagementDialog({
onClick={() => {
setShowImportDialog(true);
}}
className="flex gap-2 items-center"
className="flex items-center gap-2"
aria-label={t("common.buttons.import")}
>
<LuUpload className="size-4" />
@@ -1154,7 +1154,7 @@ export function ProxyManagementDialog({
onClick={() => {
setShowExportDialog(true);
}}
className="flex gap-2 items-center"
className="flex items-center gap-2"
aria-label={t("common.buttons.export")}
disabled={storedProxies.length === 0}
>
@@ -1173,7 +1173,7 @@ export function ProxyManagementDialog({
<RippleButton
size="sm"
onClick={handleCreateProxy}
className="flex gap-2 items-center"
className="flex items-center gap-2"
aria-label={t("proxies.management.newProxy")}
>
<GoPlus className="size-4" />
@@ -1198,7 +1198,7 @@ export function ProxyManagementDialog({
onClick={() => {
setShowVpnImportDialog(true);
}}
className="flex gap-2 items-center"
className="flex items-center gap-2"
aria-label={t("common.buttons.import")}
>
<LuUpload className="size-4" />
@@ -1216,7 +1216,7 @@ export function ProxyManagementDialog({
<RippleButton
size="sm"
onClick={handleCreateVpn}
className="flex gap-2 items-center"
className="flex items-center gap-2"
aria-label={t("proxies.management.newVpn")}
>
<GoPlus className="size-4" />
@@ -1236,9 +1236,9 @@ export function ProxyManagementDialog({
<AnimatedTabsContent
value="proxies"
className="mt-4 flex-1 min-h-0 data-[state=active]:flex flex-col"
className="mt-4 min-h-0 flex-1 flex-col data-[state=active]:flex"
>
<div className="flex flex-col gap-4 flex-1 min-h-0">
<div className="flex min-h-0 flex-1 flex-col gap-4">
{isLoading ? (
<div className="text-sm text-muted-foreground">
{t("proxies.management.loading")}
@@ -1250,7 +1250,7 @@ export function ProxyManagementDialog({
) : (
<FadingScrollArea
className={cn(
"flex-1 min-h-0",
"min-h-0 flex-1",
selectedProxies.length > 0 && "pb-16",
)}
style={
@@ -1284,7 +1284,7 @@ export function ProxyManagementDialog({
// of it).
header.column.id === "name" && "max-w-0",
header.column.id === "hostPort" &&
"hidden @2xl:table-cell max-w-0",
"hidden max-w-0 @2xl:table-cell",
(header.column.id === "protocol" ||
header.column.id === "type") &&
"hidden @2xl:table-cell",
@@ -1320,7 +1320,7 @@ export function ProxyManagementDialog({
className={cn(
cell.column.id === "name" && "max-w-0",
cell.column.id === "hostPort" &&
"hidden @2xl:table-cell max-w-0",
"hidden max-w-0 @2xl:table-cell",
(cell.column.id === "protocol" ||
cell.column.id === "type") &&
"hidden @2xl:table-cell",
@@ -1343,9 +1343,9 @@ export function ProxyManagementDialog({
<AnimatedTabsContent
value="vpns"
className="mt-4 flex-1 min-h-0 data-[state=active]:flex flex-col"
className="mt-4 min-h-0 flex-1 flex-col data-[state=active]:flex"
>
<div className="flex flex-col gap-4 flex-1 min-h-0">
<div className="flex min-h-0 flex-1 flex-col gap-4">
{isLoadingVpns ? (
<div className="text-sm text-muted-foreground">
{t("vpns.management.loading")}
@@ -1357,7 +1357,7 @@ export function ProxyManagementDialog({
) : (
<FadingScrollArea
className={cn(
"flex-1 min-h-0",
"min-h-0 flex-1",
selectedVpns.length > 0 && "pb-16",
)}
style={
@@ -1391,7 +1391,7 @@ export function ProxyManagementDialog({
// of it).
header.column.id === "name" && "max-w-0",
header.column.id === "hostPort" &&
"hidden @2xl:table-cell max-w-0",
"hidden max-w-0 @2xl:table-cell",
(header.column.id === "protocol" ||
header.column.id === "type") &&
"hidden @2xl:table-cell",
@@ -1427,7 +1427,7 @@ export function ProxyManagementDialog({
className={cn(
cell.column.id === "name" && "max-w-0",
cell.column.id === "hostPort" &&
"hidden @2xl:table-cell max-w-0",
"hidden max-w-0 @2xl:table-cell",
(cell.column.id === "protocol" ||
cell.column.id === "type") &&
"hidden @2xl:table-cell",
+22 -22
View File
@@ -290,13 +290,13 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
} = useLogoEasterEgg({ currentPage, onNavigate });
return (
<nav className="flex flex-col items-center w-10 py-2 gap-1 bg-background border-r border-border shrink-0 relative">
<nav className="relative flex w-10 shrink-0 flex-col items-center gap-1 border-r border-border bg-background py-2">
{!isHidden ? (
<button
ref={logoRef}
type="button"
aria-label={t("header.donutLogo")}
className="grid place-items-center size-7 rounded-md cursor-pointer select-none text-foreground bg-transparent shrink-0"
className="grid size-7 shrink-0 cursor-pointer place-items-center rounded-md bg-transparent text-foreground select-none"
onClick={handleClick}
onPointerDown={() => {
setIsPressed(true);
@@ -336,9 +336,9 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
<div className="size-7 shrink-0" />
)}
<div className="w-5 h-px bg-border my-1 shrink-0" />
<div className="my-1 h-px w-5 shrink-0 bg-border" />
<div className="flex flex-col items-center gap-1 w-full min-h-0 overflow-y-auto [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
<div className="flex min-h-0 w-full scrollbar-none flex-col items-center gap-1 overflow-y-auto [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden">
{TOP_ITEMS.map(({ page, Icon, labelKey }) => {
const active = currentPage === page;
return (
@@ -352,16 +352,16 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
aria-label={t(labelKey)}
aria-current={active ? "page" : undefined}
className={cn(
"relative grid place-items-center size-7 rounded-md cursor-pointer transition-colors duration-100 shrink-0",
"relative grid size-7 shrink-0 cursor-pointer place-items-center rounded-md transition-colors duration-100",
active
? "text-foreground bg-accent"
: "text-muted-foreground hover:text-card-foreground hover:bg-accent/50",
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-card-foreground",
)}
>
{active && (
<span
aria-hidden="true"
className="absolute left-[-7px] top-1.5 bottom-1.5 w-[2px] rounded-full bg-foreground"
className="absolute inset-y-1.5 left-[-7px] w-[2px] rounded-full bg-foreground"
/>
)}
<Icon className="size-3.5" />
@@ -385,10 +385,10 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
aria-label={t("rail.more.label")}
aria-expanded={moreOpen}
className={cn(
"grid place-items-center size-7 rounded-md cursor-pointer transition-colors duration-100 shrink-0",
"grid size-7 shrink-0 cursor-pointer place-items-center rounded-md transition-colors duration-100",
moreOpen
? "text-foreground bg-accent"
: "text-muted-foreground hover:text-card-foreground hover:bg-accent/50",
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-card-foreground",
)}
>
<GoKebabHorizontal className="size-3.5" />
@@ -407,16 +407,16 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
aria-label={t("rail.settings")}
aria-current={currentPage === "settings" ? "page" : undefined}
className={cn(
"relative grid place-items-center size-7 rounded-md cursor-pointer transition-colors duration-100 shrink-0",
"relative grid size-7 shrink-0 cursor-pointer place-items-center rounded-md transition-colors duration-100",
currentPage === "settings"
? "text-foreground bg-accent"
: "text-muted-foreground hover:text-card-foreground hover:bg-accent/50",
? "bg-accent text-foreground"
: "text-muted-foreground hover:bg-accent/50 hover:text-card-foreground",
)}
>
{currentPage === "settings" && (
<span
aria-hidden="true"
className="absolute left-[-7px] top-1.5 bottom-1.5 w-[2px] rounded-full bg-foreground"
className="absolute inset-y-1.5 left-[-7px] w-[2px] rounded-full bg-foreground"
/>
)}
<GoGear className="size-3.5" />
@@ -430,12 +430,12 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
<button
type="button"
aria-label={t("rail.more.closeAriaLabel")}
className="fixed inset-0 z-30 bg-transparent cursor-default"
className="fixed inset-0 z-30 cursor-default bg-transparent"
onClick={() => {
setMoreOpen(false);
}}
/>
<div className="absolute bottom-14 left-11 w-56 bg-card border border-border rounded-lg shadow-2xl p-1 z-40 animate-in fade-in-0 slide-in-from-bottom-1 duration-100">
<div className="absolute bottom-14 left-11 z-40 w-56 animate-in rounded-lg border border-border bg-card p-1 shadow-2xl duration-100 fade-in-0 slide-in-from-bottom-1">
{MORE_ITEMS.map(({ page, Icon, labelKey, hintKey }) => (
<button
key={page}
@@ -444,16 +444,16 @@ export function RailNav({ currentPage, onNavigate }: RailNavProps) {
setMoreOpen(false);
onNavigate(page);
}}
className="flex items-center gap-2 w-full px-2 py-1.5 rounded-md cursor-pointer hover:bg-accent transition-colors duration-100 text-left"
className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left transition-colors duration-100 hover:bg-accent"
>
<span className="grid place-items-center size-5 rounded bg-muted text-muted-foreground shrink-0">
<span className="grid size-5 shrink-0 place-items-center rounded bg-muted text-muted-foreground">
<Icon className="size-3" />
</span>
<span className="flex flex-col min-w-0">
<span className="text-xs font-medium text-foreground truncate">
<span className="flex min-w-0 flex-col">
<span className="truncate text-xs font-medium text-foreground">
{t(labelKey)}
</span>
<span className="text-[10px] text-muted-foreground truncate">
<span className="truncate text-[10px] text-muted-foreground">
{t(hintKey)}
</span>
</span>
+4 -4
View File
@@ -93,10 +93,10 @@ export function ReleaseTypeSelector({
role="combobox"
aria-expanded={popoverOpen}
aria-controls={listboxId}
className="justify-between w-full"
className="w-full justify-between"
>
{selectedDisplayText}
<LuChevronsUpDown className="ml-2 size-4 opacity-50 shrink-0" />
<LuChevronsUpDown className="ml-2 size-4 shrink-0 opacity-50" />
</RippleButton>
</PopoverTrigger>
<PopoverContent id={listboxId} className="p-0">
@@ -134,7 +134,7 @@ export function ReleaseTypeSelector({
: "opacity-0",
)}
/>
<div className="flex gap-2 items-center">
<div className="flex items-center gap-2">
<span className="capitalize">{option.type}</span>
{option.type === "nightly" && (
<Badge variant="secondary" className="text-xs">
@@ -161,7 +161,7 @@ export function ReleaseTypeSelector({
) : (
// Show a simple display when only one release type is available
releaseOptions.length === 1 && (
<div className="flex gap-2 justify-center items-center p-3 rounded-md border bg-muted/50">
<div className="flex items-center justify-center gap-2 rounded-md border bg-muted/50 p-3">
<span className="text-sm font-medium capitalize">
{releaseOptions[0].type}
</span>
+20 -20
View File
@@ -194,7 +194,7 @@ export function SettingsDialog({
return (
<Badge
variant="default"
className="text-success-foreground bg-success"
className="bg-success text-success-foreground"
>
{t("common.status.granted")}
</Badge>
@@ -633,7 +633,7 @@ export function SettingsDialog({
return (
<>
<Dialog open={isOpen} onOpenChange={handleClose} subPage={subPage}>
<DialogContent className="max-w-md max-h-[calc(100vh-5rem)] flex flex-col">
<DialogContent className="flex max-h-[calc(100vh-5rem)] max-w-md flex-col">
{!subPage && (
<DialogHeader className="shrink-0">
<DialogTitle>{t("settings.title")}</DialogTitle>
@@ -642,8 +642,8 @@ export function SettingsDialog({
<div
className={cn(
"grid overflow-y-auto flex-1 gap-6 min-h-0",
subPage ? "py-2 w-full max-w-2xl mx-auto" : "py-4",
"grid min-h-0 flex-1 gap-6 overflow-y-auto",
subPage ? "mx-auto w-full max-w-2xl py-2" : "py-4",
)}
>
{/* Appearance Section */}
@@ -755,14 +755,14 @@ export function SettingsDialog({
return (
<div
key={key}
className="flex flex-col gap-1 items-center"
className="flex flex-col items-center gap-1"
>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
aria-label={label}
className="size-8 rounded-md border shadow-sm cursor-pointer"
className="size-8 cursor-pointer rounded-md border shadow-sm"
style={{ backgroundColor: colorValue }}
/>
</PopoverTrigger>
@@ -771,7 +771,7 @@ export function SettingsDialog({
sideOffset={6}
>
<ColorPicker
className="p-3 rounded-md border shadow-sm bg-background"
className="rounded-md border bg-background p-3 shadow-sm"
value={colorValue}
onColorChange={([r, g, b, a]) => {
const next = Color({ r, g, b }).alpha(a);
@@ -792,21 +792,21 @@ export function SettingsDialog({
}}
>
<ColorPickerSelection className="h-36 rounded" />
<div className="flex gap-3 items-center mt-3">
<div className="mt-3 flex items-center gap-3">
<ColorPickerEyeDropper />
<div className="grid gap-1 w-full">
<div className="grid w-full gap-1">
<ColorPickerHue />
<ColorPickerAlpha />
</div>
</div>
<div className="flex gap-2 items-center mt-3">
<div className="mt-3 flex items-center gap-2">
<ColorPickerOutput />
<ColorPickerFormat />
</div>
</ColorPicker>
</PopoverContent>
</Popover>
<div className="text-[10px] text-muted-foreground text-center leading-tight">
<div className="text-center text-[10px] leading-tight text-muted-foreground">
{label}
</div>
</div>
@@ -860,7 +860,7 @@ export function SettingsDialog({
{/* Default Browser Section - hidden in portable mode */}
{!systemInfo?.portable && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<div className="flex items-center justify-between">
<Label className="text-base font-medium">
{t("settings.defaultBrowser.title")}
</Label>
@@ -909,7 +909,7 @@ export function SettingsDialog({
{permissions.map((permission) => (
<div
key={permission.permission_type}
className="flex justify-between items-center p-3 rounded-lg border"
className="flex items-center justify-between rounded-lg border p-3"
>
<div className="flex items-center gap-x-3">
{getPermissionIcon(permission.permission_type)}
@@ -1015,7 +1015,7 @@ export function SettingsDialog({
{t("settings.encryption.passwordSetDescription")}
</span>
</div>
<div className="flex gap-2 flex-wrap">
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
size="sm"
@@ -1156,7 +1156,7 @@ export function SettingsDialog({
{t("settings.commercial.title")}
</Label>
<div className="flex items-center justify-between p-3 rounded-md border bg-muted/40">
<div className="flex items-center justify-between rounded-md border bg-muted/40 p-3">
{cloudUser != null && cloudUser.plan !== "free" ? (
// Paid Donut plan supersedes the local commercial trial —
// the trial only exists to gate commercial use until the
@@ -1205,7 +1205,7 @@ export function SettingsDialog({
</Label>
{!isLinux && (
<div className="flex items-start gap-x-3 p-3 rounded-lg border">
<div className="flex items-start gap-x-3 rounded-lg border p-3">
<Checkbox
id="disable-auto-updates"
checked={settings.disable_auto_updates ?? false}
@@ -1227,7 +1227,7 @@ export function SettingsDialog({
</div>
)}
<div className="flex items-start gap-x-3 p-3 rounded-lg border">
<div className="flex items-start gap-x-3 rounded-lg border p-3">
<Checkbox
id="keep-decrypted-profiles-in-ram"
checked={settings.keep_decrypted_profiles_in_ram ?? false}
@@ -1305,8 +1305,8 @@ export function SettingsDialog({
{/* System Info */}
{systemInfo && (
<div className="pt-2 border-t">
<p className="text-xs text-muted-foreground font-mono whitespace-pre-line select-all">
<div className="border-t pt-2">
<p className="font-mono text-xs whitespace-pre-line text-muted-foreground select-all">
{`Donut Browser ${systemInfo.app_version}\n${systemInfo.os} ${systemInfo.arch}${systemInfo.portable ? " (portable)" : ""}`}
</p>
</div>
@@ -1314,7 +1314,7 @@ export function SettingsDialog({
</div>
{subPage ? (
<div className="shrink-0 flex items-center justify-end gap-2 pt-2 border-t border-border w-full max-w-2xl mx-auto">
<div className="mx-auto flex w-full max-w-2xl shrink-0 items-center justify-end gap-2 border-t border-border pt-2">
<LoadingButton
size="sm"
isLoading={isSaving}
+25 -25
View File
@@ -302,7 +302,7 @@ export function SharedCamoufoxConfigForm({
</div>
{/* Randomize Fingerprint Option */}
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex items-center gap-x-2">
<Checkbox
id="randomize-fingerprint"
@@ -316,7 +316,7 @@ export function SharedCamoufoxConfigForm({
{t("fingerprint.generateRandomOnLaunch")}
</Label>
</div>
<p className="text-sm text-muted-foreground ml-6">
<p className="ml-6 text-sm text-muted-foreground">
{t("fingerprint.generateRandomDescription")}
</p>
</div>
@@ -410,7 +410,7 @@ export function SharedCamoufoxConfigForm({
{/* Navigator Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.navigatorProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="user-agent">{t("fingerprint.userAgent")}</Label>
<Input
@@ -566,7 +566,7 @@ export function SharedCamoufoxConfigForm({
{/* Screen Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.screenProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="screen-width">
{t("fingerprint.screenWidth")}
@@ -687,7 +687,7 @@ export function SharedCamoufoxConfigForm({
{/* Window Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.windowProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="outer-width">
{t("fingerprint.outerWidth")}
@@ -800,7 +800,7 @@ export function SharedCamoufoxConfigForm({
{/* Geolocation */}
<div className="space-y-3">
<Label>{t("fingerprint.geolocation")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="latitude">{t("fingerprint.latitude")}</Label>
<Input
@@ -860,7 +860,7 @@ export function SharedCamoufoxConfigForm({
{/* Locale */}
<div className="space-y-3">
<Label>{t("fingerprint.locale")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="locale-language">
{t("fingerprint.language")}
@@ -917,7 +917,7 @@ export function SharedCamoufoxConfigForm({
{/* WebGL Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.webglProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="webgl-vendor">
{t("fingerprint.webglVendor")}
@@ -1065,7 +1065,7 @@ export function SharedCamoufoxConfigForm({
{/* Battery */}
<div className="space-y-3">
<Label>{t("fingerprint.battery")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<div className="flex items-center gap-x-2">
<Checkbox
@@ -1138,12 +1138,12 @@ export function SharedCamoufoxConfigForm({
</fieldset>
{limitedMode && (
<>
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
<div className="absolute inset-y-0 left-0 w-6 bg-linear-to-r from-background to-transparent z-[2]" />
<div className="absolute inset-y-0 right-0 w-6 bg-linear-to-l from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 top-0 h-6 bg-linear-to-b from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background to-transparent z-[2]" />
<div className="absolute inset-0 flex items-center justify-center z-[3]">
<div className="absolute inset-0 z-1 bg-background/30 backdrop-blur-[6px]" />
<div className="absolute inset-y-0 left-0 z-2 w-6 bg-linear-to-r from-background to-transparent" />
<div className="absolute inset-y-0 right-0 z-2 w-6 bg-linear-to-l from-background to-transparent" />
<div className="absolute inset-x-0 top-0 z-2 h-6 bg-linear-to-b from-background to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-2 h-6 bg-linear-to-t from-background to-transparent" />
<div className="absolute inset-0 z-3 flex items-center justify-center">
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
<ProBadge />
<span className="text-sm font-medium text-muted-foreground">
@@ -1168,7 +1168,7 @@ export function SharedCamoufoxConfigForm({
onValueChange={readOnly ? undefined : setActiveTab}
className="w-full"
>
<TabsList className="grid grid-cols-2 w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="automatic" disabled={readOnly}>
{t("fingerprint.automatic")}
</TabsTrigger>
@@ -1217,7 +1217,7 @@ export function SharedCamoufoxConfigForm({
</div>
{/* Randomize Fingerprint Option */}
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex items-center gap-x-2">
<Checkbox
id="randomize-fingerprint-auto"
@@ -1234,7 +1234,7 @@ export function SharedCamoufoxConfigForm({
{t("fingerprint.generateRandomOnLaunch")}
</Label>
</div>
<p className="text-sm text-muted-foreground ml-6">
<p className="ml-6 text-sm text-muted-foreground">
{t("fingerprint.generateRandomDescriptionAuto")}
</p>
</div>
@@ -1265,7 +1265,7 @@ export function SharedCamoufoxConfigForm({
className="space-y-3"
>
<Label>{t("fingerprint.screenResolution")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="screen-max-width">
{t("fingerprint.maxWidth")}
@@ -1354,12 +1354,12 @@ export function SharedCamoufoxConfigForm({
</fieldset>
{limitedMode && (
<>
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
<div className="absolute inset-y-0 left-0 w-6 bg-linear-to-r from-background to-transparent z-[2]" />
<div className="absolute inset-y-0 right-0 w-6 bg-linear-to-l from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 top-0 h-6 bg-linear-to-b from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background to-transparent z-[2]" />
<div className="absolute inset-0 flex items-center justify-center z-[3]">
<div className="absolute inset-0 z-1 bg-background/30 backdrop-blur-[6px]" />
<div className="absolute inset-y-0 left-0 z-2 w-6 bg-linear-to-r from-background to-transparent" />
<div className="absolute inset-y-0 right-0 z-2 w-6 bg-linear-to-l from-background to-transparent" />
<div className="absolute inset-x-0 top-0 z-2 h-6 bg-linear-to-b from-background to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-2 h-6 bg-linear-to-t from-background to-transparent" />
<div className="absolute inset-0 z-3 flex items-center justify-center">
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
<ProBadge />
<span className="text-sm font-medium text-muted-foreground">
+10 -10
View File
@@ -21,11 +21,11 @@ interface ShortcutsPageProps {
function Tokens({ tokens }: { tokens: string[] }) {
return (
<div className="flex items-center gap-1 shrink-0">
<div className="flex shrink-0 items-center gap-1">
{tokens.map((tok, i) => (
<kbd
key={i}
className="inline-flex items-center justify-center min-w-[1.5rem] h-6 px-1.5 rounded border border-border bg-muted text-[11px] font-medium text-foreground"
className="inline-flex h-6 min-w-6 items-center justify-center rounded border border-border bg-muted px-1.5 text-[11px] font-medium text-foreground"
>
{tok}
</kbd>
@@ -49,8 +49,8 @@ export function ShortcutsPage({ groupTargets }: ShortcutsPageProps) {
const digitGroups = groupTargets.slice(0, 9);
return (
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto px-6 pt-4 pb-8">
<div className="max-w-3xl w-full mx-auto flex flex-col gap-6">
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-6 pt-4 pb-8">
<div className="mx-auto flex w-full max-w-3xl flex-col gap-6">
<header className="flex flex-col gap-1">
<h1 className="text-lg font-semibold">{t("shortcutsPage.title")}</h1>
<p className="text-xs text-muted-foreground">
@@ -63,17 +63,17 @@ export function ShortcutsPage({ groupTargets }: ShortcutsPageProps) {
if (items.length === 0) return null;
return (
<section key={key} className="flex flex-col gap-2">
<h2 className="text-[10px] uppercase tracking-wide text-muted-foreground">
<h2 className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t(titleKey)}
</h2>
<div className="rounded-md border bg-card divide-y divide-border">
<div className="divide-y divide-border rounded-md border bg-card">
{items.map((s) => (
<div
key={s.id}
className="flex items-center justify-between gap-4 px-3 py-2"
>
<span
className="text-sm truncate min-w-0"
className="min-w-0 truncate text-sm"
title={t(s.labelKey)}
>
{t(s.labelKey)}
@@ -88,17 +88,17 @@ export function ShortcutsPage({ groupTargets }: ShortcutsPageProps) {
{digitGroups.length > 0 ? (
<section className="flex flex-col gap-2">
<h2 className="text-[10px] uppercase tracking-wide text-muted-foreground">
<h2 className="text-[10px] tracking-wide text-muted-foreground uppercase">
{t("commandPalette.groups.profileGroups")}
</h2>
<div className="rounded-md border bg-card divide-y divide-border">
<div className="divide-y divide-border rounded-md border bg-card">
{digitGroups.map((target, i) => (
<div
key={target.id}
className="flex items-center justify-between gap-4 px-3 py-2"
>
<span
className="text-sm truncate min-w-0"
className="min-w-0 truncate text-sm"
title={target.name}
>
{target.name}
+3 -3
View File
@@ -129,7 +129,7 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
{isLoading ? (
<div className="flex justify-center py-8">
<div className="size-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
<div className="size-6 animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
) : (
<div className="grid grid-cols-2 gap-2 py-2">
@@ -141,12 +141,12 @@ export function SyncAllDialog({ isOpen, onClose }: SyncAllDialogProps) {
<div className="flex size-9 shrink-0 items-center justify-center rounded-md bg-primary/10 text-primary">
<Icon className="size-4" />
</div>
<div className="min-w-0 flex-1 text-sm font-medium truncate">
<div className="min-w-0 flex-1 truncate text-sm font-medium">
{label}
</div>
<Badge
variant="secondary"
className="shrink-0 tabular-nums px-2"
className="shrink-0 px-2 tabular-nums"
>
{count}
</Badge>
+9 -9
View File
@@ -248,7 +248,7 @@ export function SyncConfigDialog({
{isLoggedIn && user ? (
<div className="grid gap-4 py-4">
<div className="flex gap-2 items-center text-sm">
<div className="flex items-center gap-2 text-sm">
<div className="size-2 rounded-full bg-success" />
{t("sync.cloud.connected")}
</div>
@@ -300,7 +300,7 @@ export function SyncConfigDialog({
: t("sync.team.roleMember")}
</span>
</div>
<p className="text-xs text-muted-foreground pt-1">
<p className="pt-1 text-xs text-muted-foreground">
{t("sync.team.manageOnWeb")}
</p>
</>
@@ -354,7 +354,7 @@ export function SyncConfigDialog({
<TabsContent value="cloud">
{isCloudLoading ? (
<div className="flex justify-center py-8">
<div className="size-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
<div className="size-6 animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
@@ -374,7 +374,7 @@ export function SyncConfigDialog({
<TabsContent value="self-hosted">
{isLoading ? (
<div className="flex justify-center py-8">
<div className="size-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
<div className="size-6 animate-spin rounded-full border-2 border-current border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
@@ -412,7 +412,7 @@ export function SyncConfigDialog({
onClick={() => {
setShowToken(!showToken);
}}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
className="absolute top-1/2 right-3 -translate-y-1/2 transform rounded-sm p-1 transition-colors hover:bg-accent"
aria-label={
showToken
? t("common.aria.hideToken")
@@ -434,19 +434,19 @@ export function SyncConfigDialog({
</div>
{connectionStatus === "testing" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="size-4 rounded-full border-2 border-current animate-spin border-t-transparent" />
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
{t("sync.status.syncing")}
</div>
)}
{connectionStatus === "connected" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-2 rounded-full bg-success" />
{t("sync.status.connected")}
</div>
)}
{connectionStatus === "error" && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<div className="size-2 rounded-full bg-destructive" />
{t("sync.status.disconnected")}
</div>
+7 -7
View File
@@ -127,20 +127,20 @@ export function SyncFollowerDialog({
{leaderProfile && (
<div className="space-y-3">
<div className="flex items-center gap-2 p-2 rounded-md bg-primary/10 border border-primary/20">
<div className="flex items-center gap-2 rounded-md border border-primary/20 bg-primary/10 p-2">
<Badge variant="default" className="text-xs">
{t("profiles.synchronizer.leader")}
</Badge>
<span className="text-sm font-medium truncate">
<span className="truncate text-sm font-medium">
{leaderProfile.name}
</span>
</div>
<div className="border rounded-md">
<div className="rounded-md border">
<ScrollArea className="h-[clamp(120px,30vh,20rem)]">
<div className="space-y-1 p-2">
{eligibleProfiles.length === 0 ? (
<p className="text-sm text-muted-foreground py-4 text-center">
<p className="py-4 text-center text-sm text-muted-foreground">
{t("profiles.synchronizer.wayfernOnly")}
</p>
) : (
@@ -155,7 +155,7 @@ export function SyncFollowerDialog({
return (
<div
key={profile.id}
className="flex items-center gap-3 p-2 rounded-md hover:bg-accent cursor-pointer"
className="flex cursor-pointer items-center gap-3 rounded-md p-2 hover:bg-accent"
onClick={() => {
handleToggle(
profile.id,
@@ -174,7 +174,7 @@ export function SyncFollowerDialog({
handleToggle(profile.id, checked === true);
}}
/>
<span className="text-sm truncate flex-1">
<span className="flex-1 truncate text-sm">
{profile.name}
</span>
{isFlaky && (
@@ -182,7 +182,7 @@ export function SyncFollowerDialog({
<TooltipTrigger asChild>
<Badge
variant="outline"
className="text-[10px] px-1.5 py-0 text-warning border-warning/50 shrink-0"
className="shrink-0 border-warning/50 px-1.5 py-0 text-[10px] text-warning"
>
{t("profiles.synchronizer.flakyBadge")}
</Badge>
+1 -1
View File
@@ -67,7 +67,7 @@ export function ThankYouDialog({
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
transition={{ ...spring, delay: 0.15 }}
className="mx-auto max-w-[46ch] text-sm leading-6 text-pretty text-muted-foreground"
className="mx-auto max-w-[46ch] text-sm/6 text-pretty text-muted-foreground"
>
{t("onboarding.thankYou.body")}
</motion.p>
+28 -28
View File
@@ -127,7 +127,7 @@ const TruncatedDomain = React.memo<{ domain: string }>(({ domain }) => {
}, [checkTruncation]);
const content = (
<span ref={ref} className="truncate block min-w-0 flex-1">
<span ref={ref} className="block min-w-0 flex-1 truncate">
{domain}
</span>
);
@@ -209,8 +209,8 @@ export function TrafficDetailsDialog({
const formattedTime = time.toLocaleTimeString();
return (
<div className="bg-popover border rounded-lg px-3 py-2 shadow-lg">
<p className="text-xs text-muted-foreground mb-1">{formattedTime}</p>
<div className="rounded-lg border bg-popover px-3 py-2 shadow-lg">
<p className="mb-1 text-xs text-muted-foreground">{formattedTime}</p>
{payload.map((entry) => (
<p key={String(entry.dataKey)} className="text-sm">
<span className="text-muted-foreground">
@@ -262,7 +262,7 @@ export function TrafficDetailsDialog({
<DialogTitle>
{t("traffic.title")}
{profileName && (
<span className="text-muted-foreground font-normal ml-2">
<span className="ml-2 font-normal text-muted-foreground">
{profileName}
</span>
)}
@@ -273,7 +273,7 @@ export function TrafficDetailsDialog({
<div className="space-y-6 pr-4">
{/* Chart with Period Selector */}
<div>
<div className="flex items-center justify-between mb-2">
<div className="mb-2 flex items-center justify-between">
<h3 className="text-sm font-medium">
{t("traffic.bandwidthOverTime")}
</h3>
@@ -283,7 +283,7 @@ export function TrafficDetailsDialog({
setTimePeriod(v as TimePeriod);
}}
>
<SelectTrigger className="w-[120px] h-8">
<SelectTrigger className="h-8 w-[120px]">
<SelectValue
placeholder={t("traffic.timePeriodPlaceholder")}
/>
@@ -396,7 +396,7 @@ export function TrafficDetailsDialog({
</ResponsiveContainer>
</div>
<div className="flex items-center justify-center gap-6 mt-2">
<div className="mt-2 flex items-center justify-center gap-6">
<div className="flex items-center gap-2">
<div
className="size-3 rounded"
@@ -420,7 +420,7 @@ export function TrafficDetailsDialog({
{/* Period Stats - now uses backend-computed values */}
<div className="grid grid-cols-3 gap-4">
<div className="bg-muted/50 rounded-lg p-3">
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-xs text-muted-foreground">
{t("traffic.sentLabel", {
period:
@@ -433,7 +433,7 @@ export function TrafficDetailsDialog({
{formatBytes(stats?.period_bytes_sent ?? 0)}
</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-xs text-muted-foreground">
{t("traffic.receivedLabel", {
period:
@@ -446,7 +446,7 @@ export function TrafficDetailsDialog({
{formatBytes(stats?.period_bytes_received ?? 0)}
</p>
</div>
<div className="bg-muted/50 rounded-lg p-3">
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-xs text-muted-foreground">
{t("traffic.requestsLabel", {
period:
@@ -462,7 +462,7 @@ export function TrafficDetailsDialog({
</div>
{/* Total Stats (smaller, under period stats) */}
<div className="flex items-center gap-6 text-sm text-muted-foreground border-t pt-4">
<div className="flex items-center gap-6 border-t pt-4 text-sm text-muted-foreground">
<div>
<span className="font-medium">
{t("traffic.allTimeTraffic")}
@@ -488,7 +488,7 @@ export function TrafficDetailsDialog({
{/* Top Domains by Traffic */}
{topDomainsByTraffic.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2">
<h3 className="mb-2 text-sm font-medium">
{t("traffic.topByTraffic", {
period:
timePeriod === "all"
@@ -496,8 +496,8 @@ export function TrafficDetailsDialog({
: timePeriod,
})}
</h3>
<div className="border rounded-md">
<div className="grid grid-cols-[1fr_80px_80px_80px] gap-2 px-3 py-2 text-xs font-medium text-muted-foreground border-b bg-muted/30">
<div className="rounded-md border">
<div className="grid grid-cols-[1fr_80px_80px_80px] gap-2 border-b bg-muted/30 px-3 py-2 text-xs font-medium text-muted-foreground">
<span>{t("traffic.columnDomain")}</span>
<span className="text-right">
{t("traffic.columnRequests")}
@@ -513,10 +513,10 @@ export function TrafficDetailsDialog({
{topDomainsByTraffic.map((domain, index) => (
<div
key={domain.domain}
className="grid grid-cols-[1fr_80px_80px_80px] gap-2 px-3 py-2 text-sm border-b last:border-b-0 hover:bg-muted/30"
className="grid grid-cols-[1fr_80px_80px_80px] gap-2 border-b px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
>
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs text-muted-foreground w-4 shrink-0">
<div className="flex min-w-0 items-center gap-2">
<span className="w-4 shrink-0 text-xs text-muted-foreground">
{index + 1}
</span>
<TruncatedDomain domain={domain.domain} />
@@ -540,7 +540,7 @@ export function TrafficDetailsDialog({
{/* Top Domains by Requests */}
{topDomainsByRequests.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2">
<h3 className="mb-2 text-sm font-medium">
{t("traffic.topByRequests", {
period:
timePeriod === "all"
@@ -548,8 +548,8 @@ export function TrafficDetailsDialog({
: timePeriod,
})}
</h3>
<div className="border rounded-md">
<div className="grid grid-cols-[1fr_80px_100px] gap-2 px-3 py-2 text-xs font-medium text-muted-foreground border-b bg-muted/30">
<div className="rounded-md border">
<div className="grid grid-cols-[1fr_80px_100px] gap-2 border-b bg-muted/30 px-3 py-2 text-xs font-medium text-muted-foreground">
<span>{t("traffic.columnDomain")}</span>
<span className="text-right">
{t("traffic.columnRequests")}
@@ -562,10 +562,10 @@ export function TrafficDetailsDialog({
{topDomainsByRequests.map((domain, index) => (
<div
key={domain.domain}
className="grid grid-cols-[1fr_80px_100px] gap-2 px-3 py-2 text-sm border-b last:border-b-0 hover:bg-muted/30"
className="grid grid-cols-[1fr_80px_100px] gap-2 border-b px-3 py-2 text-sm last:border-b-0 hover:bg-muted/30"
>
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs text-muted-foreground w-4 shrink-0">
<div className="flex min-w-0 items-center gap-2">
<span className="w-4 shrink-0 text-xs text-muted-foreground">
{index + 1}
</span>
<TruncatedDomain domain={domain.domain} />
@@ -588,15 +588,15 @@ export function TrafficDetailsDialog({
{/* Unique IPs */}
{stats?.unique_ips && stats.unique_ips.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2">
<h3 className="mb-2 text-sm font-medium">
{t("traffic.uniqueIps", { count: stats.unique_ips.length })}
</h3>
<FadingScrollArea className="p-3 max-h-[clamp(120px,15vh,240px)]">
<FadingScrollArea className="max-h-[clamp(120px,15vh,240px)] p-3">
<div className="flex flex-wrap gap-1.5">
{stats.unique_ips.map((ip) => (
<span
key={ip}
className="text-xs bg-muted px-2 py-1 rounded font-mono"
className="rounded bg-muted px-2 py-1 font-mono text-xs"
>
{ip}
</span>
@@ -608,9 +608,9 @@ export function TrafficDetailsDialog({
{/* No data state */}
{!stats && (
<div className="text-center py-8 text-muted-foreground">
<div className="py-8 text-center text-muted-foreground">
<p>{t("traffic.noData")}</p>
<p className="text-sm mt-1">{t("traffic.noDataHint")}</p>
<p className="mt-1 text-sm">{t("traffic.noDataHint")}</p>
</div>
)}
</div>
+3 -3
View File
@@ -4,13 +4,13 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const alertVariants = cva(
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[--spacing(4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
{
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
"bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
@@ -55,7 +55,7 @@ function AlertDescription({
<div
data-slot="alert-description"
className={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
"col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
className,
)}
{...props}
+2 -2
View File
@@ -22,9 +22,9 @@ function AnimatedSwitch({ className, ...props }: AnimatedSwitchProps) {
data-slot="animated-switch"
className={cn(
"peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center justify-start rounded-full border border-transparent px-[2px]",
"bg-input data-[state=checked]:bg-primary data-[state=checked]:justify-end",
"bg-input data-[state=checked]:justify-end data-[state=checked]:bg-primary",
"transition-colors duration-200 ease-out",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
"disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
+3 -3
View File
@@ -78,7 +78,7 @@ function AnimatedTabsList({
<TabsPrimitive.List
data-slot="animated-tabs-list"
className={cn(
"relative inline-flex max-w-full items-center gap-1 overflow-x-auto rounded-md p-0 [scrollbar-width:none]",
"relative inline-flex max-w-full scrollbar-none items-center gap-1 overflow-x-auto rounded-md p-0",
className,
)}
onMouseLeave={(event) => {
@@ -120,10 +120,10 @@ function AnimatedTabsTrigger({
onMouseEnter?.(event);
}}
className={cn(
"relative isolate inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 whitespace-nowrap rounded-md px-3 text-sm font-medium transition-colors duration-150",
"relative isolate inline-flex h-7 cursor-pointer items-center justify-center gap-1.5 rounded-md px-3 text-sm font-medium whitespace-nowrap transition-colors duration-150",
"text-muted-foreground hover:text-foreground",
isActive && "text-foreground",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
"focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background focus-visible:outline-none",
"disabled:pointer-events-none disabled:opacity-50",
className,
)}
+3 -3
View File
@@ -5,16 +5,16 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
"inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-md border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary dark:bg-secondary/60 text-secondary-foreground [a&]:hover:bg-secondary/90",
"border-transparent bg-secondary text-secondary-foreground dark:bg-secondary/60 [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
+4 -4
View File
@@ -5,16 +5,16 @@ import type * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default:
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
destructive:
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
ghost:
@@ -23,7 +23,7 @@ const buttonVariants = cva(
},
size: {
default: "h-9 px-4 py-2 has-[>svg]:px-3",
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
icon: "size-9",
},
+2 -2
View File
@@ -5,7 +5,7 @@ function Card({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="card"
className={cn(
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
"flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
className,
)}
{...props}
@@ -40,7 +40,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="card-description"
className={cn("text-muted-foreground text-sm", className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
);
+5 -5
View File
@@ -61,7 +61,7 @@ const ChartContainer = React.forwardRef<
data-chart={chartId}
ref={ref}
className={cn(
"flex aspect-video max-h-[min(45vh,20rem)] w-full justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
"flex aspect-video max-h-[min(45vh,20rem)] w-full justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector]:outline-none [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-none",
className,
)}
{...props}
@@ -196,7 +196,7 @@ const ChartTooltipContent = React.forwardRef<
<div
ref={ref}
className={cn(
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
"grid min-w-32 items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
className,
)}
>
@@ -213,7 +213,7 @@ const ChartTooltipContent = React.forwardRef<
<div
key={String(item.dataKey ?? index)}
className={cn(
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:size-2.5 [&>svg]:text-muted-foreground",
indicator === "dot" && "items-center",
)}
>
@@ -258,7 +258,7 @@ const ChartTooltipContent = React.forwardRef<
</span>
</div>
{item.value && (
<span className="font-mono font-medium tabular-nums text-foreground">
<span className="font-mono font-medium text-foreground tabular-nums">
{item.value.toLocaleString()}
</span>
)}
@@ -314,7 +314,7 @@ const ChartLegendContent = React.forwardRef<
<div
key={item.value}
className={cn(
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
"flex items-center gap-1.5 [&>svg]:size-3 [&>svg]:text-muted-foreground",
)}
>
{itemConfig?.icon && !hideIcon ? (
+1 -1
View File
@@ -14,7 +14,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"cursor-pointer peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer size-4 shrink-0 cursor-pointer rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary",
className,
)}
{...props}
+15 -15
View File
@@ -152,7 +152,7 @@ export const ColorPicker = ({
}}
>
<div
className={cn("flex flex-col gap-4 size-full", className)}
className={cn("flex size-full flex-col gap-4", className)}
{...props}
>
{children}
@@ -232,7 +232,7 @@ export const ColorPickerSelection = memo(
return (
<div
className={cn("relative rounded cursor-pointer size-full", className)}
className={cn("relative size-full cursor-pointer rounded", className)}
onPointerDown={(e) => {
e.preventDefault();
setIsDragging(true);
@@ -245,7 +245,7 @@ export const ColorPickerSelection = memo(
{...props}
>
<div
className="absolute size-4 rounded-full border-2 border-white -translate-x-1/2 -translate-y-1/2 pointer-events-none"
className="pointer-events-none absolute size-4 -translate-1/2 rounded-full border-2 border-white"
style={{
left: `${positionX * 100}%`,
top: `${positionY * 100}%`,
@@ -269,7 +269,7 @@ export const ColorPickerHue = ({
return (
<Slider.Root
className={cn("flex relative w-full h-4 touch-none", className)}
className={cn("relative flex h-4 w-full touch-none", className)}
max={360}
onValueChange={([hue]) => {
setHue(hue);
@@ -281,7 +281,7 @@ export const ColorPickerHue = ({
<Slider.Track className="relative my-0.5 h-3 w-full grow rounded-full bg-[linear-gradient(90deg,#FF0000,#FFFF00,#00FF00,#00FFFF,#0000FF,#FF00FF,#FF0000)]">
<Slider.Range className="absolute h-full" />
</Slider.Track>
<Slider.Thumb className="block size-4 rounded-full border shadow transition-colors border-primary/50 bg-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
<Slider.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" />
</Slider.Root>
);
};
@@ -296,7 +296,7 @@ export const ColorPickerAlpha = ({
return (
<Slider.Root
className={cn("flex relative w-full h-4 touch-none", className)}
className={cn("relative flex h-4 w-full touch-none", className)}
max={100}
onValueChange={([alpha]) => {
setAlpha(alpha);
@@ -312,10 +312,10 @@ export const ColorPickerAlpha = ({
'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAMUlEQVQ4T2NkYGAQYcAP3uCTZhw1gGGYhAGBZIA/nYDCgBDAm9BGDWAAJyRCgLaBCAAgXwixzAS0pgAAAABJRU5ErkJggg==") left center',
}}
>
<div className="absolute inset-0 bg-linear-to-r from-transparent rounded-full to-black/50" />
<Slider.Range className="absolute h-full bg-transparent rounded-full" />
<div className="absolute inset-0 rounded-full bg-linear-to-r from-transparent to-black/50" />
<Slider.Range className="absolute h-full rounded-full bg-transparent" />
</Slider.Track>
<Slider.Thumb className="block size-4 rounded-full border shadow transition-colors border-primary/50 bg-background focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50" />
<Slider.Thumb className="block size-4 rounded-full border border-primary/50 bg-background shadow transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50" />
</Slider.Root>
);
};
@@ -372,7 +372,7 @@ export const ColorPickerOutput = ({
return (
<Select onValueChange={setMode} value={mode}>
<SelectTrigger className="w-20 h-8 text-xs shrink-0" {...props}>
<SelectTrigger className="h-8 w-20 shrink-0 text-xs" {...props}>
<SelectValue placeholder={t("common.labels.mode")} />
</SelectTrigger>
<SelectContent>
@@ -396,11 +396,11 @@ const PercentageInput = ({ className, ...props }: PercentageInputProps) => {
type="text"
{...props}
className={cn(
"h-8 w-[3.25rem] rounded-l-none bg-secondary px-2 text-xs shadow-none",
"h-8 w-13 rounded-l-none bg-secondary px-2 text-xs shadow-none",
className,
)}
/>
<span className="absolute right-2 top-1/2 text-xs -translate-y-1/2 text-muted-foreground">
<span className="absolute top-1/2 right-2 -translate-y-1/2 text-xs text-muted-foreground">
%
</span>
</div>
@@ -422,13 +422,13 @@ export const ColorPickerFormat = ({
return (
<div
className={cn(
"flex relative items-center -space-x-px w-full rounded-md shadow-sm",
"relative flex w-full items-center -space-x-px rounded-md shadow-sm",
className,
)}
{...props}
>
<Input
className="px-2 h-8 text-xs rounded-r-none shadow-none bg-secondary"
className="h-8 rounded-r-none bg-secondary px-2 text-xs shadow-none"
readOnly
type="text"
value={hex}
@@ -479,7 +479,7 @@ export const ColorPickerFormat = ({
return (
<div className={cn("w-full rounded-md shadow-sm", className)} {...props}>
<Input
className="px-2 w-full h-8 text-xs shadow-none bg-secondary"
className="h-8 w-full bg-secondary px-2 text-xs shadow-none"
readOnly
type="text"
value={`rgba(${rgb.join(", ")}, ${alpha}%)`}
+7 -7
View File
@@ -22,7 +22,7 @@ function Command({
<CommandPrimitive
data-slot="command"
className={cn(
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
"flex size-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className,
)}
{...props}
@@ -57,7 +57,7 @@ function CommandDialog({
<Command
filter={filter}
shouldFilter={shouldFilter}
className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:size-5 [&_[cmdk-item]_svg]:size-5 **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground **:[[cmdk-group]]:px-2 **:[[cmdk-input]]:h-12 **:[[cmdk-item]]:px-2 **:[[cmdk-item]]:py-3"
>
{children}
</Command>
@@ -79,7 +79,7 @@ function CommandInput({
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
"flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
@@ -124,7 +124,7 @@ function CommandGroup({
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-x-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
"overflow-x-hidden p-1 text-foreground **:[[cmdk-group-heading]]:px-2 **:[[cmdk-group-heading]]:py-1.5 **:[[cmdk-group-heading]]:text-xs **:[[cmdk-group-heading]]:font-medium **:[[cmdk-group-heading]]:text-muted-foreground",
className,
)}
{...props}
@@ -139,7 +139,7 @@ function CommandSeparator({
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
);
@@ -153,7 +153,7 @@ function CommandItem({
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className,
)}
{...props}
@@ -169,7 +169,7 @@ function CommandShortcut({
<span
data-slot="command-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
+5 -5
View File
@@ -258,14 +258,14 @@ function DialogContent({
// w-[calc(100%-2rem)] (not w-full + max-w) keeps the 1rem window
// gutter even when callers override max-w-*: tailwind-merge drops
// a base max-w in favor of the caller's, but leaves width alone.
"bg-background fixed top-[50%] left-[50%] z-10000 grid w-[calc(100%-2rem)] max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg max-h-[calc(100vh-3rem)] overflow-y-auto",
"fixed top-[50%] left-[50%] z-10000 grid max-h-[calc(100vh-3rem)] w-[calc(100%-2rem)] max-w-lg -translate-[50%] gap-4 overflow-y-auto rounded-lg border bg-background p-6 shadow-lg",
className,
)}
{...props}
>
{children}
{!hideClose && dismissible && (
<DialogPrimitive.Close className="cursor-pointer ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<DialogPrimitive.Close className="absolute top-4 right-4 cursor-pointer rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<RxCross2 />
<span className="sr-only">{t("common.buttons.close")}</span>
</DialogPrimitive.Close>
@@ -286,7 +286,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-left pr-8", className)}
className={cn("flex flex-col gap-2 pr-8 text-left", className)}
{...props}
/>
);
@@ -297,7 +297,7 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
<div
data-slot="dialog-footer"
className={cn(
"flex flex-row flex-wrap justify-end gap-2 shrink-0",
"flex shrink-0 flex-row flex-wrap justify-end gap-2",
className,
)}
{...props}
@@ -312,7 +312,7 @@ function DialogTitle({
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg font-semibold leading-none", className)}
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
);
+9 -9
View File
@@ -42,7 +42,7 @@ function DropdownMenuContent({
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[50000] max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
"z-50000 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className,
)}
{...props}
@@ -74,7 +74,7 @@ function DropdownMenuItem({
data-inset={inset}
data-variant={variant}
className={cn(
"cursor-pointer focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
className,
)}
{...props}
@@ -92,7 +92,7 @@ function DropdownMenuCheckboxItem({
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
checked={checked}
@@ -128,7 +128,7 @@ function DropdownMenuRadioItem({
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"relative flex cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className,
)}
{...props}
@@ -155,7 +155,7 @@ function DropdownMenuLabel({
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
"px-2 py-1.5 text-sm font-medium data-inset:pl-8",
className,
)}
{...props}
@@ -170,7 +170,7 @@ function DropdownMenuSeparator({
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
@@ -184,7 +184,7 @@ function DropdownMenuShortcut({
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
"ml-auto text-xs tracking-widest text-muted-foreground",
className,
)}
{...props}
@@ -211,7 +211,7 @@ function DropdownMenuSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
"flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-inset:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className,
)}
{...props}
@@ -232,7 +232,7 @@ function DropdownMenuSubContent({
data-slot="dropdown-menu-sub-content"
collisionPadding={collisionPadding}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[50000] max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto rounded-md border p-1 shadow-lg",
"z-50000 max-h-(--radix-dropdown-menu-content-available-height) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className,
)}
{...props}
+2 -2
View File
@@ -9,7 +9,7 @@ export type FadingScrollAreaProps = HTMLAttributes<HTMLDivElement>;
/**
* Scrollable container with top/bottom fade overlays. The fades only become
* visible when the matching direction is actually scrollable. Use in place
* of `<div className="border rounded-md max-h-[...] overflow-auto">` for
* of `<div className="max-h-[...] overflow-auto rounded-md border">` for
* lists that should match the borderless aesthetic of the profile table.
*/
export function FadingScrollArea({
@@ -23,7 +23,7 @@ export function FadingScrollArea({
return (
<div
ref={ref}
className={cn("overflow-y-auto scroll-fade", className)}
className={cn("scroll-fade overflow-y-auto", className)}
{...props}
>
{children}
+3 -3
View File
@@ -8,9 +8,9 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
type={type}
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
className,
)}
{...props}
+1 -1
View File
@@ -32,7 +32,7 @@ function PopoverContent({
sideOffset={sideOffset}
collisionPadding={collisionPadding}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[50000] max-h-(--radix-popover-content-available-height) origin-(--radix-popover-content-transform-origin) overflow-y-auto rounded-md border p-4 shadow-md outline-hidden",
"z-50000 max-h-(--radix-popover-content-available-height) origin-(--radix-popover-content-transform-origin) overflow-y-auto rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
className,
)}
{...props}
+1 -1
View File
@@ -4,7 +4,7 @@ export function ProBadge({ className }: { className?: string }) {
return (
<span
className={cn(
"text-[10px] font-semibold px-1 py-0.5 rounded bg-primary text-primary-foreground",
"rounded bg-primary px-1 py-0.5 text-[10px] font-semibold text-primary-foreground",
className,
)}
>
+2 -2
View File
@@ -14,14 +14,14 @@ function Progress({
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
className,
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
className="size-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
/>
</ProgressPrimitive.Root>
+1 -1
View File
@@ -28,7 +28,7 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"cursor-pointer aspect-square size-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aspect-square size-4 cursor-pointer rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
+5 -5
View File
@@ -7,15 +7,15 @@ import * as React from "react";
import { cn } from "@/lib/utils";
const buttonVariants = cva(
"relative overflow-hidden cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"relative inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 overflow-hidden rounded-lg text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
outline:
"border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
"border bg-background hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost:
@@ -23,7 +23,7 @@ const buttonVariants = cva(
},
size: {
default: "h-10 px-4 py-2 has-[>svg]:px-3",
sm: "h-9 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
sm: "h-9 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
lg: "h-11 px-8 has-[>svg]:px-6",
icon: "size-10",
},
@@ -35,7 +35,7 @@ const buttonVariants = cva(
},
);
const rippleVariants = cva("absolute rounded-full size-5 pointer-events-none", {
const rippleVariants = cva("pointer-events-none absolute size-5 rounded-full", {
variants: {
variant: {
default: "bg-primary-foreground",
+2 -2
View File
@@ -18,7 +18,7 @@ function ScrollArea({
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
>
{children}
</ScrollAreaPrimitive.Viewport>
@@ -49,7 +49,7 @@ function ScrollBar({
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full"
className="relative flex-1 rounded-full bg-border"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
+6 -6
View File
@@ -37,7 +37,7 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"cursor-pointer border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"flex w-fit cursor-pointer items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
className,
)}
{...props}
@@ -61,7 +61,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-[50000] max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
"relative z-50000 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className,
@@ -74,7 +74,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1",
)}
>
{children}
@@ -92,7 +92,7 @@ function SelectLabel({
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
{...props}
/>
);
@@ -107,7 +107,7 @@ function SelectItem({
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"cursor-pointer focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
"relative flex w-full cursor-pointer items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className,
)}
{...props}
@@ -129,7 +129,7 @@ function SelectSeparator({
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
{...props}
/>
);
+1 -1
View File
@@ -9,7 +9,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
className="group toaster"
style={
{
"--normal-bg": "var(--card)",
+4 -4
View File
@@ -16,7 +16,7 @@ function Table({
>
<table
data-slot="table"
className={cn("w-full text-sm caption-bottom", className)}
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
@@ -48,7 +48,7 @@ function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
className,
)}
{...props}
@@ -61,7 +61,7 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
className,
)}
{...props}
@@ -74,7 +74,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
<th
data-slot="table-head"
className={cn(
"px-2 h-8 font-medium text-left align-middle whitespace-nowrap text-foreground",
"h-8 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground",
className,
)}
{...props}
+3 -3
View File
@@ -78,7 +78,7 @@ const TabsList = React.forwardRef<
ref={ref}
data-slot="tabs-list"
className={cn(
"inline-flex h-10 max-w-full items-center justify-center overflow-x-auto rounded-md bg-muted p-1 text-muted-foreground [scrollbar-width:none]",
"inline-flex h-10 max-w-full scrollbar-none items-center justify-center overflow-x-auto rounded-md bg-muted p-1 text-muted-foreground",
className,
)}
{...props}
@@ -104,7 +104,7 @@ const TabsTrigger = React.forwardRef<
ref={ref}
data-slot="tabs-trigger"
className={cn(
"cursor-pointer inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
"inline-flex cursor-pointer items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap ring-offset-background transition-all focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className,
)}
{...props}
@@ -134,7 +134,7 @@ function TabsContent({
exit={{ opacity: 0, filter: "blur(4px)" }}
transition={transition}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
"mt-2 ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none",
className,
)}
{...props}
+1 -1
View File
@@ -9,7 +9,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
ref={ref}
+2 -2
View File
@@ -51,14 +51,14 @@ function TooltipContent({
sideOffset={sideOffset}
alignOffset={alignOffset}
className={cn(
"bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[50000] w-fit max-w-[min(24rem,calc(100vw-2rem))] origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
"z-50000 w-fit max-w-[min(24rem,calc(100vw-2rem))] origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-primary px-3 py-1.5 text-xs text-balance text-primary-foreground fade-in-0 zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow
className="fill-primary z-[50000]"
className="z-50000 fill-primary"
style={
arrowOffset !== 0
? { transform: `translateX(${-arrowOffset}px)` }
+2 -2
View File
@@ -75,11 +75,11 @@ export function VpnCheckButton({
disabled={isCurrentlyChecking || disabled}
>
{isCurrentlyChecking ? (
<div className="size-3 rounded-full border border-current animate-spin border-t-transparent" />
<div className="size-3 animate-spin rounded-full border border-current border-t-transparent" />
) : result?.is_valid ? (
<FiCheck className="size-3 text-success" />
) : result && !result.is_valid ? (
<span className="text-destructive text-sm"></span>
<span className="text-sm text-destructive"></span>
) : (
<FiCheck className="size-3" />
)}
+6 -6
View File
@@ -219,8 +219,8 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
}
}}
>
<LuUpload className="size-10 text-muted-foreground mb-4" />
<p className="text-sm text-muted-foreground text-center">
<LuUpload className="mb-4 size-10 text-muted-foreground" />
<p className="text-center text-sm text-muted-foreground">
{t("vpns.import.dropzonePrompt")}
</p>
<input
@@ -235,7 +235,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
}}
/>
</div>
<p className="text-xs text-muted-foreground text-center">
<p className="text-center text-xs text-muted-foreground">
{t("vpns.import.pasteHint", { modKey })}
</p>
</div>
@@ -243,7 +243,7 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
{step === "vpn-preview" && vpnPreview && (
<div className="space-y-4">
<div className="flex items-center gap-3 p-4 bg-muted/30 rounded-lg">
<div className="flex items-center gap-3 rounded-lg bg-muted/30 p-4">
<LuShield className="size-8 text-primary" />
<div>
<div className="font-medium">
@@ -275,8 +275,8 @@ export function VpnImportDialog({ isOpen, onClose }: VpnImportDialogProps) {
<div className="space-y-2">
<Label>{t("vpns.import.configPreview")}</Label>
<ScrollArea className="h-[min(150px,25vh)] border rounded-md">
<pre className="p-2 text-xs font-mono whitespace-pre-wrap break-all">
<ScrollArea className="h-[min(150px,25vh)] rounded-md border">
<pre className="p-2 font-mono text-xs break-all whitespace-pre-wrap">
{vpnPreview.content.slice(0, 1000)}
{vpnPreview.content.length > 1000 && "..."}
</pre>
+29 -29
View File
@@ -227,7 +227,7 @@ export function WayfernConfigForm({
</div>
{/* Randomize Fingerprint Option */}
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex items-center gap-x-2">
<Checkbox
id="randomize-fingerprint"
@@ -241,7 +241,7 @@ export function WayfernConfigForm({
{t("fingerprint.generateRandomOnLaunch")}
</Label>
</div>
<p className="text-sm text-muted-foreground ml-6">
<p className="ml-6 text-sm text-muted-foreground">
{t("fingerprint.generateRandomDescription")}
</p>
</div>
@@ -290,8 +290,8 @@ export function WayfernConfigForm({
{/* User Agent and Platform */}
<div className="space-y-3">
<Label>{t("fingerprint.userAgentAndPlatform")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="space-y-2 col-span-full">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="col-span-full space-y-2">
<Label htmlFor="user-agent">{t("fingerprint.userAgent")}</Label>
<Input
id="user-agent"
@@ -381,7 +381,7 @@ export function WayfernConfigForm({
{/* Hardware Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.hardwareProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="hardware-concurrency">
{t("fingerprint.hardwareConcurrency")}
@@ -439,7 +439,7 @@ export function WayfernConfigForm({
{/* Screen Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.screenProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="screen-width">
{t("fingerprint.screenWidth")}
@@ -561,7 +561,7 @@ export function WayfernConfigForm({
{/* Window Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.windowProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="window-outer-width">
{t("fingerprint.outerWidth")}
@@ -674,7 +674,7 @@ export function WayfernConfigForm({
{/* Language & Locale */}
<div className="space-y-3">
<Label>{t("fingerprint.languageAndLocale")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="language">
{t("fingerprint.primaryLanguage")}
@@ -756,7 +756,7 @@ export function WayfernConfigForm({
<p className="text-sm text-muted-foreground">
{t("fingerprint.timezoneGeolocationDescription")}
</p>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="timezone">
{t("fingerprint.timezoneIana")}
@@ -853,7 +853,7 @@ export function WayfernConfigForm({
{/* WebGL Properties */}
<div className="space-y-3">
<Label>{t("fingerprint.webglProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="webgl-vendor">
{t("fingerprint.webglVendor")}
@@ -951,7 +951,7 @@ export function WayfernConfigForm({
{/* Audio */}
<div className="space-y-3">
<Label>{t("fingerprint.audioProperties")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="audio-sample-rate">
{t("fingerprint.sampleRate")}
@@ -994,7 +994,7 @@ export function WayfernConfigForm({
{/* Battery */}
<div className="space-y-3">
<Label>{t("fingerprint.battery")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<div className="flex items-center gap-x-2">
<Checkbox
@@ -1040,7 +1040,7 @@ export function WayfernConfigForm({
{/* Vendor Info */}
<div className="space-y-3">
<Label>{t("fingerprint.vendorInfo")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2 @2xl:grid-cols-3">
<div className="space-y-2">
<Label htmlFor="vendor">{t("fingerprint.vendor")}</Label>
<Input
@@ -1094,12 +1094,12 @@ export function WayfernConfigForm({
</fieldset>
{limitedMode && (
<>
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
<div className="absolute inset-y-0 left-0 w-6 bg-linear-to-r from-background to-transparent z-[2]" />
<div className="absolute inset-y-0 right-0 w-6 bg-linear-to-l from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 top-0 h-6 bg-linear-to-b from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background to-transparent z-[2]" />
<div className="absolute inset-0 flex items-center justify-center z-[3]">
<div className="absolute inset-0 z-1 bg-background/30 backdrop-blur-[6px]" />
<div className="absolute inset-y-0 left-0 z-2 w-6 bg-linear-to-r from-background to-transparent" />
<div className="absolute inset-y-0 right-0 z-2 w-6 bg-linear-to-l from-background to-transparent" />
<div className="absolute inset-x-0 top-0 z-2 h-6 bg-linear-to-b from-background to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-2 h-6 bg-linear-to-t from-background to-transparent" />
<div className="absolute inset-0 z-3 flex items-center justify-center">
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
<ProBadge />
<span className="text-sm font-medium text-muted-foreground">
@@ -1123,7 +1123,7 @@ export function WayfernConfigForm({
onValueChange={readOnly ? undefined : setActiveTab}
className="w-full"
>
<TabsList className="grid grid-cols-2 w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="automatic" disabled={readOnly}>
{t("fingerprint.automatic")}
</TabsTrigger>
@@ -1180,7 +1180,7 @@ export function WayfernConfigForm({
</div>
{/* Randomize Fingerprint Option */}
<div className="space-y-3 p-4 border rounded-lg bg-muted/30">
<div className="space-y-3 rounded-lg border bg-muted/30 p-4">
<div className="flex items-center gap-x-2">
<Checkbox
id="randomize-fingerprint-auto"
@@ -1197,7 +1197,7 @@ export function WayfernConfigForm({
{t("fingerprint.generateRandomOnLaunch")}
</Label>
</div>
<p className="text-sm text-muted-foreground ml-6">
<p className="ml-6 text-sm text-muted-foreground">
{t("fingerprint.generateRandomDescription")}
</p>
</div>
@@ -1228,7 +1228,7 @@ export function WayfernConfigForm({
className="space-y-3"
>
<Label>{t("fingerprint.screenResolution")}</Label>
<div className="grid grid-cols-1 @md:grid-cols-2 gap-4">
<div className="grid grid-cols-1 gap-4 @md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="screen-max-width">
{t("fingerprint.maxWidth")}
@@ -1317,12 +1317,12 @@ export function WayfernConfigForm({
</fieldset>
{limitedMode && (
<>
<div className="absolute inset-0 backdrop-blur-[6px] bg-background/30 z-[1]" />
<div className="absolute inset-y-0 left-0 w-6 bg-linear-to-r from-background to-transparent z-[2]" />
<div className="absolute inset-y-0 right-0 w-6 bg-linear-to-l from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 top-0 h-6 bg-linear-to-b from-background to-transparent z-[2]" />
<div className="absolute inset-x-0 bottom-0 h-6 bg-linear-to-t from-background to-transparent z-[2]" />
<div className="absolute inset-0 flex items-center justify-center z-[3]">
<div className="absolute inset-0 z-1 bg-background/30 backdrop-blur-[6px]" />
<div className="absolute inset-y-0 left-0 z-2 w-6 bg-linear-to-r from-background to-transparent" />
<div className="absolute inset-y-0 right-0 z-2 w-6 bg-linear-to-l from-background to-transparent" />
<div className="absolute inset-x-0 top-0 z-2 h-6 bg-linear-to-b from-background to-transparent" />
<div className="absolute inset-x-0 bottom-0 z-2 h-6 bg-linear-to-t from-background to-transparent" />
<div className="absolute inset-0 z-3 flex items-center justify-center">
<div className="flex items-center gap-2 rounded-md bg-background/80 px-3 py-1.5">
<ProBadge />
<span className="text-sm font-medium text-muted-foreground">
+1 -1
View File
@@ -70,7 +70,7 @@ export function WayfernTermsDialog({
href="https://wayfern.com/tos"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm font-medium block"
className="block text-sm font-medium text-primary hover:underline"
>
https://wayfern.com/tos
</a>
+8 -8
View File
@@ -214,7 +214,7 @@ export function WelcomeDialog({
<h2 className="text-2xl font-semibold tracking-tight text-balance">
{t("welcome.license.title")}
</h2>
<p className="mx-auto max-w-[55ch] text-sm leading-6 text-pretty text-muted-foreground">
<p className="mx-auto max-w-[55ch] text-sm/6 text-pretty text-muted-foreground">
{t("welcome.license.body")}
</p>
</div>
@@ -286,7 +286,7 @@ export function WelcomeDialog({
<LuMic className="size-5 shrink-0" />
{t("welcome.permissions.title")}
</h2>
<p className="mx-auto max-w-[55ch] text-sm leading-6 text-pretty text-muted-foreground">
<p className="mx-auto max-w-[55ch] text-sm/6 text-pretty text-muted-foreground">
{t("welcome.permissions.desc")}
</p>
</div>
@@ -337,7 +337,7 @@ export function WelcomeDialog({
<LuTriangleAlert className="size-5 shrink-0" />
{t("welcome.ready.errorTitle")}
</h2>
<p className="max-w-[55ch] text-sm leading-6 text-pretty text-muted-foreground">
<p className="max-w-[55ch] text-sm/6 text-pretty text-muted-foreground">
{setup.error?.stage === "downloading"
? t("welcome.ready.errorDownload", {
browser: browserName,
@@ -371,7 +371,7 @@ export function WelcomeDialog({
<h2 className="text-2xl font-semibold tracking-tight text-balance">
{t("welcome.ready.title")}
</h2>
<p className="max-w-[55ch] text-sm leading-6 text-pretty text-muted-foreground">
<p className="max-w-[55ch] text-sm/6 text-pretty text-muted-foreground">
{setup.phase === "ready"
? t("welcome.ready.descReady")
: setup.phase === "extracting"
@@ -396,14 +396,14 @@ export function WelcomeDialog({
}}
/>
</div>
<div className="flex items-center justify-between text-sm tabular-nums text-muted-foreground">
<div className="flex items-center justify-between text-sm text-muted-foreground tabular-nums">
<span className="inline-flex items-center gap-1.5">
<LuLoaderCircle className="size-4 shrink-0 animate-spin" />
{t("welcome.ready.downloading")}
</span>
<span>{setup.downloadPercent}%</span>
</div>
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-0.5 text-xs tabular-nums text-muted-foreground">
<div className="flex flex-wrap items-center justify-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground tabular-nums">
<span>
{setup.totalBytes != null
? t("welcome.ready.stats", {
@@ -435,7 +435,7 @@ export function WelcomeDialog({
{setup.phase === "extracting" && (
<div className="flex w-full max-w-xs flex-col gap-2">
{setup.extractionOvertime ? (
<div className="flex items-center justify-center gap-1.5 text-sm tabular-nums text-muted-foreground">
<div className="flex items-center justify-center gap-1.5 text-sm text-muted-foreground tabular-nums">
<LuLoaderCircle className="size-4 shrink-0 animate-spin" />
{t("welcome.ready.almostFinished")}
</div>
@@ -455,7 +455,7 @@ export function WelcomeDialog({
}}
/>
</div>
<div className="flex items-center justify-between text-sm tabular-nums text-muted-foreground">
<div className="flex items-center justify-between text-sm text-muted-foreground tabular-nums">
<span className="inline-flex items-center gap-1.5">
<LuLoaderCircle className="size-4 shrink-0 animate-spin" />
{t("welcome.ready.extracting")}
+4 -4
View File
@@ -109,7 +109,7 @@ export function WindowDragArea() {
return (
<div
className="fixed top-0 right-0 z-50 flex items-center h-11 select-none"
className="fixed top-0 right-0 z-50 flex h-11 items-center select-none"
aria-hidden="false"
>
<button
@@ -117,7 +117,7 @@ export function WindowDragArea() {
onClick={() => {
void handleMinimize();
}}
className="flex items-center justify-center w-11 h-full hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
className="flex h-full w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
aria-label={t("common.window.minimize")}
>
<svg
@@ -136,7 +136,7 @@ export function WindowDragArea() {
onClick={() => {
void handleToggleMaximize();
}}
className="flex items-center justify-center w-11 h-full hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
className="flex h-full w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
aria-label={
isMaximized ? t("common.window.restore") : t("common.window.maximize")
}
@@ -175,7 +175,7 @@ export function WindowDragArea() {
onClick={() => {
void handleClose();
}}
className="flex items-center justify-center w-11 h-full hover:bg-destructive/90 transition-colors text-muted-foreground hover:text-destructive-foreground"
className="flex h-full w-11 items-center justify-center text-muted-foreground transition-colors hover:bg-destructive/90 hover:text-destructive-foreground"
aria-label={t("common.buttons.close")}
>
<svg
+1
View File
@@ -424,6 +424,7 @@ export interface WayfernConfig {
fingerprint?: string; // JSON string of the complete fingerprint config
randomize_fingerprint_on_launch?: boolean; // Generate new fingerprint on every launch
os?: WayfernOS; // Operating system for fingerprint generation
geo_proxy_signature?: string; // Internal: routing the fingerprint's location was computed for
}
// Wayfern fingerprint config - matches the C++ FingerprintData structure