- Средняя кнопка тайтлбара теперь toggle maximize/restore (была hide-to-tray, но иконка Square вводила в заблуждение — выглядит как нативная maximize). Double-click по тайтлбару тоже работает. - Иконка свапается Square ↔ Copy в зависимости от max-state, aria-label локализован (titlebar.maximize_aria / restore_aria). - Новый IPC: toggleMaximizeMain, isMaximizedMain (invoke), evtMaximizeChanged (event main → renderer на maximize/unmaximize). - Фикс drag-зоны: titlebar-nodrag перенесён с обёртки правого кластера на сами кнопки. Из-за flex-1 basis-0 пустое место слева от кнопок раньше было no-drag — окно нельзя было ухватить рядом. - minWidth/minHeight окна 900x600 → 1100x700, чтобы Tailwind lg: всегда срабатывал (4 hero-stat в один ряд, heatmap без скролла). - CLAUDE.md: контекст проекта для будущих сессий Claude Code (стек, архитектура, команды, релиз, тех. долг, чего не делать).
115 lines
3.8 KiB
TypeScript
115 lines
3.8 KiB
TypeScript
import { Minus, X, Square, Copy, Menu } from 'lucide-react'
|
||
import { useEffect, useState } from 'react'
|
||
import { useT } from '../i18n'
|
||
|
||
type Props = {
|
||
title?: string
|
||
onMenuClick?: () => void
|
||
}
|
||
|
||
export function Titlebar({ title, onMenuClick }: Props): JSX.Element {
|
||
const { t } = useT()
|
||
const effectiveTitle = title ?? t('titlebar.app_title')
|
||
|
||
// Локально отслеживаем maximize-state, чтобы свапать иконку (квадрат ↔
|
||
// «двойной квадрат», как в нативной винде). Стартовое значение спрашиваем
|
||
// у main; дальше подписываемся на evtMaximizeChanged.
|
||
const [maximized, setMaximized] = useState(false)
|
||
useEffect(() => {
|
||
void window.api.isMaximizedMain().then(setMaximized)
|
||
const unsub = window.api.onMaximizeChanged((v) => setMaximized(v))
|
||
return unsub
|
||
}, [])
|
||
|
||
// Double-click по тайтлбару — стандартный Windows-жест для toggle maximize.
|
||
// Игнорируем клики по элементам с no-drag (кнопки/меню) — у них своя логика.
|
||
function onDoubleClick(e: React.MouseEvent<HTMLDivElement>): void {
|
||
const target = e.target as HTMLElement
|
||
if (target.closest('.titlebar-nodrag')) return
|
||
window.api.toggleMaximizeMain()
|
||
}
|
||
|
||
return (
|
||
<div
|
||
onDoubleClick={onDoubleClick}
|
||
className="titlebar-drag relative h-10 px-2 sm:px-3 flex items-center justify-between vibrancy hairline-b"
|
||
>
|
||
<div className="flex items-center gap-1 min-w-0 flex-1 basis-0">
|
||
{onMenuClick && (
|
||
<button
|
||
onClick={onMenuClick}
|
||
className="titlebar-nodrag md:hidden w-8 h-7 grid place-items-center rounded-md hover:bg-text/[0.08] text-text/65 hover:text-text transition-colors"
|
||
aria-label={t('titlebar.menu_aria')}
|
||
>
|
||
<Menu size={15} strokeWidth={2} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
|
||
<div className="text-[12px] font-medium text-text/55 truncate px-2">
|
||
{effectiveTitle}
|
||
</div>
|
||
|
||
{/* no-drag навешен на сами кнопки, не на обёртку: иначе из-за
|
||
flex-1 basis-0 весь кластер (включая пустое место слева от кнопок)
|
||
становится no-drag, и окно нельзя ухватить рядом с кнопками. */}
|
||
<div className="flex items-center justify-end gap-0.5 min-w-0 flex-1 basis-0">
|
||
<WinBtn
|
||
onClick={() => window.api.minimizeMain()}
|
||
label={t('titlebar.minimize_aria')}
|
||
>
|
||
<Minus size={13} strokeWidth={2} />
|
||
</WinBtn>
|
||
<WinBtn
|
||
onClick={() => window.api.toggleMaximizeMain()}
|
||
label={
|
||
maximized
|
||
? t('titlebar.restore_aria')
|
||
: t('titlebar.maximize_aria')
|
||
}
|
||
>
|
||
{maximized ? (
|
||
<Copy size={11} strokeWidth={2} />
|
||
) : (
|
||
<Square size={11} strokeWidth={2} />
|
||
)}
|
||
</WinBtn>
|
||
<WinBtn
|
||
onClick={() => window.api.closeMain()}
|
||
label={t('titlebar.close_aria')}
|
||
danger
|
||
>
|
||
<X size={13} strokeWidth={2} />
|
||
</WinBtn>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function WinBtn({
|
||
children,
|
||
onClick,
|
||
label,
|
||
danger = false
|
||
}: {
|
||
children: React.ReactNode
|
||
onClick: () => void
|
||
label: string
|
||
danger?: boolean
|
||
}): JSX.Element {
|
||
return (
|
||
<button
|
||
onClick={onClick}
|
||
aria-label={label}
|
||
className={[
|
||
'titlebar-nodrag w-9 h-7 grid place-items-center rounded-md transition-colors text-text/55',
|
||
danger
|
||
? 'hover:bg-destructive hover:text-white'
|
||
: 'hover:bg-text/[0.08] hover:text-text'
|
||
].join(' ')}
|
||
>
|
||
{children}
|
||
</button>
|
||
)
|
||
}
|