feat(window): maximize toggle + drag-zone fix + minWidth bump

- Средняя кнопка тайтлбара теперь 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
  (стек, архитектура, команды, релиз, тех. долг, чего не делать).
This commit is contained in:
AnRil
2026-05-19 17:52:54 +07:00
parent 2503b27d42
commit e96ca06587
7 changed files with 251 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
import { Minus, X, Square, Menu } from 'lucide-react'
import { Minus, X, Square, Copy, Menu } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useT } from '../i18n'
type Props = {
@@ -10,8 +11,29 @@ 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 className="titlebar-drag relative h-10 px-2 sm:px-3 flex items-center justify-between vibrancy hairline-b">
<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
@@ -28,7 +50,10 @@ export function Titlebar({ title, onMenuClick }: Props): JSX.Element {
{effectiveTitle}
</div>
<div className="titlebar-nodrag flex items-center justify-end gap-0.5 min-w-0 flex-1 basis-0">
{/* 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')}
@@ -36,10 +61,18 @@ export function Titlebar({ title, onMenuClick }: Props): JSX.Element {
<Minus size={13} strokeWidth={2} />
</WinBtn>
<WinBtn
onClick={() => window.api.hideMain()}
label={t('titlebar.tray_aria')}
onClick={() => window.api.toggleMaximizeMain()}
label={
maximized
? t('titlebar.restore_aria')
: t('titlebar.maximize_aria')
}
>
<Square size={11} strokeWidth={2} />
{maximized ? (
<Copy size={11} strokeWidth={2} />
) : (
<Square size={11} strokeWidth={2} />
)}
</WinBtn>
<WinBtn
onClick={() => window.api.closeMain()}
@@ -69,7 +102,7 @@ function WinBtn({
onClick={onClick}
aria-label={label}
className={[
'w-9 h-7 grid place-items-center rounded-md transition-colors text-text/55',
'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'

View File

@@ -21,6 +21,8 @@ export const ru: Dict = {
'sidebar.status_tracking': 'Активность отслеживается',
'titlebar.menu_aria': 'Меню',
'titlebar.minimize_aria': 'Свернуть',
'titlebar.maximize_aria': 'Развернуть',
'titlebar.restore_aria': 'Восстановить размер',
'titlebar.tray_aria': 'В трей',
'titlebar.close_aria': 'Закрыть',
'titlebar.app_title': 'Exercise Reminder',
@@ -265,6 +267,8 @@ export const en: Dict = {
'sidebar.status_tracking': 'Activity tracking is on',
'titlebar.menu_aria': 'Menu',
'titlebar.minimize_aria': 'Minimize',
'titlebar.maximize_aria': 'Maximize',
'titlebar.restore_aria': 'Restore size',
'titlebar.tray_aria': 'To tray',
'titlebar.close_aria': 'Close',
'titlebar.app_title': 'Exercise Reminder',