diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b462b7af0..ed4ecefc74 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 diff --git a/Sources/CodexBar/MenuBarLayoutEditor.swift b/Sources/CodexBar/MenuBarLayoutEditor.swift index 2c91c15a67..35fc265440 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,68 @@ extension MenuBarLayoutToken { } } } + +/// Size, gap, and vertical-offset controls. Pickers stay at their ideal width so the +/// current selection is always visible without opening the menu. +struct MenuBarLayoutDisplayOptions: View { + @Binding var size: MenuBarLayoutSize + @Binding var gap: MenuBarLayoutGap + @Binding var verticalAdjustment: Int + + var body: some View { + HStack(spacing: 18) { + HStack(spacing: 8) { + Text(L("menu_bar_layout_size")) + .lineLimit(1) + .fixedSize() + + Picker(L("menu_bar_layout_size"), selection: self.$size) { + ForEach(MenuBarLayoutSize.allCases) { size in + Text(size.label).tag(size) + } + } + .labelsHidden() + .pickerStyle(.menu) + .fixedSize() + } + + HStack(spacing: 8) { + Text(L("menu_bar_layout_gap")) + .lineLimit(1) + .fixedSize() + + Picker(L("menu_bar_layout_gap"), selection: self.$gap) { + ForEach(MenuBarLayoutGap.allCases) { gap in + Text(gap.label).tag(gap) + } + } + .labelsHidden() + .pickerStyle(.menu) + .fixedSize() + } + + 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 f9a5568aa6..40ccd1767e 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 d65692b57a..bcf0c234d3 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 297e6a3261..0249d60082 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 80869da279..7a1045763d 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 a34e1b800b..f29eac914b 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 b89daecc56..d2faf83b8c 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 ab656cf83f..45fedb9313 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 bbb6028fdb..fb39117759 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 3179602689..3a7728c8fb 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 0108f0aaa6..2cb9e05e66 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 5224a5d001..650efd2902 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 4da3c84f60..eb0e20a630 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 47e264ad1b..9db6fd9d1a 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 8efb5212f6..1b02226bd0 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 3b6d184e35..9052aa280e 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 45aa0d58fd..15397e31fe 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 ee28d36569..0269ef0d27 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 415797276e..a48aa714cf 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 90a0c82331..b515b7f26a 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 d19069b1a9..32cf89c810 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 3f774390de..7e87f92900 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 5c7522c8d1..842067c7ca 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 9a79bc4694..beaa5d8174 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 0000000000..700a9a871a --- /dev/null +++ b/Tests/CodexBarTests/MenuBarLayoutDisplayOptionsTests.swift @@ -0,0 +1,74 @@ +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 hint = "Delete removes the selected token" + let text = MenuLayoutScreenshotRenderTests.accessibilityText(hosting) + + #expect(text.contains(L("menu_bar_layout_footer"))) + #expect(!text.contains(hint)) + #expect(L("menu_bar_layout_keyboard_hint") != hint) + } + + @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 78792f863b..6520383536 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 a5a9c18833..bbed90ced0 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 47fae1fec4..887c2ef1d2 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