Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@

## [Не выпущено]

### Исправлено

- Панель на двух мониторах больше не уезжает к чужому краю. Строка меню в
полноэкранном режиме прячется, уводя окно значка на 30 точек вверх — за
верхнюю кромку своего экрана, а если сверху вплотную стоит второй монитор, то
и на его территорию. Панель считалась от границ соседа и повисала у его
нижнего края, оторванная от строки меню на пол-экрана. Теперь экран
определяется по верхней кромке, до которой значку ближе всего, и спрятанная
строка меню этого не сбивает.

## [0.1.11] — 2026-08-16

### Добавлено
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ ARCH=arm64 ./scripts/make-dmg.sh # dist/ClaudeWeek-<версия>-arm64.dmg
| [docs/USAGE.md](docs/USAGE.md) | руководство: доступ, панель, расчёт плана, все настройки и ключи конфига, командная строка |
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | карта кода: кто за что отвечает, потоки данных, инварианты, рецепты правок |
| [docs/API.md](docs/API.md) | официальный источник: схема ответа, токен, дисциплина запросов |
| [docs/SPACES.md](docs/SPACES.md) | панель и рабочие столы: почему «не открывается по щелчку», измерения и пробы для разбора |
| [docs/SPACES.md](docs/SPACES.md) | панель, рабочие столы и мониторы: почему «не открывается по щелчку» и почему вставала у чужого края, измерения и пробы для разбора |
| [docs/PLAN.md](docs/PLAN.md) | план работ, модель данных, разбор рисков и отвергнутых вариантов |
| [docs/ROADMAP.md](docs/ROADMAP.md) | чего не хватает и что делать дальше |

Expand Down
33 changes: 32 additions & 1 deletion Sources/ClaudeWeekApp/DropdownPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -246,14 +246,45 @@ final class DropdownPanel {
x: anchor.frame.midX - size.width / 2,
y: anchor.frame.minY - size.height
)
if let screen = (anchor.screen ?? NSScreen.main)?.frame {
if let screen = screen(for: anchor)?.frame {
let margin = Theme.panelScreenMargin
origin.x = min(max(origin.x, screen.minX + margin), screen.maxX - size.width - margin)
// Верхняя кромка — не выше своего экрана. Строка меню в
// полноэкранном режиме прячется вместе со значком за верхний край,
// и панель, приклеенная к значку, ушла бы туда же — на соседний
// монитор, если он стоит сверху.
origin.y = min(origin.y, screen.maxY - size.height)
origin.y = max(origin.y, screen.minY + margin)
}
return NSRect(origin: origin, size: size)
}

/// Экран, на строке меню которого стоит значок.
///
/// `anchor.screen` на этот вопрос отвечает неверно. В полноэкранном режиме
/// строка меню прячется, и её окно вместе со значком уезжает вверх ровно на
/// свою высоту — за верхнюю кромку своего экрана. Стоит сверху вплотную
/// второй монитор, и значок оказывается уже на его территории: `anchor.screen`
/// честно отвечает «второй», панель считается от чужих границ и прижимается
/// к его нижнему краю. Так она и висела посреди верхнего монитора, оторванная
/// от строки меню. `NSScreen.main` на замену не годится тем более: это экран
/// с клавиатурным фокусом, то есть чужого окна, а не нашего значка.
///
/// Признак надёжнее: строка меню всегда прижата к верхней кромке своего
/// экрана. Значит значок принадлежит тому из накрывающих его по горизонтали
/// экранов, чья верхняя кромка к нему ближе, — и спрятанная строка меню
/// с её сдвигом на 30 точек этого не меняет.
private func screen(for anchor: NSWindow) -> NSScreen? {
let icon = anchor.frame
let covering = NSScreen.screens.filter {
$0.frame.minX <= icon.midX && icon.midX <= $0.frame.maxX
}
// Значок, не влезший в строку меню, лежит левее всех экранов — по
// горизонтали его не накрывает никто, и выбирать приходится из всех.
return (covering.isEmpty ? NSScreen.screens : covering)
.min { abs($0.frame.maxY - icon.maxY) < abs($1.frame.maxY - icon.maxY) }
}

/// Содержимое меняет высоту на ходу: приходят данные, появляется строка
/// сессии или прогноза, раскрывается недельный ряд. Растём вниз — верхняя
/// кромка приклеена к строке меню и дёргаться не должна.
Expand Down
30 changes: 23 additions & 7 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ SwiftUI` — значит, расчёт просочился в UI или нао
| `main.swift` | разбор аргументов командной строки, режимы `--json`, `--calibrate`, `--screenshot`, `--icon`, `--update`; запуск `NSApplication` в режиме `.accessory`. Незнакомый флаг — ошибка с кодом 2 (`CLI.isKnown`), а не молчаливый запуск иконки |
| `StatusItemController.swift` | **склейка всего**: пункт строки меню, таймеры, сон/пробуждение, смена таймзоны, перечитывание конфига, открытие панели и настроек |
| `PanelModel.swift` | состояние для SwiftUI: снимок, статус, текущий момент, производные подписи |
| `DropdownPanel.swift` | окно панели: форма как у системного меню, материал, обводка, позиция под строкой меню, закрытие по клику мимо, закрепление на время настроек |
| `DropdownPanel.swift` | окно панели: форма как у системного меню, материал, обводка, позиция под строкой меню своего монитора, закрытие по клику мимо, закрепление на время настроек |
| `PopoverView.swift` | вёрстка панели: заголовок, сессия, ряд дней (семь строк или одни текущие сутки — `appearance.panelLayout`), футер |
| `DayBar.swift` | одна двухцветная полоса и строка дня; в компактном виде строка ещё и раскрывает неделю кликом (`DayRowTap`), а цифры справа открывают разбивку по моделям (`valueTap`) |
| `SessionRow.swift` | полоса пятичасовой сессии; её процент нажимается так же, как цифры дня |
Expand Down Expand Up @@ -260,16 +260,21 @@ mtime) — атомарная запись меняет inode, поэтому н
тут же открылась по `handleClick`.
3. **Размер окна панели ведётся вручную** (`SizingHostingView`): `NSHostingView`
не двигает окно, когда SwiftUI меняет высоту контента.
4. **Цвет нигде не единственный носитель смысла**: рядом с каждой полосой
4. **Монитор панели выбирается по кромке, а не по `anchor.screen`**
(`DropdownPanel.screen(for:)`): спрятанная в полноэкранном режиме строка меню
уводит окно значка за верхний край своего экрана — и на соседний монитор,
если тот стоит вплотную сверху. Своим считается экран, к чьей верхней кромке
значок ближе.
5. **Цвет нигде не единственный носитель смысла**: рядом с каждой полосой
числа «факт / план», у перерасхода — значок ⚠.
5. **Родная палитра проверена валидатором** на дальтонизм и контраст;
6. **Родная палитра проверена валидатором** на дальтонизм и контраст;
остальные темы — игровая площадка, о чём написано и в настройках.
6. **Колонка значений размечена по самой длинной своей строке**
7. **Колонка значений размечена по самой длинной своей строке**
(`Theme.valueWidth` меряет «⚠ 100 / 100 %» шрифтом панели). Числом её
задавать нельзя: кегль берётся у системного меню и растёт в универсальном
доступе, а не влезшее значение переносится на второй ряд и тянет за собой
высоту всей панели. Пункты на это отдаёт полоса — её длина условная.
7. **Сброс недели панель ждёт будильником, а не плановым опросом**
8. **Сброс недели панель ждёт будильником, а не плановым опросом**
(`StatusItemController.scheduleResetRefresh`): момент известен заранее, он
же написан в заголовке. `tick` раз в минуту работает страховкой на случай,
когда будильник не сработал — машина спала, часы перевели, снимка на момент
Expand Down Expand Up @@ -339,14 +344,16 @@ mtime) — атомарная запись меняет inode, поэтому н
swift scripts/probe-usage.swift # один запрос, печатает ответ целиком
```

### Разобрать «панель не открывается по щелчку»
### Разобрать «панель не открывается по щелчку» или «встала не там»

```bash
swift scripts/probe-panel.swift --watch # сама погасла или это был щелчок
swift scripts/probe-panel.swift --behaviors # прежде чем менять collectionBehavior
swift scripts/probe-panel.swift --displays # два монитора: где лежит окно значка
```

Что означают строки и на что смотреть — [SPACES.md](SPACES.md).
Что означают строки и на что смотреть — [SPACES.md](SPACES.md): §1–6 про
рабочие столы, §7 про панель у чужого края на двух мониторах.

---

Expand Down Expand Up @@ -492,6 +499,15 @@ Intel — собирает у себя, `install.sh` соберёт нативн
- **`isShown` и `isOpen` — разные вопросы.** `isShown` («человек видит панель
здесь») спрашивают `togglePanel` и `resize`; `isOpen` («есть ли что
закрывать») — `close`, монитор кликов, `pinToAnchor`, `hideWhilePinned`.
- **`anchor.screen` врёт про монитор значка.** Строка меню в полноэкранном
режиме прячется, и её окно вместе со значком уезжает вверх ровно на свою
высоту — за верхнюю кромку своего экрана. Стоит сверху вплотную второй
монитор, и значок оказывается на его территории: `anchor.screen` отвечает
«второй», панель клэмпится по его границам и повисает у нижнего края чужого
монитора. `NSScreen.main` не спасает — это экран с клавиатурным фокусом, то
есть чужого окна. Экран выбирает `DropdownPanel.screen(for:)` по признаку,
который сдвиг строки меню переживает: у своего экрана верхняя кромка к значку
ближе всех. Разбор и замеры — в [SPACES.md](SPACES.md), §7.
- **`byAdding: .hour` — это плюс 3600 секунд, а не «тот же час завтра».**
Границы рабочего дня ищутся `date(bySettingHour:…)` от местной полуночи:
прибавление часов давало в сутки осеннего перевода начало в 10:00, в сутки
Expand Down
6 changes: 6 additions & 0 deletions docs/L10N.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
будут жить** переводы, **что сломается** по дороге и **в каком порядке** это
делать. Кода он не содержит — это план, а не отчёт.

> **Статус: выполнено в 0.1.11** (2026-08-16). План читается как описание
> принятых решений — что переводим, где живут строки, как выбирается язык, —
> но написан он до работы и в будущем времени. Что вышло по факту: раздел
> [0.1.11 в журнале](../CHANGELOG.md) и [ROADMAP](ROADMAP.md#2026-08-16);
> нынешнее устройство — `Sources/ClaudeWeekCore/Localization.swift`.

## Оглавление

- [Сколько текста на самом деле](#сколько-текста-на-самом-деле)
Expand Down
7 changes: 6 additions & 1 deletion docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,11 @@ codesign --force --sign - ClaudeWeek.app # ad-hoc подпись
анимации, фон — материал `.menu` с тонкой вуалью поверх (`Theme.panelTint`),
скругление 12 pt. `NSPopover` не подошёл: стрелку не убрать, отступает от
строки и рисует свой непрозрачный фон.
- Место панели считается от окна значка, а монитор — по верхней кромке, к
которой значок ближе всего (`DropdownPanel.screen(for:)`). Спрашивать
`anchor.screen` нельзя: спрятанная в полноэкранном режиме строка меню уводит
значок за верхний край своего экрана, а у вплотную стоящих мониторов это уже
территория соседа — [SPACES.md](SPACES.md), §7.
- Плотность вуали идёт по шкале с потолком `Theme.maxPanelTint` (0.55): на
полной вуаль гасила материал целиком, и «прозрачный фон с размытием»
отличался от сплошного одним оттенком. Глухой фон — отдельный режим
Expand Down Expand Up @@ -648,7 +653,7 @@ assert-хелперов, запускаемый `swift run ClaudeWeekTests`; н
= падение. Когда/если появится Xcode, таргет переносится на swift-testing без
изменения самих проверок.

Что покрыто (336 проверок, `swift run ClaudeWeekTests`):
Что покрыто (439 проверок, `swift run ClaudeWeekTests`):

1. **Окно недели.** Момент ровно на сбросе; за минуту до и после; полсекунды до;
воскресный полдень; смена таймзоны между вызовами; другой день сброса.
Expand Down
Loading