diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b462b7af01..fc91db5b946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.67.1 — Unreleased +### Changed + +- Settings: simplify menu bar layout controls while keeping token-removal instructions in the section footer (#3999). Thanks @elijahfriedman! + ## 0.67.0 — 2026-09-25 ### Highlights @@ -499,7 +503,6 @@ - Groq: reject unavailable, malformed, or overflowing Prometheus rates instead of showing nonnumeric usage or inventing zero, while preserving genuine empty-query results and API error messages. - Documentation: link the community-maintained CodexBar for Windows companion and AI Monitor USB desk display (#3525, #3544). Thanks @hinneslung and @tobymarks! - ## 0.59.0 — 2026-09-10 ### Highlights @@ -2603,7 +2606,6 @@ - Cost usage scanner: optimize JSONL chunk parsing to avoid buffer-front removal overhead on large logs (#392). Thanks @asonawalla! - TTY runner: fence shutdown registration to avoid launch/shutdown races, isolate process groups before shutdown rejection, and ensure lingering CLI descendants are cleaned up on app termination (#429). Thanks @uraimo! - ## 0.18.0-beta.3 — 2026-02-13 ### Highlights - Claude OAuth/keychain flows were reworked across a series of follow-up PRs to reduce prompt storms, stabilize background behavior, surface a setting to control prompt policy and make failure modes deterministic (#245, #305, #308, #309, #364). Thanks @manikv12! diff --git a/Sources/CodexBar/MenuBarLayoutEditor.swift b/Sources/CodexBar/MenuBarLayoutEditor.swift index 2c91c15a678..ec559cb253c 100644 --- a/Sources/CodexBar/MenuBarLayoutEditor.swift +++ b/Sources/CodexBar/MenuBarLayoutEditor.swift @@ -716,48 +716,10 @@ struct MenuBarLayoutEditor: View { } private var displayOptions: some View { - HStack(spacing: 18) { - Picker(L("menu_bar_layout_size"), selection: self.sizeBinding) { - ForEach(MenuBarLayoutSize.allCases) { size in - Text(size.label).tag(size) - } - } - .pickerStyle(.menu) - - Picker(L("menu_bar_layout_gap"), selection: self.gapBinding) { - ForEach(MenuBarLayoutGap.allCases) { gap in - Text(gap.label).tag(gap) - } - } - .pickerStyle(.menu) - - HStack(spacing: 8) { - Text(L("menu_bar_layout_vertical_adjustment")) - .lineLimit(1) - .fixedSize() - - TextField( - "", - value: self.$settings.menuBarLayoutVerticalAdjustment, - format: .number) - .labelsHidden() - .textFieldStyle(.roundedBorder) - .multilineTextAlignment(.trailing) - .monospacedDigit() - .frame(width: 44) - - Stepper(value: self.$settings.menuBarLayoutVerticalAdjustment, in: -20...20, step: 1) { - EmptyView() - } - .labelsHidden() - } - - Spacer() - - Text(L("menu_bar_layout_keyboard_hint")) - .font(.caption) - .foregroundStyle(.tertiary) - } + MenuBarLayoutDisplayOptions( + size: self.sizeBinding, + gap: self.gapBinding, + verticalAdjustment: self.$settings.menuBarLayoutVerticalAdjustment) } private func applyPreset(_ preset: MenuBarLayoutPreset) { @@ -1345,3 +1307,50 @@ extension MenuBarLayoutToken { } } } + +struct MenuBarLayoutDisplayOptions: View { + @Binding var size: MenuBarLayoutSize + @Binding var gap: MenuBarLayoutGap + @Binding var verticalAdjustment: Int + + var body: some View { + HStack(spacing: 18) { + Picker(L("menu_bar_layout_size"), selection: self.$size) { + ForEach(MenuBarLayoutSize.allCases) { size in + Text(size.label).tag(size) + } + } + .pickerStyle(.menu) + + Picker(L("menu_bar_layout_gap"), selection: self.$gap) { + ForEach(MenuBarLayoutGap.allCases) { gap in + Text(gap.label).tag(gap) + } + } + .pickerStyle(.menu) + + HStack(spacing: 8) { + Text(L("menu_bar_layout_vertical_adjustment")) + .lineLimit(1) + .fixedSize() + + TextField( + "", + value: self.$verticalAdjustment, + format: .number) + .labelsHidden() + .textFieldStyle(.roundedBorder) + .multilineTextAlignment(.trailing) + .monospacedDigit() + .frame(width: 44) + + Stepper(value: self.$verticalAdjustment, in: -20...20, step: 1) { + EmptyView() + } + .labelsHidden() + } + + Spacer() + } + } +} diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index f9a5568aa6d..40ccd1767ec 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "ضيّق"; "menu_bar_layout_gap_regular" = "عادي"; "menu_bar_layout_vertical_adjustment" = "الضبط العمودي"; -"menu_bar_layout_keyboard_hint" = "يحذف Delete العنصر المحدد"; "menu_bar_layout_sample_account" = "حساب"; "menu_bar_layout_sample_runs_out" = "ينفد الجمعة"; "menu_bar_layout_token_icon" = "الأيقونة"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index d65692b57ad..bcf0c234d37 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -1416,7 +1416,6 @@ "menu_bar_layout_gap_tight" = "Estret"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Ajust vertical"; -"menu_bar_layout_keyboard_hint" = "La tecla Supr elimina la fitxa seleccionada"; "menu_bar_layout_sample_account" = "compte"; "menu_bar_layout_sample_runs_out" = "s'esgota dv."; "menu_bar_layout_token_icon" = "Icona"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index 297e6a3261a..0249d60082e 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -1414,7 +1414,6 @@ "menu_bar_layout_gap_tight" = "Eng"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Vertikale Ausrichtung"; -"menu_bar_layout_keyboard_hint" = "Die Löschtaste entfernt den ausgewählten Baustein"; "menu_bar_layout_sample_account" = "Konto"; "menu_bar_layout_sample_runs_out" = "reicht bis Fr."; "menu_bar_layout_token_icon" = "Symbol"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index 80869da2790..7a1045763df 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -1422,7 +1422,6 @@ "menu_bar_layout_gap_tight" = "Tight"; "menu_bar_layout_gap_regular" = "Regular"; "menu_bar_layout_vertical_adjustment" = "Vertical"; -"menu_bar_layout_keyboard_hint" = "Delete removes the selected token"; "menu_bar_layout_sample_account" = "account"; "menu_bar_layout_sample_runs_out" = "runs out Fri"; "menu_bar_layout_token_icon" = "Icon"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index a34e1b800b7..f29eac914b4 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -1412,7 +1412,6 @@ "menu_bar_layout_gap_tight" = "Estrecha"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Ajuste vertical"; -"menu_bar_layout_keyboard_hint" = "Suprimir quita la ficha seleccionada"; "menu_bar_layout_sample_account" = "cuenta"; "menu_bar_layout_sample_runs_out" = "se agota vie."; "menu_bar_layout_token_icon" = "Icono"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index b89daecc561..d2faf83b8c4 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "فشرده"; "menu_bar_layout_gap_regular" = "معمولی"; "menu_bar_layout_vertical_adjustment" = "تنظیم عمودی"; -"menu_bar_layout_keyboard_hint" = "Delete نشانهٔ انتخاب‌شده را حذف می‌کند"; "menu_bar_layout_sample_account" = "حساب"; "menu_bar_layout_sample_runs_out" = "جمعه تمام می‌شود"; "menu_bar_layout_token_icon" = "آیکون"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index ab656cf83fc..45fedb93137 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -1413,7 +1413,6 @@ "menu_bar_layout_gap_tight" = "Serré"; "menu_bar_layout_gap_regular" = "Normale"; "menu_bar_layout_vertical_adjustment" = "Ajustement vertical"; -"menu_bar_layout_keyboard_hint" = "Supprimer retire le jeton sélectionné"; "menu_bar_layout_sample_account" = "compte"; "menu_bar_layout_sample_runs_out" = "épuisé ven."; "menu_bar_layout_token_icon" = "Icône"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index bbb6028fdb2..fb391177590 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -1413,7 +1413,6 @@ "menu_bar_layout_gap_tight" = "Estreita"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Axuste vertical"; -"menu_bar_layout_keyboard_hint" = "Suprimir retira a ficha seleccionada"; "menu_bar_layout_sample_account" = "conta"; "menu_bar_layout_sample_runs_out" = "esgótase ven."; "menu_bar_layout_token_icon" = "Icona"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 31796026892..3a7728c8fb2 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "Rapat"; "menu_bar_layout_gap_regular" = "Reguler"; "menu_bar_layout_vertical_adjustment" = "Penyesuaian Vertikal"; -"menu_bar_layout_keyboard_hint" = "Delete menghapus token yang dipilih"; "menu_bar_layout_sample_account" = "akun"; "menu_bar_layout_sample_runs_out" = "habis Jum."; "menu_bar_layout_token_icon" = "Ikon"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index 0108f0aaa6e..2cb9e05e668 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "Stretta"; "menu_bar_layout_gap_regular" = "Normale"; "menu_bar_layout_vertical_adjustment" = "Regolazione verticale"; -"menu_bar_layout_keyboard_hint" = "Canc rimuove il token selezionato"; "menu_bar_layout_sample_account" = "account"; "menu_bar_layout_sample_runs_out" = "termina ven."; "menu_bar_layout_token_icon" = "Icona"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 5224a5d001b..650efd29027 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -1414,7 +1414,6 @@ "menu_bar_layout_gap_tight" = "狭い"; "menu_bar_layout_gap_regular" = "標準"; "menu_bar_layout_vertical_adjustment" = "垂直調整"; -"menu_bar_layout_keyboard_hint" = "Delete キーで選択したトークンを削除"; "menu_bar_layout_sample_account" = "アカウント"; "menu_bar_layout_sample_runs_out" = "金曜に使い切る"; "menu_bar_layout_token_icon" = "アイコン"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 4da3c84f602..eb0e20a630a 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -1383,7 +1383,6 @@ "menu_bar_layout_gap_tight" = "좁게"; "menu_bar_layout_gap_regular" = "보통"; "menu_bar_layout_vertical_adjustment" = "세로 조정"; -"menu_bar_layout_keyboard_hint" = "Delete 키로 선택한 토큰 제거"; "menu_bar_layout_sample_account" = "계정"; "menu_bar_layout_sample_runs_out" = "금요일 소진"; "menu_bar_layout_token_icon" = "아이콘"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 47e264ad1ba..9db6fd9d1a4 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -1413,7 +1413,6 @@ "menu_bar_layout_gap_tight" = "Krap"; "menu_bar_layout_gap_regular" = "Normaal"; "menu_bar_layout_vertical_adjustment" = "Verticale aanpassing"; -"menu_bar_layout_keyboard_hint" = "Delete verwijdert het geselecteerde token"; "menu_bar_layout_sample_account" = "account"; "menu_bar_layout_sample_runs_out" = "op vr."; "menu_bar_layout_token_icon" = "Pictogram"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 8efb5212f64..1b02226bd0a 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "Wąski"; "menu_bar_layout_gap_regular" = "Zwykły"; "menu_bar_layout_vertical_adjustment" = "Regulacja pionowa"; -"menu_bar_layout_keyboard_hint" = "Delete usuwa zaznaczony element"; "menu_bar_layout_sample_account" = "konto"; "menu_bar_layout_sample_runs_out" = "wyczerpie się pt."; "menu_bar_layout_token_icon" = "Ikona"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 3b6d184e35d..9052aa280ed 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -1414,7 +1414,6 @@ "menu_bar_layout_gap_tight" = "Apertado"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Ajuste vertical"; -"menu_bar_layout_keyboard_hint" = "Delete remove o item selecionado"; "menu_bar_layout_sample_account" = "conta"; "menu_bar_layout_sample_runs_out" = "acaba sex."; "menu_bar_layout_token_icon" = "Ícone"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 45aa0d58fd7..15397e31fe8 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -1415,7 +1415,6 @@ "menu_bar_layout_gap_tight" = "Узкий"; "menu_bar_layout_gap_regular" = "Обычный"; "menu_bar_layout_vertical_adjustment" = "Вертикальная настройка"; -"menu_bar_layout_keyboard_hint" = "Delete удаляет выбранный элемент"; "menu_bar_layout_sample_account" = "аккаунт"; "menu_bar_layout_sample_runs_out" = "закончится пт."; "menu_bar_layout_token_icon" = "Значок"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index ee28d365696..0269ef0d271 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -1412,7 +1412,6 @@ "menu_bar_layout_gap_tight" = "Tätt"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Vertikal justering"; -"menu_bar_layout_keyboard_hint" = "Delete tar bort den markerade brickan"; "menu_bar_layout_sample_account" = "konto"; "menu_bar_layout_sample_runs_out" = "tar slut fre."; "menu_bar_layout_token_icon" = "Ikon"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index 415797276ee..a48aa714cf8 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "ชิด"; "menu_bar_layout_gap_regular" = "ปกติ"; "menu_bar_layout_vertical_adjustment" = "ปรับแนวตั้ง"; -"menu_bar_layout_keyboard_hint" = "Delete ลบโทเค็นที่เลือก"; "menu_bar_layout_sample_account" = "บัญชี"; "menu_bar_layout_sample_runs_out" = "หมดวันศุกร์"; "menu_bar_layout_token_icon" = "ไอคอน"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 90a0c82331c..b515b7f26ab 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -1415,7 +1415,6 @@ "menu_bar_layout_gap_tight" = "Dar"; "menu_bar_layout_gap_regular" = "Normal"; "menu_bar_layout_vertical_adjustment" = "Dikey ayar"; -"menu_bar_layout_keyboard_hint" = "Delete seçili öğeyi kaldırır"; "menu_bar_layout_sample_account" = "hesap"; "menu_bar_layout_sample_runs_out" = "Cuma biter"; "menu_bar_layout_token_icon" = "Simge"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index d19069b1a97..32cf89c810c 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -1413,7 +1413,6 @@ "menu_bar_layout_gap_tight" = "Вузький"; "menu_bar_layout_gap_regular" = "Звичайний"; "menu_bar_layout_vertical_adjustment" = "Вертикальне налаштування"; -"menu_bar_layout_keyboard_hint" = "Delete видаляє вибраний елемент"; "menu_bar_layout_sample_account" = "обліковий запис"; "menu_bar_layout_sample_runs_out" = "закінчиться пт."; "menu_bar_layout_token_icon" = "Значок"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 3f774390de7..7e87f929005 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -1414,7 +1414,6 @@ "menu_bar_layout_gap_tight" = "Hẹp"; "menu_bar_layout_gap_regular" = "Thường"; "menu_bar_layout_vertical_adjustment" = "Điều chỉnh dọc"; -"menu_bar_layout_keyboard_hint" = "Delete xóa thẻ đã chọn"; "menu_bar_layout_sample_account" = "tài khoản"; "menu_bar_layout_sample_runs_out" = "hết vào T6"; "menu_bar_layout_token_icon" = "Biểu tượng"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 5c7522c8d16..842067c7ca3 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -1417,7 +1417,6 @@ "menu_bar_layout_gap_tight" = "紧凑"; "menu_bar_layout_gap_regular" = "常规"; "menu_bar_layout_vertical_adjustment" = "垂直调整"; -"menu_bar_layout_keyboard_hint" = "Delete 键会移除所选项目"; "menu_bar_layout_sample_account" = "帐户"; "menu_bar_layout_sample_runs_out" = "周五用尽"; "menu_bar_layout_token_icon" = "图标"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index 9a79bc46940..beaa5d81741 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -1444,7 +1444,6 @@ "menu_bar_layout_gap_tight" = "緊湊"; "menu_bar_layout_gap_regular" = "一般"; "menu_bar_layout_vertical_adjustment" = "垂直調整"; -"menu_bar_layout_keyboard_hint" = "Delete 鍵會移除所選項目"; "menu_bar_layout_sample_account" = "帳號"; "menu_bar_layout_sample_runs_out" = "週五用盡"; "menu_bar_layout_token_icon" = "圖示"; diff --git a/Tests/CodexBarTests/MenuBarLayoutDisplayOptionsTests.swift b/Tests/CodexBarTests/MenuBarLayoutDisplayOptionsTests.swift new file mode 100644 index 00000000000..7b9245863ce --- /dev/null +++ b/Tests/CodexBarTests/MenuBarLayoutDisplayOptionsTests.swift @@ -0,0 +1,72 @@ +import AppKit +import SwiftUI +import Testing +@testable import CodexBar + +@MainActor +struct MenuBarLayoutDisplayOptionsTests { + @Test(arguments: MenuBarLayoutSize.allCases, MenuBarLayoutGap.allCases) + func `size and gap selections remain accessible in a narrow row`( + size: MenuBarLayoutSize, + gap: MenuBarLayoutGap) + { + let hosting = Self.hostingView(width: 480, size: size, gap: gap) + let text = MenuLayoutScreenshotRenderTests.accessibilityText(hosting) + + #expect(text.contains(L("menu_bar_layout_size"))) + #expect(text.contains(L("menu_bar_layout_gap"))) + #expect(text.contains(size.label)) + #expect(text.contains(gap.label)) + } + + @Test + func `keyboard instructions remain in the footer without duplication in the row`() { + let hosting = Self.hostingView(width: 720, size: .regular, gap: .regular) + let text = MenuLayoutScreenshotRenderTests.accessibilityText(hosting) + + #expect(text.contains(L("menu_bar_layout_footer"))) + #expect(!text.contains("Delete removes the selected token")) + } + + @Test + func `synthetic display options screenshot`() throws { + guard let directory = ProcessInfo.processInfo.environment["CODEXBAR_LAYOUT_OPTIONS_SCREENSHOT_DIR"] + else { return } + for appearance in [NSAppearance.Name.aqua, .darkAqua] { + for width: CGFloat in [480, 520] { + let hosting = Self.hostingView(width: width, size: .small, gap: .tight, appearance: appearance) + let bitmap = try #require(hosting.bitmapImageRepForCachingDisplay(in: hosting.bounds)) + hosting.cacheDisplay(in: hosting.bounds, to: bitmap) + let png = try #require(bitmap.representation(using: .png, properties: [:])) + let name = appearance == .aqua ? "light" : "dark" + try png.write(to: URL(fileURLWithPath: directory) + .appendingPathComponent("menu-bar-layout-options-\(Int(width))-\(name).png")) + } + } + } + + private static func hostingView( + width: CGFloat, + size: MenuBarLayoutSize, + gap: MenuBarLayoutGap, + appearance: NSAppearance.Name = .aqua) + -> NSHostingView + { + let view = VStack(alignment: .leading, spacing: 12) { + MenuBarLayoutDisplayOptions( + size: .constant(size), + gap: .constant(gap), + verticalAdjustment: .constant(0)) + SettingsSectionFooter(L("menu_bar_layout_footer")) + } + .padding(16) + .frame(width: width) + .environment(\.accessibilityEnabled, true) + .background(Color(nsColor: .windowBackgroundColor)) + let hosting = NSHostingView(rootView: view) + hosting.appearance = NSAppearance(named: appearance) + hosting.frame = CGRect(origin: .zero, size: hosting.fittingSize) + hosting.layoutSubtreeIfNeeded() + return hosting + } +} diff --git a/Tests/CodexBarTests/MenuLayoutScreenshotRenderTests.swift b/Tests/CodexBarTests/MenuLayoutScreenshotRenderTests.swift index 78792f863ba..65203835360 100644 --- a/Tests/CodexBarTests/MenuLayoutScreenshotRenderTests.swift +++ b/Tests/CodexBarTests/MenuLayoutScreenshotRenderTests.swift @@ -215,7 +215,7 @@ final class MenuLayoutScreenshotRenderTests: XCTestCase { } } - private static func accessibilityText(_ element: Any, depth: Int = 0) -> String { + static func accessibilityText(_ element: Any, depth: Int = 0) -> String { guard depth < 30, let accessible = element as? NSObject else { return "" } // SwiftUI nodes implement these public selectors without adopting the full NSAccessibility protocol. let fields = [ @@ -261,6 +261,7 @@ final class MenuLayoutScreenshotRenderTests: XCTestCase { .map { ProviderConfig(id: $0.instanceID, enabled: order.contains($0)) }) let settings = testSettingsStore( suiteName: "MenuLayoutScreenshotRenderTests-overrides", + userDefaults: InMemoryUserDefaults(), config: config, prepareDefaults: { defaults in defaults.set(AppGroupSupport.migrationVersion, forKey: AppGroupSupport.migrationVersionKey) @@ -291,16 +292,26 @@ final class MenuLayoutScreenshotRenderTests: XCTestCase { XCTAssertEqual(settings.menuBarLayoutForGlobalEditing(representativeProvider: .claude), global) XCTAssertEqual(settings.menuBarLayout(for: .claude), override) - let view = AnyView(MenuBarLayoutEditor(settings: settings, store: store) + let directory = URL(fileURLWithPath: dir, isDirectory: true) + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + for dark in [false, true] { + let view = AnyView(VStack(alignment: .leading, spacing: 12) { + MenuBarLayoutEditor(settings: settings, store: store) + SettingsSectionFooter(L("menu_bar_layout_footer")) + } .frame(width: 560) .padding(16) .environment(\.locale, Locale(identifier: "en_US_POSIX")) - .environment(\.colorScheme, .dark) + .environment(\.colorScheme, dark ? .dark : .light) + .environment(\.accessibilityEnabled, true) .background(Color(nsColor: .windowBackgroundColor))) - let data = try XCTUnwrap(Self.pngData(for: view)) - let directory = URL(fileURLWithPath: dir, isDirectory: true) - try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) - try data.write(to: directory.appendingPathComponent("layout-override.png")) + let hosting = NSHostingView(rootView: view) + hosting.appearance = NSAppearance(named: dark ? .darkAqua : .aqua) + let data = try XCTUnwrap(Self.pngData(hosting: hosting)) + XCTAssertTrue(Self.accessibilityText(hosting).contains(L("menu_bar_layout_footer"))) + let appearance = dark ? "dark" : "light" + try data.write(to: directory.appendingPathComponent("layout-override-\(appearance).png")) + } XCTAssertEqual(settings.menuBarLayoutOverrides, [.claude: override]) XCTAssertEqual(settings.menuBarLayout, global) } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index a5a9c188333..bbed90ced05 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -1830,7 +1830,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuBarLayoutEditor.swift", - line: 932, + line: 894, anchor: "let provider = self.provider ?? .codex", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -1838,7 +1838,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared construct dispatches a provider-owned capability at the generic integration boundary."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuBarLayoutEditor.swift", - line: 963, + line: 925, anchor: "if provider == .codex,", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, diff --git a/docs/ui.md b/docs/ui.md index 47fae1fec4e..887c2ef1d20 100644 --- a/docs/ui.md +++ b/docs/ui.md @@ -58,6 +58,8 @@ read_when: editing still starts from the representative provider's effective layout. - Small/Regular controls the token font scale. Tight/Regular controls status-item padding. Compact stacked uses two tightly spaced lines sized to fit the menu bar. +- Size, gap, and vertical adjustment share a compact options row. The Layout section footer keeps the token-editing + instructions, including selecting a placed token and pressing Delete to remove it. ### Layout tokens