feat(desktop): Tier A desktop features — web side (P5-46/36/44/43/49/47/55/57)
Web half of the desktop feature wave. A shared bridge (`hooks/useTauri.ts`: invokeTauri/isTauri/useTauriEvent) backs per-feature hooks that no-op in the browser and drive the native Tauri commands (compiled in cinny-desktop): - P5-46 useTauriCallPower — hold system awake while a call is active. - P5-36 useTauriJumpList — Windows jump list of recent rooms → matrix: deep links. - P5-44 useTauriThumbbar — taskbar Mute/Deafen/End; events toggle mic/sound/hangup. - P5-43 useTauriSmtc — SMTC call state + button events. - P5-49 useTauriNetwork — react to native network-change → mx.retryImmediately(). - P5-47 window chrome — opt-in `customWindowChromeAtom` + TDS `TitleBar`; DesktopChrome wrapper in App.tsx (zero layout impact when off) + a desktop-only settings toggle. - P5-55 composer toolbar drag-reorder (settings order[] + pragmatic-drag-and-drop). - P5-57 DraftIndicator — subtle "draft saved" cue in the composer. Client-scoped hooks mount via TauriDesktopFeatures in ClientNonUIFeatures; window chrome mounts at App level. Gates: tsc/eslint/prettier clean, build OK, 556 tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import React, { MouseEvent, ReactNode } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { Text } from 'folds';
|
||||
import { customWindowChromeAtom } from '../../state/customWindowChrome';
|
||||
import { invokeTauri, isTauri } from '../../hooks/useTauri';
|
||||
import * as css from './TitleBar.css';
|
||||
|
||||
/**
|
||||
* Detect macOS from the web side (no `tauri-plugin-os` dependency). We only need
|
||||
* a coarse "is this a Mac" signal to decide which side the window controls sit
|
||||
* on, so the UA/platform sniff is sufficient and stays cross-platform.
|
||||
*/
|
||||
const isMacOS = (): boolean => {
|
||||
const platform =
|
||||
(
|
||||
navigator as unknown as {
|
||||
userAgentData?: { platform?: string };
|
||||
}
|
||||
).userAgentData?.platform ??
|
||||
navigator.platform ??
|
||||
navigator.userAgent;
|
||||
return /mac/i.test(platform);
|
||||
};
|
||||
|
||||
const MIN_GLYPH = (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden>
|
||||
<rect x="1" y="4.5" width="8" height="1" fill="currentColor" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const MAX_GLYPH = (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden>
|
||||
<rect x="1" y="1" width="8" height="8" fill="none" stroke="currentColor" strokeWidth="1" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const CLOSE_GLYPH = (
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden>
|
||||
<path d="M1 1 L9 9 M9 1 L1 9" stroke="currentColor" strokeWidth="1" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
type ControlButtonProps = {
|
||||
label: string;
|
||||
glyph: ReactNode;
|
||||
onClick: () => void;
|
||||
close?: boolean;
|
||||
};
|
||||
|
||||
function ControlButton({ label, glyph, onClick, close }: ControlButtonProps) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={label}
|
||||
title={label}
|
||||
onClick={onClick}
|
||||
className={`${css.ControlButton}${close ? ` ${css.ControlButtonClose}` : ''}`}
|
||||
>
|
||||
{glyph}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* P5-47 — TDS Custom Window Chrome titlebar.
|
||||
*
|
||||
* Renders `null` unless we're inside Tauri **and** the user opted into custom
|
||||
* window chrome. Otherwise it draws a thin (~32px) folds/TDS-styled titlebar: a
|
||||
* draggable region (`data-tauri-drag-region`) with the app brand, plus
|
||||
* minimize / maximize / close controls that call the native window commands.
|
||||
*
|
||||
* OS-aware: Windows/Linux put the controls on the right; macOS mirrors them to
|
||||
* the left (the native traffic-light position) since decorations — and thus the
|
||||
* real traffic lights — are stripped while custom chrome is on.
|
||||
*/
|
||||
export function TitleBar() {
|
||||
const enabled = useAtomValue(customWindowChromeAtom);
|
||||
|
||||
if (!isTauri() || !enabled) return null;
|
||||
|
||||
const mac = isMacOS();
|
||||
|
||||
const handleDoubleClick = (evt: MouseEvent<HTMLDivElement>): void => {
|
||||
// Only the drag surface itself toggles maximize, not the brand/children.
|
||||
if (evt.target !== evt.currentTarget) return;
|
||||
invokeTauri('window_toggle_maximize');
|
||||
};
|
||||
|
||||
const controls = (
|
||||
<div className={css.Controls}>
|
||||
<ControlButton
|
||||
label="Minimize"
|
||||
glyph={MIN_GLYPH}
|
||||
onClick={() => invokeTauri('window_minimize')}
|
||||
/>
|
||||
<ControlButton
|
||||
label="Maximize"
|
||||
glyph={MAX_GLYPH}
|
||||
onClick={() => invokeTauri('window_toggle_maximize')}
|
||||
/>
|
||||
<ControlButton
|
||||
label="Close"
|
||||
glyph={CLOSE_GLYPH}
|
||||
onClick={() => invokeTauri('window_close')}
|
||||
close
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const dragRegion = (
|
||||
<div className={css.DragRegion} data-tauri-drag-region onDoubleClick={handleDoubleClick}>
|
||||
<span className={css.Brand}>
|
||||
<Text as="span" size="T200" truncate>
|
||||
Lotus Chat
|
||||
</Text>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<header className={css.TitleBar}>
|
||||
{mac ? (
|
||||
<>
|
||||
{controls}
|
||||
{dragRegion}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{dragRegion}
|
||||
{controls}
|
||||
</>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user