fix(ui): align cross-platform chrome

This commit is contained in:
NimBold
2026-07-01 14:29:22 +03:30
parent 21fc115d98
commit fdca17910a
5 changed files with 128 additions and 59 deletions
+1 -1
View File
@@ -577,7 +577,7 @@ function App() {
return (
<div className="app-shell flex h-screen w-screen overflow-hidden text-text-primary">
{platform.os === 'windows' && <WindowControls />}
{(platform.os === 'windows' || platform.os === 'linux') && <WindowControls />}
<div
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${
isSidebarVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
+8 -2
View File
@@ -18,6 +18,7 @@ import {
startActionLabel
} from '../utils/downloadActions';
import { isActiveDownloadStatus } from '../utils/downloads';
import { usePlatformInfo } from '../utils/platform';
interface DownloadTableProps {
filter: SidebarFilter;
@@ -30,8 +31,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, queues, assignToQueue, toggleAddModal, openDeleteModal, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const { addToast } = useToast();
const platform = usePlatformInfo();
const isMac = navigator.userAgent.includes('Mac');
const usesCustomWindowControls = !isMac && platform.os !== 'macos';
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [animationParent] = useAutoAnimate<HTMLDivElement>();
@@ -373,7 +376,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
return (
<div className="downloads-view flex-1 flex flex-col h-full min-w-0">
<div className={`main-titlebar ${!isSidebarVisible ? 'pl-[80px]' : ''}`} data-tauri-drag-region>
<div
className={`main-titlebar ${!isSidebarVisible ? (usesCustomWindowControls ? 'main-titlebar--custom-controls-collapsed' : 'pl-[88px]') : ''}`}
data-tauri-drag-region
>
{!isSidebarVisible && (
<button
onClick={toggleSidebar}
@@ -647,7 +653,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
}}
className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
>
Show in Finder
Show in Folder
</button>
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div>
+37 -18
View File
@@ -2,10 +2,12 @@ import React, { useState } from 'react';
import { useSettingsStore } from '../store/useSettingsStore';
import { invokeCommand as invoke } from '../ipc';
import { KeyRound, ShieldAlert } from 'lucide-react';
import { usePlatformInfo } from '../utils/platform';
export const KeychainPermissionModal: React.FC = () => {
const showKeychainModal = useSettingsStore(state => state.showKeychainModal);
const setShowKeychainModal = useSettingsStore(state => state.setShowKeychainModal);
const platform = usePlatformInfo();
const [isGranting, setIsGranting] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -13,16 +15,26 @@ export const KeychainPermissionModal: React.FC = () => {
return null;
}
const isMac = platform.os === 'macos';
const storeName =
platform.os === 'windows'
? 'Windows Credential Manager'
: platform.os === 'linux'
? 'your Linux credential store'
: platform.os === 'macos'
? 'macOS Keychain'
: "this system's credential store";
const grantLabel = isMac ? 'Grant Access' : 'Enable Secure Storage';
const handleGrant = async () => {
setIsGranting(true);
setError(null);
try {
const result = await invoke('grant_keychain_access');
if (result.persistent) {
// Set all keychain-related state directly from the grant result
// instead of calling hydratePairingToken() again, which would
// re-read the DB before Zustand's persist middleware has written
// keychainAccessGranted and could flip persistent back to false.
// Keep state in sync with the grant result instead of rehydrating
// before Zustand has persisted keychainAccessGranted.
useSettingsStore.setState({
keychainAccessGranted: true,
extensionPairingToken: result.token,
@@ -30,7 +42,7 @@ export const KeychainPermissionModal: React.FC = () => {
});
setShowKeychainModal(false);
} else {
setError(result.error || 'Failed to grant keychain access.');
setError(result.error || `${storeName} is unavailable.`);
}
} catch (e: any) {
setError(e.toString());
@@ -50,35 +62,42 @@ export const KeychainPermissionModal: React.FC = () => {
onClick={(e) => e.stopPropagation()}
>
<div className="px-5 py-4 border-b border-border-modal flex items-center gap-3">
<div className="p-2 bg-blue-500/10 rounded-full flex items-center justify-center">
<div className="p-2 bg-blue-500/10 rounded-full items-center justify-center">
<KeyRound size={20} className="text-blue-500" />
</div>
<h2 className="text-lg font-semibold text-text-primary m-0">Credential Storage Access Needed</h2>
<h2 className="text-lg font-semibold text-text-primary m-0">Credential Storage Access Needed</h2>
</div>
<div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed space-y-4">
<p>
Firelink uses the browser extension to seamlessly capture downloads.
To securely store the pairing token that connects the app and the extension,
we need access to this system's credential store.
Firelink uses the browser extension to capture downloads. To keep the extension paired after restarts,
Firelink stores its pairing token in {storeName}.
</p>
<p>
<strong>Note:</strong> Firelink only requests access to its own dedicated credential entry. It cannot and will not access other saved passwords or credential items on your system.
{isMac
? 'macOS may show a Keychain prompt after you grant access.'
: 'This usually completes silently. If the credential service is unavailable, Firelink will show the error here and the extension will stay paired for this session only.'}
</p>
<p>
<strong>Note:</strong> Firelink only writes its own dedicated credential entry. It cannot access other
saved passwords or credential items on your system.
</p>
{error && (
<div className="flex items-start gap-2 text-red-400 bg-red-400/10 p-3 rounded-lg border border-red-400/20 text-xs">
<ShieldAlert size={14} className="mt-0.5 flex-shrink-0" />
<span>{error}</span>
</div>
)}
<div className="bg-bg-modal-accent p-3 rounded-lg border border-border-modal text-xs">
<strong>Hint:</strong> If you select Later, the extension will only work for this session.
You can grant access anytime from <strong>Settings &gt; Integrations</strong>.
<strong>Hint:</strong> If you select Later, the extension will only work for this session.
You can enable secure storage anytime from <strong>Settings &gt; Integrations</strong>.
</div>
</div>
<div className="px-5 py-4 border-t border-border-modal flex justify-end gap-3 bg-bg-modal-accent">
<button
onClick={handleLater}
@@ -92,7 +111,7 @@ export const KeychainPermissionModal: React.FC = () => {
disabled={isGranting}
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors bg-accent text-white hover:bg-accent/90 disabled:opacity-50"
>
{isGranting ? 'Granting...' : 'Grant Access'}
{isGranting ? 'Enabling...' : grantLabel}
</button>
</div>
</div>
+37 -20
View File
@@ -1,37 +1,54 @@
import { getCurrentWindow } from '@tauri-apps/api/window';
import { Minus, Square, X } from 'lucide-react';
import type { PointerEvent } from 'react';
const appWindow = getCurrentWindow();
const stopTitlebarDrag = (event: PointerEvent<HTMLButtonElement>) => {
event.stopPropagation();
};
export function WindowControls() {
return (
<div className="window-controls" aria-label="Window controls">
<button
type="button"
className="window-control"
title="Minimize"
aria-label="Minimize"
onClick={() => void appWindow.minimize()}
>
<Minus size={13} strokeWidth={2.25} />
</button>
<button
type="button"
className="window-control"
title="Maximize"
aria-label="Maximize"
onClick={() => void appWindow.toggleMaximize()}
>
<Square size={11} strokeWidth={2.25} />
</button>
<button
type="button"
className="window-control close"
title="Close"
aria-label="Close"
onClick={() => void appWindow.close()}
onPointerDown={stopTitlebarDrag}
onClick={(event) => {
event.stopPropagation();
void appWindow.close();
}}
>
<X size={14} strokeWidth={2.25} />
<X size={10} strokeWidth={3} />
</button>
<button
type="button"
className="window-control minimize"
title="Minimize"
aria-label="Minimize"
onPointerDown={stopTitlebarDrag}
onClick={(event) => {
event.stopPropagation();
void appWindow.minimize();
}}
>
<Minus size={10} strokeWidth={3} />
</button>
<button
type="button"
className="window-control maximize"
title="Maximize"
aria-label="Maximize"
onPointerDown={stopTitlebarDrag}
onClick={(event) => {
event.stopPropagation();
void appWindow.toggleMaximize();
}}
>
<Square size={8} strokeWidth={3} />
</button>
</div>
);
+45 -18
View File
@@ -1285,46 +1285,73 @@
.window-controls {
position: fixed;
top: 10px;
left: 12px;
top: 15px;
left: 22px;
z-index: 60;
display: inline-flex;
align-items: center;
gap: 7px;
gap: 8px;
-webkit-app-region: no-drag;
pointer-events: auto;
}
.window-control {
width: 22px;
height: 22px;
width: 14px;
height: 14px;
display: inline-flex;
align-items: center;
justify-content: center;
color: hsl(var(--text-muted));
color: hsl(0 0% 10% / 0);
border-radius: 999px;
border: 0.5px solid hsl(0 0% 0% / 0.28);
box-shadow:
inset 0 1px 0 hsl(0 0% 100% / 0.26),
0 1px 2px hsl(0 0% 0% / 0.16);
transition:
filter 120ms ease,
color 120ms ease,
transform 120ms ease;
-webkit-app-region: no-drag;
}
.window-control.close {
background: #ff5f57;
}
.window-control.minimize {
background: #ffbd2e;
}
.window-control.maximize {
background: #28c840;
}
.window-control:hover {
color: hsl(var(--text-primary));
background: hsl(var(--item-hover));
color: hsl(0 0% 8% / 0.68);
filter: saturate(1.14) brightness(1.05);
}
.window-control.close:hover {
color: white;
background: #c42b1c;
.window-control:active {
transform: scale(0.92);
filter: brightness(0.9);
}
.main-titlebar {
height: 52px;
display: flex;
align-items: center;
padding: 0 18px;
align-items: center;
padding: 0 18px;
border-bottom: 1px solid hsl(var(--border-color));
background: hsl(var(--statusbar-bg));
}
background: hsl(var(--statusbar-bg));
}
.main-titlebar-title {
font-size: 14px;
font-weight: 700;
.main-titlebar--custom-controls-collapsed {
padding-left: 124px;
}
.main-titlebar-title {
font-size: 14px;
font-weight: 700;
color: hsl(var(--text-primary));
letter-spacing: -0.01em;
}