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 ( return (
<div className="app-shell flex h-screen w-screen overflow-hidden text-text-primary"> <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 <div
className={`app-sidebar-shell relative z-20 shrink-0 transition-all duration-300 ease-[cubic-bezier(0.2,0.8,0.2,1)] ${ 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' isSidebarVisible ? 'opacity-100' : 'opacity-0 pointer-events-none'
+8 -2
View File
@@ -18,6 +18,7 @@ import {
startActionLabel startActionLabel
} from '../utils/downloadActions'; } from '../utils/downloadActions';
import { isActiveDownloadStatus } from '../utils/downloads'; import { isActiveDownloadStatus } from '../utils/downloads';
import { usePlatformInfo } from '../utils/platform';
interface DownloadTableProps { interface DownloadTableProps {
filter: SidebarFilter; filter: SidebarFilter;
@@ -30,8 +31,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
const { downloads, queues, assignToQueue, toggleAddModal, openDeleteModal, redownload } = useDownloadStore(); const { downloads, queues, assignToQueue, toggleAddModal, openDeleteModal, redownload } = useDownloadStore();
const { isSidebarVisible, toggleSidebar } = useSettingsStore(); const { isSidebarVisible, toggleSidebar } = useSettingsStore();
const { addToast } = useToast(); const { addToast } = useToast();
const platform = usePlatformInfo();
const isMac = navigator.userAgent.includes('Mac'); 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 [contextMenu, setContextMenu] = useState<{ x: number; y: number; id: string } | null>(null);
const [animationParent] = useAutoAnimate<HTMLDivElement>(); const [animationParent] = useAutoAnimate<HTMLDivElement>();
@@ -373,7 +376,10 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
return ( return (
<div className="downloads-view flex-1 flex flex-col h-full min-w-0"> <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 && ( {!isSidebarVisible && (
<button <button
onClick={toggleSidebar} 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" className="w-full text-left px-3 py-2 hover:bg-item-hover transition-colors"
> >
Show in Finder Show in Folder
</button> </button>
<div className="h-[1px] bg-border-modal/60 my-1.5 mx-2"></div> <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 { useSettingsStore } from '../store/useSettingsStore';
import { invokeCommand as invoke } from '../ipc'; import { invokeCommand as invoke } from '../ipc';
import { KeyRound, ShieldAlert } from 'lucide-react'; import { KeyRound, ShieldAlert } from 'lucide-react';
import { usePlatformInfo } from '../utils/platform';
export const KeychainPermissionModal: React.FC = () => { export const KeychainPermissionModal: React.FC = () => {
const showKeychainModal = useSettingsStore(state => state.showKeychainModal); const showKeychainModal = useSettingsStore(state => state.showKeychainModal);
const setShowKeychainModal = useSettingsStore(state => state.setShowKeychainModal); const setShowKeychainModal = useSettingsStore(state => state.setShowKeychainModal);
const platform = usePlatformInfo();
const [isGranting, setIsGranting] = useState(false); const [isGranting, setIsGranting] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -13,16 +15,26 @@ export const KeychainPermissionModal: React.FC = () => {
return null; 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 () => { const handleGrant = async () => {
setIsGranting(true); setIsGranting(true);
setError(null); setError(null);
try { try {
const result = await invoke('grant_keychain_access'); const result = await invoke('grant_keychain_access');
if (result.persistent) { if (result.persistent) {
// Set all keychain-related state directly from the grant result // Keep state in sync with the grant result instead of rehydrating
// instead of calling hydratePairingToken() again, which would // before Zustand has persisted keychainAccessGranted.
// re-read the DB before Zustand's persist middleware has written
// keychainAccessGranted and could flip persistent back to false.
useSettingsStore.setState({ useSettingsStore.setState({
keychainAccessGranted: true, keychainAccessGranted: true,
extensionPairingToken: result.token, extensionPairingToken: result.token,
@@ -30,7 +42,7 @@ export const KeychainPermissionModal: React.FC = () => {
}); });
setShowKeychainModal(false); setShowKeychainModal(false);
} else { } else {
setError(result.error || 'Failed to grant keychain access.'); setError(result.error || `${storeName} is unavailable.`);
} }
} catch (e: any) { } catch (e: any) {
setError(e.toString()); setError(e.toString());
@@ -50,35 +62,42 @@ export const KeychainPermissionModal: React.FC = () => {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="px-5 py-4 border-b border-border-modal flex items-center gap-3"> <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" /> <KeyRound size={20} className="text-blue-500" />
</div> </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>
<div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed space-y-4"> <div className="px-5 py-6 flex-1 text-sm text-text-secondary leading-relaxed space-y-4">
<p> <p>
Firelink uses the browser extension to seamlessly capture downloads. Firelink uses the browser extension to capture downloads. To keep the extension paired after restarts,
To securely store the pairing token that connects the app and the extension, Firelink stores its pairing token in {storeName}.
we need access to this system's credential store.
</p> </p>
<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>
<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 && ( {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"> <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" /> <ShieldAlert size={14} className="mt-0.5 flex-shrink-0" />
<span>{error}</span> <span>{error}</span>
</div> </div>
)} )}
<div className="bg-bg-modal-accent p-3 rounded-lg border border-border-modal text-xs"> <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. <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>. You can enable secure storage anytime from <strong>Settings &gt; Integrations</strong>.
</div> </div>
</div> </div>
<div className="px-5 py-4 border-t border-border-modal flex justify-end gap-3 bg-bg-modal-accent"> <div className="px-5 py-4 border-t border-border-modal flex justify-end gap-3 bg-bg-modal-accent">
<button <button
onClick={handleLater} onClick={handleLater}
@@ -92,7 +111,7 @@ export const KeychainPermissionModal: React.FC = () => {
disabled={isGranting} 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" 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> </button>
</div> </div>
</div> </div>
+37 -20
View File
@@ -1,37 +1,54 @@
import { getCurrentWindow } from '@tauri-apps/api/window'; import { getCurrentWindow } from '@tauri-apps/api/window';
import { Minus, Square, X } from 'lucide-react'; import { Minus, Square, X } from 'lucide-react';
import type { PointerEvent } from 'react';
const appWindow = getCurrentWindow(); const appWindow = getCurrentWindow();
const stopTitlebarDrag = (event: PointerEvent<HTMLButtonElement>) => {
event.stopPropagation();
};
export function WindowControls() { export function WindowControls() {
return ( return (
<div className="window-controls" aria-label="Window controls"> <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 <button
type="button" type="button"
className="window-control close" className="window-control close"
title="Close" title="Close"
aria-label="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> </button>
</div> </div>
); );
+45 -18
View File
@@ -1285,46 +1285,73 @@
.window-controls { .window-controls {
position: fixed; position: fixed;
top: 10px; top: 15px;
left: 12px; left: 22px;
z-index: 60; z-index: 60;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 7px; gap: 8px;
-webkit-app-region: no-drag;
pointer-events: auto;
} }
.window-control { .window-control {
width: 22px; width: 14px;
height: 22px; height: 14px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: hsl(var(--text-muted)); color: hsl(0 0% 10% / 0);
border-radius: 999px; 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 { .window-control:hover {
color: hsl(var(--text-primary)); color: hsl(0 0% 8% / 0.68);
background: hsl(var(--item-hover)); filter: saturate(1.14) brightness(1.05);
} }
.window-control.close:hover { .window-control:active {
color: white; transform: scale(0.92);
background: #c42b1c; filter: brightness(0.9);
} }
.main-titlebar { .main-titlebar {
height: 52px; height: 52px;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0 18px; padding: 0 18px;
border-bottom: 1px solid hsl(var(--border-color)); border-bottom: 1px solid hsl(var(--border-color));
background: hsl(var(--statusbar-bg)); background: hsl(var(--statusbar-bg));
} }
.main-titlebar-title { .main-titlebar--custom-controls-collapsed {
font-size: 14px; padding-left: 124px;
font-weight: 700; }
.main-titlebar-title {
font-size: 14px;
font-weight: 700;
color: hsl(var(--text-primary)); color: hsl(var(--text-primary));
letter-spacing: -0.01em; letter-spacing: -0.01em;
} }