diff --git a/CHANGELOG.md b/CHANGELOG.md
index e05e49e..140a4da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,21 @@ is the place for the shorter, human-facing story.
## [Unreleased]
+### Changed
+
+- New app icon: the ring from the menu bar, drawn large on an ink-coloured
+ tile — green from 0 to 80 %, amber to 90 %, red closing the circle to 100 %,
+ in the same colours the panel uses. The old icon was three week bars, and
+ below 32 pt they merged into a green smudge; the ring survives down to 16 pt,
+ which is the size Spotlight and the "Open with" list show. Thresholds of the
+ icon are deliberately not the notification defaults (80 / 95): five per cent
+ of a circle is one pixel at that size, and red would not be visible at all.
+- Notification screenshots in the README and the guide retaken — the banners
+ carry the app icon, so both showed the old one. The README now opens with the
+ icon as well (`docs/images/app-icon.png`).
+
+## [0.1.9] — 2026-08-16
+
### Added
- Notifications when spend crosses a threshold you set: two thresholds per
@@ -40,6 +55,24 @@ is the place for the shorter, human-facing story.
- `Formatting.longDuration` — the same interval in words ("2 дня 4 часа",
"1 час 12 минут"), with Russian pluralisation. The panel keeps the short form;
a banner has room for the long one.
+
+### Notes
+
+- Notifications are on out of the box: a feature that stays silent until you
+ find its tab does not do the job it was added for. macOS asks for permission
+ once, on the first launch of the new version; if it is refused, the tab says
+ so and offers the only place the ban can be lifted — System Settings. A build
+ run straight from `swift run` has no notifications at all: macOS identifies an
+ app by its bundle, and there isn't one.
+- Session notifications need the official source. The local estimate does not
+ compute the 5-hour percentage at all, so offline there is nothing to warn
+ about — and an expired session stays quiet rather than warning off a cached
+ number that reset hours ago.
+
+## [0.1.8] — 2026-08-15
+
+### Added
+
- Model breakdown in the panel: clicking the percentage — on a day row or on
the 5-hour session row — replaces the day rows with one row per model (Opus,
Sonnet, Haiku), each with the same kind of bar showing its share of the
@@ -63,16 +96,6 @@ is the place for the shorter, human-facing story.
### Notes
-- Notifications are on out of the box: a feature that stays silent until you
- find its tab does not do the job it was added for. macOS asks for permission
- once, on the first launch of the new version; if it is refused, the tab says
- so and offers the only place the ban can be lifted — System Settings. A build
- run straight from `swift run` has no notifications at all: macOS identifies an
- app by its bundle, and there isn't one.
-- Session notifications need the official source. The local estimate does not
- compute the 5-hour percentage at all, so offline there is nothing to warn
- about — and an expired session stays quiet rather than warning off a cached
- number that reset hours ago.
- The breakdown is computed from local transcripts in `~/.claude/projects` and
weighted by model prices, so it is an estimate — which the panel states
plainly while the breakdown is on screen. The official `/api/oauth/usage`
@@ -175,7 +198,9 @@ seven days of the week window, the 5-hour session, official numbers from
`/api/oauth/usage` with a local estimate as fallback, settings window, MIT
licence, CI, and a disk image built by tag for Apple Silicon.
-[Unreleased]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.7...HEAD
+[Unreleased]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.9...HEAD
+[0.1.9]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.8...v0.1.9
+[0.1.8]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.7...v0.1.8
[0.1.7]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.6...v0.1.7
[0.1.6]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.5...v0.1.6
[0.1.5]: https://github.com/Greem4/ClaudeWeek/compare/v0.1.4...v0.1.5
diff --git a/README.md b/README.md
index 67feee0..aee60c1 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+
+
+
+
# ClaudeWeek
[](https://github.com/Greem4/ClaudeWeek/actions/workflows/ci.yml)
diff --git a/Sources/ClaudeWeekApp/AppIcon.swift b/Sources/ClaudeWeekApp/AppIcon.swift
index dce09e0..2aa54d7 100644
--- a/Sources/ClaudeWeekApp/AppIcon.swift
+++ b/Sources/ClaudeWeekApp/AppIcon.swift
@@ -1,15 +1,30 @@
import AppKit
-/// Иконка приложения — та же метафора, что и в панели: три полосы недели,
-/// где зелёная упирается в янтарную. Рисуем кодом, потому что без Xcode
-/// собрать ассет-каталог нечем.
+/// Иконка приложения — то же кольцо, что висит в строке меню, только крупно:
+/// шкала недельного лимита, разложенная по порогам. Рисуем кодом, потому что
+/// без Xcode собрать ассет-каталог нечем.
///
-/// Цвета берутся из родной палитры, тёмной её половины: иконка на плитке
-/// Launchpad живёт своей жизнью и на тему системы не смотрит, но роли у полос
+/// Цвета дуг берутся из родной палитры, тёмной её половины: иконка на плитке
+/// Launchpad живёт своей жизнью и на тему системы не смотрит, но роли у цветов
/// те же, что в панели, — и расходиться их оттенки не должны.
enum AppIcon {
private static let ink = Palette.system
+ /// Где зелёное сменяется янтарным и янтарное — красным. Числа не совпадают
+ /// с порогами уведомлений (80 и 95 по умолчанию) намеренно: на 16 pt пять
+ /// процентов круга вырождаются в одну точку, и красного в знаке попросту
+ /// не видно. Тридцать шесть градусов — минимум, который переживает
+ /// уменьшение до иконки в Spotlight.
+ private static let warningStart: CGFloat = 80
+ private static let criticalStart: CGFloat = 90
+
+ /// Плитка. Своих цветов у неё в палитре нет: фон панели рассчитан на текст
+ /// поверх материала, а плитке нужен градиент, который читается на любом
+ /// рабочем столе. Холодный графит выбран под кольцо — тёплый серый рядом с
+ /// зелёным желтит.
+ private static let plateTop: UInt32 = 0x2A2E38
+ private static let plateBottom: UInt32 = 0x111318
+
/// Пары «имя файла в .iconset — сторона в пикселях».
static let variants: [(name: String, pixels: Int)] = [
("icon_16x16", 16), ("icon_16x16@2x", 32),
@@ -48,68 +63,92 @@ enum AppIcon {
private static func draw(side: CGFloat) -> NSImage {
NSImage(size: NSSize(width: side, height: side), flipped: false) { _ in
- // macOS сама не скругляет иконку — рисуем «сквиркл» вручную,
- // с отступом по гайдлайну (примерно десятая часть стороны).
- let inset = side * 0.08
- let plate = NSRect(x: inset, y: inset, width: side - inset * 2, height: side - inset * 2)
- let corner = plate.width * 0.22
+ let plate = plateRect(side: side)
+ fillPlate(plate)
+ drawRing(in: plate)
+ return true
+ }
+ }
- NSColor(hex: ink.panelBackground.dark).setFill()
- NSBezierPath(roundedRect: plate, xRadius: corner, yRadius: corner).fill()
+ /// Отступ по гайдлайну: плитка не занимает квадрат целиком.
+ private static func plateRect(side: CGFloat) -> NSRect {
+ let inset = side * 0.055
+ return NSRect(x: inset, y: inset, width: side - inset * 2, height: side - inset * 2)
+ }
- let barHeight = plate.height * 0.11
- let spacing = plate.height * 0.10
- let left = plate.minX + plate.width * 0.14
- let usable = plate.width * 0.72
- let radius = barHeight / 2
- let totalHeight = barHeight * 3 + spacing * 2
- var y = plate.midY + totalHeight / 2 - barHeight
+ private static func fillPlate(_ rect: NSRect) {
+ NSGraphicsContext.current?.saveGraphicsState()
+ defer { NSGraphicsContext.current?.restoreGraphicsState() }
- // Сверху вниз: недобор, ровно по плану, перерасход.
- let rows: [(fill: Double, plan: Double)] = [
- (0.45, 0.70),
- (0.70, 0.70),
- (0.95, 0.70),
- ]
+ squircle(in: rect).addClip()
+ NSGradient(colors: [NSColor(hex: plateTop), NSColor(hex: plateBottom)])?
+ .draw(in: rect, angle: -90)
+ // Блик по верхней кромке: без него плитка выглядит наклейкой, а не
+ // иконкой рядом с системными.
+ NSGradient(colors: [NSColor(white: 1, alpha: 0.14), NSColor(white: 1, alpha: 0)])?
+ .draw(
+ in: NSRect(
+ x: rect.minX, y: rect.maxY - rect.height * 0.4,
+ width: rect.width, height: rect.height * 0.4
+ ),
+ angle: -90
+ )
+ }
- for row in rows {
- NSColor(hex: ink.track.dark).setFill()
- NSBezierPath(
- roundedRect: NSRect(x: left, y: y, width: usable, height: barHeight),
- xRadius: radius, yRadius: radius
- ).fill()
+ /// Форма плитки macOS — суперэллипс, а не прямоугольник со скруглением:
+ /// у скруглённого угол ломается там, где дуга встречается с прямой, и
+ /// рядом с системными иконками это видно.
+ private static func squircle(in rect: NSRect, exponent: CGFloat = 5) -> NSBezierPath {
+ let path = NSBezierPath()
+ let a = rect.width / 2, b = rect.height / 2
+ let steps = 720
+ for step in 0...steps {
+ let t = CGFloat(step) / CGFloat(steps) * 2 * .pi
+ let cosT = cos(t), sinT = sin(t)
+ let point = NSPoint(
+ x: rect.midX + a * copysign(pow(abs(cosT), 2 / exponent), cosT),
+ y: rect.midY + b * copysign(pow(abs(sinT), 2 / exponent), sinT)
+ )
+ if step == 0 { path.move(to: point) } else { path.line(to: point) }
+ }
+ path.close()
+ return path
+ }
- NSColor(hex: ink.plan.dark).setFill()
- NSBezierPath(
- roundedRect: NSRect(x: left, y: y, width: usable * row.plan, height: barHeight),
- xRadius: radius, yRadius: radius
- ).fill()
+ private static func drawRing(in plate: NSRect) {
+ let center = NSPoint(x: plate.midX, y: plate.midY)
+ let radius = plate.width * 0.305
+ let width = plate.width * 0.165
- NSColor(hex: ink.good.dark).setFill()
- NSBezierPath(
- roundedRect: NSRect(
- x: left, y: y,
- width: usable * min(row.fill, row.plan), height: barHeight
- ),
- xRadius: radius, yRadius: radius
- ).fill()
+ // Соседние дуги перекрываются на полградуса: встык AppKit оставляет
+ // волосяную щель, и на мелком размере она читается как грязь.
+ let bleed: CGFloat = 0.5
+ let warningAngle = warningStart * 3.6
+ let criticalAngle = criticalStart * 3.6
- if row.fill > row.plan {
- let gap = max(side * 0.012, 1)
- let start = left + usable * row.plan + gap
- NSColor(hex: ink.warning.dark).setFill()
- NSBezierPath(
- roundedRect: NSRect(
- x: start, y: y,
- width: max(left + usable * row.fill - start, 1), height: barHeight
- ),
- xRadius: radius, yRadius: radius
- ).fill()
- }
+ arc(center: center, radius: radius, width: width,
+ from: 90, sweep: warningAngle + bleed, color: ink.good.dark)
+ arc(center: center, radius: radius, width: width,
+ from: 90 - warningAngle, sweep: criticalAngle - warningAngle + bleed,
+ color: ink.warning.dark)
+ arc(center: center, radius: radius, width: width,
+ from: 90 - criticalAngle, sweep: 360 - criticalAngle, color: ink.critical.dark)
+ }
- y -= barHeight + spacing
- }
- return true
- }
+ /// 12 часов — верх круга; в системе координат AppKit это 90°, дальше по
+ /// часовой стрелке, как заполняется кольцо в строке меню.
+ private static func arc(
+ center: NSPoint, radius: CGFloat, width: CGFloat,
+ from: CGFloat, sweep: CGFloat, color: UInt32
+ ) {
+ let path = NSBezierPath()
+ path.appendArc(
+ withCenter: center, radius: radius,
+ startAngle: from, endAngle: from - min(sweep, 359.9), clockwise: true
+ )
+ path.lineWidth = width
+ path.lineCapStyle = .butt
+ NSColor(hex: color).setStroke()
+ path.stroke()
}
}
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index 623cc2e..17c5c78 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -157,7 +157,7 @@ SwiftUI` — значит, расчёт просочился в UI или нао
| `NotificationController.swift` | уведомления глазами системы: разрешение macOS, отправка баннеров, лог сказанного на диске |
| `AlertArtwork.swift` | картинка баннера: дуга для сессии, красное число для недели |
| `Screenshot.swift` | `--screenshot`: панель во всех темах, она же в разбивке по моделям, вкладка уведомлений, иконка в PNG |
-| `AppIcon.swift` | `--icon`: `.iconset` для сборки бандла |
+| `AppIcon.swift` | `--icon`: кольцо недели по порогам, `.iconset` для сборки бандла |
| `CLI.swift` | `--json`, `--calibrate` и `--update` |
### Потоки данных
diff --git a/docs/images/app-icon.png b/docs/images/app-icon.png
new file mode 100644
index 0000000..0aaafbb
Binary files /dev/null and b/docs/images/app-icon.png differ
diff --git a/docs/images/notification-session-dark.png b/docs/images/notification-session-dark.png
index 5e2a7b8..7b235ca 100644
Binary files a/docs/images/notification-session-dark.png and b/docs/images/notification-session-dark.png differ
diff --git a/docs/images/notification-week-dark.png b/docs/images/notification-week-dark.png
index 67ee2d9..44a4aa7 100644
Binary files a/docs/images/notification-week-dark.png and b/docs/images/notification-week-dark.png differ