From a55b92ec30820042ddfaf2636c73b56a7dbe4047 Mon Sep 17 00:00:00 2001 From: dylanstieri Date: Sat, 15 Aug 2026 00:32:06 -0700 Subject: [PATCH 1/2] Feat: configurable workday ticks including hidden, subtle, and high contrast --- .../CodexBar/MenuCardView+ModelHelpers.swift | 1 + .../CodexBar/MenuCardView+ModelInput.swift | 3 + .../MenuCardView+SessionEquivalent.swift | 14 ++-- Sources/CodexBar/MenuCardView.swift | 15 ++-- Sources/CodexBar/PreferencesMenuPane.swift | 13 ++++ Sources/CodexBar/PreferencesMenuPicker.swift | 1 + .../PreferencesProviderDetailView.swift | 3 +- .../CodexBar/PreferencesProvidersPane.swift | 1 + .../Resources/ar.lproj/Localizable.strings | 5 ++ .../Resources/ca.lproj/Localizable.strings | 5 ++ .../Resources/de.lproj/Localizable.strings | 5 ++ .../Resources/en.lproj/Localizable.strings | 5 ++ .../Resources/es.lproj/Localizable.strings | 5 ++ .../Resources/fa.lproj/Localizable.strings | 5 ++ .../Resources/fr.lproj/Localizable.strings | 5 ++ .../Resources/gl.lproj/Localizable.strings | 5 ++ .../Resources/id.lproj/Localizable.strings | 5 ++ .../Resources/it.lproj/Localizable.strings | 5 ++ .../Resources/ja.lproj/Localizable.strings | 5 ++ .../Resources/ko.lproj/Localizable.strings | 5 ++ .../Resources/nl.lproj/Localizable.strings | 5 ++ .../Resources/pl.lproj/Localizable.strings | 5 ++ .../Resources/pt-BR.lproj/Localizable.strings | 5 ++ .../Resources/ru.lproj/Localizable.strings | 5 ++ .../Resources/sv.lproj/Localizable.strings | 5 ++ .../Resources/th.lproj/Localizable.strings | 5 ++ .../Resources/tr.lproj/Localizable.strings | 5 ++ .../Resources/uk.lproj/Localizable.strings | 5 ++ .../Resources/vi.lproj/Localizable.strings | 5 ++ .../zh-Hans.lproj/Localizable.strings | 5 ++ .../zh-Hant.lproj/Localizable.strings | 5 ++ Sources/CodexBar/SettingsStore+Defaults.swift | 8 +++ .../SettingsStore+MenuObservation.swift | 1 + Sources/CodexBar/SettingsStore+Sync.swift | 6 ++ Sources/CodexBar/SettingsStore.swift | 21 ++++++ Sources/CodexBar/SettingsStoreState.swift | 1 + .../StatusItemController+MenuCardModel.swift | 1 + Sources/CodexBar/UsageProgressBar.swift | 69 +++++++++++++++---- Sources/CodexBarCore/Sync/SyncModels.swift | 3 + .../CloudSyncSettingsTests.swift | 18 +++++ .../LocalizationLanguageCatalogTests.swift | 11 +++ .../MenuCardModelCodexProjectionTests.swift | 55 +++++++++------ .../MenuCardQuotaWarningMarkerTests.swift | 28 ++++++++ .../PreferencesPaneSmokeTests.swift | 3 + .../ProviderArchitectureGatekeeperTests.swift | 54 +++++++-------- .../SettingsStoreCoverageTests.swift | 21 ++++++ Tests/CodexBarTests/SettingsStoreTests.swift | 24 ++++--- 47 files changed, 408 insertions(+), 82 deletions(-) diff --git a/Sources/CodexBar/MenuCardView+ModelHelpers.swift b/Sources/CodexBar/MenuCardView+ModelHelpers.swift index 1f1ece9917..15f89ca13d 100644 --- a/Sources/CodexBar/MenuCardView+ModelHelpers.swift +++ b/Sources/CodexBar/MenuCardView+ModelHelpers.swift @@ -239,6 +239,7 @@ extension UsageMenuCardView.Model { paceOnTop: metric.paceOnTop, warningMarkerPercents: metric.warningMarkerPercents, workdayMarkerPercents: metric.workdayMarkerPercents, + workdayTickAppearance: metric.workdayTickAppearance, cardStyle: metric.cardStyle, sessionEquivalentDetail: metric.sessionEquivalentDetail) } diff --git a/Sources/CodexBar/MenuCardView+ModelInput.swift b/Sources/CodexBar/MenuCardView+ModelInput.swift index c06a0f4584..754047d8d6 100644 --- a/Sources/CodexBar/MenuCardView+ModelInput.swift +++ b/Sources/CodexBar/MenuCardView+ModelInput.swift @@ -42,6 +42,7 @@ extension UsageMenuCardView.Model { let sessionEquivalentForecast: SessionEquivalentForecast? let quotaWarningThresholds: [QuotaWarningWindow: [Int]] let workDaysPerWeek: Int? + let workdayTickAppearance: WorkdayTickAppearance let usesLiveSubtitle: Bool let preferredCurrencyCode: String let now: Date @@ -84,6 +85,7 @@ extension UsageMenuCardView.Model { sessionEquivalentForecast: SessionEquivalentForecast? = nil, quotaWarningThresholds: [QuotaWarningWindow: [Int]] = [:], workDaysPerWeek: Int? = nil, + workdayTickAppearance: WorkdayTickAppearance = .subtle, usesLiveSubtitle: Bool = false, preferredCurrencyCode: String = "auto", now: Date) @@ -125,6 +127,7 @@ extension UsageMenuCardView.Model { self.sessionEquivalentForecast = sessionEquivalentForecast self.quotaWarningThresholds = quotaWarningThresholds self.workDaysPerWeek = workDaysPerWeek + self.workdayTickAppearance = workdayTickAppearance self.usesLiveSubtitle = usesLiveSubtitle self.preferredCurrencyCode = preferredCurrencyCode self.now = now diff --git a/Sources/CodexBar/MenuCardView+SessionEquivalent.swift b/Sources/CodexBar/MenuCardView+SessionEquivalent.swift index 4adfbb20da..6d19bc5639 100644 --- a/Sources/CodexBar/MenuCardView+SessionEquivalent.swift +++ b/Sources/CodexBar/MenuCardView+SessionEquivalent.swift @@ -51,6 +51,13 @@ extension UsageMenuCardView.Model { id = "monthly" paceDetail = nil } + let workdayMarkerPercents: [Double] = if lane == .weekly, input.workdayTickAppearance != .hidden { + workDayMarkerPercents( + workDays: input.workDaysPerWeek, + windowMinutes: window.windowMinutes) + } else { + [] + } return Metric( id: id, @@ -66,11 +73,8 @@ extension UsageMenuCardView.Model { warningMarkerPercents: Self.warningMarkerPercents( thresholds: lane.quotaWarningWindow.flatMap { input.quotaWarningThresholds[$0] }, showUsed: input.usageBarsShowUsed), - workdayMarkerPercents: lane == .weekly - ? workDayMarkerPercents( - workDays: input.workDaysPerWeek, - windowMinutes: window.windowMinutes) - : [], + workdayMarkerPercents: workdayMarkerPercents, + workdayTickAppearance: input.workdayTickAppearance, sessionEquivalentDetail: lane == .weekly ? Self.sessionEquivalentDetail(input: input, weeklyWindow: window, weeklyWindowID: nil) : nil) diff --git a/Sources/CodexBar/MenuCardView.swift b/Sources/CodexBar/MenuCardView.swift index 681429edb3..284a09b547 100644 --- a/Sources/CodexBar/MenuCardView.swift +++ b/Sources/CodexBar/MenuCardView.swift @@ -44,6 +44,7 @@ struct UsageMenuCardView: View { let paceOnTop: Bool let warningMarkerPercents: [Double] let workdayMarkerPercents: [Double] + let workdayTickAppearance: WorkdayTickAppearance let cardStyle: Bool let sessionEquivalentDetail: UsagePaceText.SessionEquivalentDetail? @@ -61,6 +62,7 @@ struct UsageMenuCardView: View { paceOnTop: Bool, warningMarkerPercents: [Double] = [], workdayMarkerPercents: [Double] = [], + workdayTickAppearance: WorkdayTickAppearance = .subtle, cardStyle: Bool = false, sessionEquivalentDetail: UsagePaceText.SessionEquivalentDetail? = nil) { @@ -77,6 +79,7 @@ struct UsageMenuCardView: View { self.paceOnTop = paceOnTop self.warningMarkerPercents = warningMarkerPercents self.workdayMarkerPercents = workdayMarkerPercents + self.workdayTickAppearance = workdayTickAppearance self.cardStyle = cardStyle self.sessionEquivalentDetail = sessionEquivalentDetail } @@ -539,7 +542,8 @@ private struct MetricRow: View { pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents, - workdayMarkerPercents: self.metric.workdayMarkerPercents) + workdayMarkerPercents: self.metric.workdayMarkerPercents, + workdayTickAppearance: self.metric.workdayTickAppearance) if let metaText = presentation.metaText { Text(metaText) .font(.footnote) @@ -1525,9 +1529,12 @@ extension UsageMenuCardView.Model { warningMarkerPercents: Self.warningMarkerPercents( thresholds: input.quotaWarningThresholds[.weekly], showUsed: input.usageBarsShowUsed), - workdayMarkerPercents: workDayMarkerPercents( - workDays: input.workDaysPerWeek, - windowMinutes: weekly.windowMinutes), + workdayMarkerPercents: input.workdayTickAppearance == .hidden + ? [] + : workDayMarkerPercents( + workDays: input.workDaysPerWeek, + windowMinutes: weekly.windowMinutes), + workdayTickAppearance: input.workdayTickAppearance, sessionEquivalentDetail: Self.sessionEquivalentDetail( input: input, weeklyWindow: weekly, diff --git a/Sources/CodexBar/PreferencesMenuPane.swift b/Sources/CodexBar/PreferencesMenuPane.swift index 6b0ffb6d45..242671ee91 100644 --- a/Sources/CodexBar/PreferencesMenuPane.swift +++ b/Sources/CodexBar/PreferencesMenuPane.swift @@ -33,6 +33,19 @@ struct MenuPane: View { Text(MenuSettingsMenuOptions.weeklyProgressWorkDaysLabel(workDays)) }) + SettingsMenuPicker( + selection: self.$settings.workdayTickAppearance, + options: MenuSettingsMenuOptions.workdayTickAppearances, + label: { + SettingsRowLabel( + L("workday_tick_appearance_title"), + subtitle: L("workday_tick_appearance_subtitle")) + }, + optionLabel: { appearance in + Text(appearance.label) + }) + .disabled(self.settings.weeklyProgressWorkDays == nil) + SettingsMenuPicker( selection: self.$settings.resetTimesOption, options: MenuSettingsMenuOptions.resetTimes, diff --git a/Sources/CodexBar/PreferencesMenuPicker.swift b/Sources/CodexBar/PreferencesMenuPicker.swift index e072bc0d2d..5a81f7f398 100644 --- a/Sources/CodexBar/PreferencesMenuPicker.swift +++ b/Sources/CodexBar/PreferencesMenuPicker.swift @@ -71,6 +71,7 @@ enum MenuBarSettingsMenuOptions { enum MenuSettingsMenuOptions { static let weeklyProgressWorkDays: [Int?] = [nil, 4, 5, 7] + static let workdayTickAppearances = WorkdayTickAppearance.allCases static let multiAccountLayouts = MultiAccountMenuLayout.allCases static let usageBarsFill = UsageBarsFillOption.allCases static let resetTimes = ResetTimesOption.allCases diff --git a/Sources/CodexBar/PreferencesProviderDetailView.swift b/Sources/CodexBar/PreferencesProviderDetailView.swift index 31f6b3701e..8bb499c6e6 100644 --- a/Sources/CodexBar/PreferencesProviderDetailView.swift +++ b/Sources/CodexBar/PreferencesProviderDetailView.swift @@ -503,7 +503,8 @@ private struct ProviderMetricInlineRow: View { pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents, - workdayMarkerPercents: self.metric.workdayMarkerPercents) + workdayMarkerPercents: self.metric.workdayMarkerPercents, + workdayTickAppearance: self.metric.workdayTickAppearance) .frame(maxWidth: .infinity) if let metaText = presentation.metaText { diff --git a/Sources/CodexBar/PreferencesProvidersPane.swift b/Sources/CodexBar/PreferencesProvidersPane.swift index 986ac799e6..8b9891b1ae 100644 --- a/Sources/CodexBar/PreferencesProvidersPane.swift +++ b/Sources/CodexBar/PreferencesProvidersPane.swift @@ -604,6 +604,7 @@ struct ProvidersPane: View { .weekly: self.quotaWarningMarkerThresholds(provider: provider, window: .weekly), ], workDaysPerWeek: self.settings.weeklyProgressWorkDays, + workdayTickAppearance: self.settings.workdayTickAppearance, now: now) return UsageMenuCardView.Model.make(input) } diff --git a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings index 91cf51a0ae..ba0f911abf 100644 --- a/Sources/CodexBar/Resources/ar.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ar.lproj/Localizable.strings @@ -613,6 +613,11 @@ "show_quota_warning_markers_subtitle" = "ارسم علامات عتبة على أشرطة الاستخدام عند تكوين تحذيرات الحصص."; "weekly_progress_work_days_title" = "أيام العمل الأسبوعية للتقدم"; "weekly_progress_work_days_subtitle" = "حدد أيام عمل لمؤشرات شريط الاستخدام الأسبوعي وحسابات السرعة."; +"workday_tick_appearance_title" = "مظهر علامات أيام العمل"; +"workday_tick_appearance_subtitle" = "اختر كيفية ظهور حدود أيام العمل على أشرطة الاستخدام الأسبوعية."; +"workday_tick_appearance_hidden" = "مخفية"; +"workday_tick_appearance_subtle" = "خفيفة"; +"workday_tick_appearance_high_contrast" = "تباين عالٍ"; "show_provider_changelog_links_title" = "اعرض روابط سجل التغييرات لمزود الخدمة"; "show_provider_changelog_links_subtitle" = "يضيف روابط ملاحظات الإصدار لمزودي الخدمة المدعومين بدعم CLI إلى القائمة."; "show_credits_extra_usage_title" = "اعتمادات العرض + الاستخدام الإضافي"; diff --git a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings index a4517ef04f..6f8af22ebe 100644 --- a/Sources/CodexBar/Resources/ca.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ca.lproj/Localizable.strings @@ -591,6 +591,11 @@ "show_quota_warning_markers_subtitle" = "Dibuixa marques de llindar a les barres d'ús quan hi ha avisos de quota configurats."; "weekly_progress_work_days_title" = "Dies laborables del progrés setmanal"; "weekly_progress_work_days_subtitle" = "Definiu els dies laborables per als marcadors de les barres d'ús setmanal i els càlculs de ritme."; +"workday_tick_appearance_title" = "Aparença de les marques dels dies laborables"; +"workday_tick_appearance_subtitle" = "Trieu com es mostren els límits dels dies laborables a les barres d'ús setmanal."; +"workday_tick_appearance_hidden" = "Ocultes"; +"workday_tick_appearance_subtle" = "Subtils"; +"workday_tick_appearance_high_contrast" = "Contrast alt"; "show_provider_changelog_links_title" = "Mostreu els enllaços al registre de canvis del proveïdor"; "show_provider_changelog_links_subtitle" = "Afegiu al menú enllaços a les notes de versió dels proveïdors compatibles basats en CLI."; "show_credits_extra_usage_title" = "Mostreu crèdits + ús addicional"; diff --git a/Sources/CodexBar/Resources/de.lproj/Localizable.strings b/Sources/CodexBar/Resources/de.lproj/Localizable.strings index d319691269..0ff581c51b 100644 --- a/Sources/CodexBar/Resources/de.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/de.lproj/Localizable.strings @@ -605,6 +605,11 @@ "show_quota_warning_markers_subtitle" = "Zeichnen Sie Schwellenwertmarkierungen auf Nutzungsbalken, wenn Kontingentwarnungen konfiguriert sind."; "weekly_progress_work_days_title" = "Wöchentliche Fortschrittsarbeitstage"; "weekly_progress_work_days_subtitle" = "Legt Arbeitstage für Markierungen in wöchentlichen Nutzungsbalken und Tempo-Berechnungen fest."; +"workday_tick_appearance_title" = "Darstellung der Arbeitstagsmarkierungen"; +"workday_tick_appearance_subtitle" = "Wähle, wie Arbeitstagsgrenzen in wöchentlichen Nutzungsbalken erscheinen."; +"workday_tick_appearance_hidden" = "Ausgeblendet"; +"workday_tick_appearance_subtle" = "Dezent"; +"workday_tick_appearance_high_contrast" = "Hoher Kontrast"; "show_provider_changelog_links_title" = "Links zum Änderungsprotokoll des Anbieters anzeigen"; "show_provider_changelog_links_subtitle" = "Fügt dem Menü Versionshinweise-Links für unterstützte CLI-gestützte Anbieter hinzu."; "show_credits_extra_usage_title" = "Credits + zusätzliche Nutzung anzeigen"; diff --git a/Sources/CodexBar/Resources/en.lproj/Localizable.strings b/Sources/CodexBar/Resources/en.lproj/Localizable.strings index aa1580cb8f..0369b2114d 100644 --- a/Sources/CodexBar/Resources/en.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/en.lproj/Localizable.strings @@ -590,6 +590,11 @@ "show_quota_warning_markers_subtitle" = "Draw threshold tick marks on usage bars when quota warnings are configured."; "weekly_progress_work_days_title" = "Work days"; "weekly_progress_work_days_subtitle" = "Set work days for weekly usage-bar markers and pace calculations."; +"workday_tick_appearance_title" = "Workday tick appearance"; +"workday_tick_appearance_subtitle" = "Choose how workday boundaries appear on weekly usage bars."; +"workday_tick_appearance_hidden" = "Hidden"; +"workday_tick_appearance_subtle" = "Subtle"; +"workday_tick_appearance_high_contrast" = "High contrast"; "show_provider_changelog_links_title" = "Show provider changelog links"; "show_provider_changelog_links_subtitle" = "Adds release-notes links for supported CLI-backed providers to the menu."; "show_credits_extra_usage_title" = "Show credits & extra usage"; diff --git a/Sources/CodexBar/Resources/es.lproj/Localizable.strings b/Sources/CodexBar/Resources/es.lproj/Localizable.strings index 6fc06eb6af..5cc79f8d17 100644 --- a/Sources/CodexBar/Resources/es.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/es.lproj/Localizable.strings @@ -599,6 +599,11 @@ "show_quota_warning_markers_subtitle" = "Dibuja marcas de umbral en las barras de uso cuando hay avisos de cuota configurados."; "weekly_progress_work_days_title" = "Días laborables del progreso semanal"; "weekly_progress_work_days_subtitle" = "Define los días laborables para los marcadores de las barras de uso semanal y los cálculos de ritmo."; +"workday_tick_appearance_title" = "Aspecto de las marcas de días laborables"; +"workday_tick_appearance_subtitle" = "Elige cómo aparecen los límites de los días laborables en las barras de uso semanal."; +"workday_tick_appearance_hidden" = "Ocultas"; +"workday_tick_appearance_subtle" = "Sutiles"; +"workday_tick_appearance_high_contrast" = "Contraste alto"; "show_provider_changelog_links_title" = "Mostrar enlaces al registro de cambios del proveedor"; "show_provider_changelog_links_subtitle" = "Añade al menú enlaces a las notas de versión de los proveedores compatibles basados en CLI."; "show_credits_extra_usage_title" = "Mostrar créditos + uso adicional"; diff --git a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings index 4c16ef57d8..5a6b4f0c0e 100644 --- a/Sources/CodexBar/Resources/fa.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fa.lproj/Localizable.strings @@ -613,6 +613,11 @@ "show_quota_warning_markers_subtitle" = "علامت تیک آستانه را روی نوارهای استفاده هنگام پیکربندی هشدارهای سهمیه رسم کنید."; "weekly_progress_work_days_title" = "روزهای کاری پیشرفت هفتگی"; "weekly_progress_work_days_subtitle" = "روزهای کاری را برای نشانگرهای نوار مصرف هفتگی و محاسبات سرعت تعیین کنید."; +"workday_tick_appearance_title" = "نمایش نشانه‌های روز کاری"; +"workday_tick_appearance_subtitle" = "نحوهٔ نمایش مرز روزهای کاری در نوارهای مصرف هفتگی را انتخاب کنید."; +"workday_tick_appearance_hidden" = "پنهان"; +"workday_tick_appearance_subtle" = "ملایم"; +"workday_tick_appearance_high_contrast" = "کنتراست بالا"; "show_provider_changelog_links_title" = "لینک های تغییرات ارائه دهنده را نمایش دهید"; "show_provider_changelog_links_subtitle" = "لینک های یادداشت های انتشار برای ارائه دهندگان پشتیبانی شده با پشتیبانی CLI به منو اضافه می شود."; "show_credits_extra_usage_title" = "نمایش اعتبارها + استفاده اضافی"; diff --git a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings index 5aa43d5d01..642258bae1 100644 --- a/Sources/CodexBar/Resources/fr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/fr.lproj/Localizable.strings @@ -607,6 +607,11 @@ "show_quota_warning_markers_subtitle" = "Dessinez des coches de seuil sur les barres d’utilisation lorsque des avertissements de quota sont configurés."; "weekly_progress_work_days_title" = "Jours de travail hebdomadaires"; "weekly_progress_work_days_subtitle" = "Définit les jours ouvrés pour les repères des barres d’utilisation hebdomadaire et le calcul du rythme."; +"workday_tick_appearance_title" = "Apparence des repères de jours ouvrés"; +"workday_tick_appearance_subtitle" = "Choisissez l’apparence des limites des jours ouvrés sur les barres d’utilisation hebdomadaire."; +"workday_tick_appearance_hidden" = "Masqués"; +"workday_tick_appearance_subtle" = "Discrets"; +"workday_tick_appearance_high_contrast" = "Contraste élevé"; "show_provider_changelog_links_title" = "Afficher les liens du journal des modifications du fournisseur"; "show_provider_changelog_links_subtitle" = "Ajoute au menu des liens de notes de version pour les fournisseurs pris en charge par CLI."; "show_credits_extra_usage_title" = "Afficher les crédits + utilisation supplémentaire"; diff --git a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings index 31845a12f8..51aacd4671 100644 --- a/Sources/CodexBar/Resources/gl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/gl.lproj/Localizable.strings @@ -586,6 +586,11 @@ "show_quota_warning_markers_subtitle" = "Debuxa marcas de limiar nas barras de uso cando hai avisos de cota configurados."; "weekly_progress_work_days_title" = "Días laborables do progreso semanal"; "weekly_progress_work_days_subtitle" = "Define os días laborables para os marcadores das barras de uso semanal e os cálculos de ritmo."; +"workday_tick_appearance_title" = "Aspecto das marcas dos días laborables"; +"workday_tick_appearance_subtitle" = "Escolle como aparecen os límites dos días laborables nas barras de uso semanal."; +"workday_tick_appearance_hidden" = "Ocultas"; +"workday_tick_appearance_subtle" = "Sutís"; +"workday_tick_appearance_high_contrast" = "Contraste alto"; "show_provider_changelog_links_title" = "Amosar as ligazóns ao rexistro de cambios do provedor"; "show_provider_changelog_links_subtitle" = "Engade ao menú enlaces ás notas de versión dos provedores compatibles baseados en CLI."; "show_credits_extra_usage_title" = "Amosar créditos + uso adicional"; diff --git a/Sources/CodexBar/Resources/id.lproj/Localizable.strings b/Sources/CodexBar/Resources/id.lproj/Localizable.strings index 5832025fc3..c3af976146 100644 --- a/Sources/CodexBar/Resources/id.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/id.lproj/Localizable.strings @@ -615,6 +615,11 @@ "show_quota_warning_markers_subtitle" = "Gambar tanda centang ambang batas pada bilah penggunaan saat peringatan kuota dikonfigurasi."; "weekly_progress_work_days_title" = "Hari kerja progres mingguan"; "weekly_progress_work_days_subtitle" = "Atur hari kerja untuk penanda bilah penggunaan mingguan dan perhitungan pace."; +"workday_tick_appearance_title" = "Tampilan tanda hari kerja"; +"workday_tick_appearance_subtitle" = "Pilih tampilan batas hari kerja pada bilah penggunaan mingguan."; +"workday_tick_appearance_hidden" = "Tersembunyi"; +"workday_tick_appearance_subtle" = "Samar"; +"workday_tick_appearance_high_contrast" = "Kontras tinggi"; "show_provider_changelog_links_title" = "Tampilkan tautan changelog penyedia"; "show_provider_changelog_links_subtitle" = "Menambahkan tautan catatan rilis untuk penyedia berbasis CLI yang didukung ke menu."; "show_credits_extra_usage_title" = "Tampilkan kredit + penggunaan ekstra"; diff --git a/Sources/CodexBar/Resources/it.lproj/Localizable.strings b/Sources/CodexBar/Resources/it.lproj/Localizable.strings index ec76fed89f..cf52ba2c97 100644 --- a/Sources/CodexBar/Resources/it.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/it.lproj/Localizable.strings @@ -615,6 +615,11 @@ "show_quota_warning_markers_subtitle" = "Disegna tacche soglia sulle barre quando gli avvisi quota sono configurati."; "weekly_progress_work_days_title" = "Giorni lavorativi progresso settimanale"; "weekly_progress_work_days_subtitle" = "Disegna i confini giornalieri sulle barre settimanali."; +"workday_tick_appearance_title" = "Aspetto delle tacche dei giorni lavorativi"; +"workday_tick_appearance_subtitle" = "Scegli come mostrare i confini dei giorni lavorativi nelle barre di utilizzo settimanale."; +"workday_tick_appearance_hidden" = "Nascoste"; +"workday_tick_appearance_subtle" = "Discrete"; +"workday_tick_appearance_high_contrast" = "Contrasto elevato"; "show_provider_changelog_links_title" = "Mostra link changelog provider"; "show_provider_changelog_links_subtitle" = "Aggiunge link alle note di rilascio per i provider CLI supportati."; "show_credits_extra_usage_title" = "Mostra crediti + uso extra"; diff --git a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings index 21d1180d60..bf07b69209 100644 --- a/Sources/CodexBar/Resources/ja.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ja.lproj/Localizable.strings @@ -604,6 +604,11 @@ "show_quota_warning_markers_subtitle" = "クォータ警告が設定されている場合、使用量バーにしきい値の目盛りを描画します。"; "weekly_progress_work_days_title" = "週間進捗の作業日"; "weekly_progress_work_days_subtitle" = "週間使用量バーの目盛りとペース計算に使用する作業日を設定します。"; +"workday_tick_appearance_title" = "稼働日の目盛りの表示"; +"workday_tick_appearance_subtitle" = "週間使用量バーで稼働日の境界をどのように表示するか選択します。"; +"workday_tick_appearance_hidden" = "非表示"; +"workday_tick_appearance_subtle" = "控えめ"; +"workday_tick_appearance_high_contrast" = "高コントラスト"; "show_provider_changelog_links_title" = "プロバイダの変更履歴リンクを表示"; "show_provider_changelog_links_subtitle" = "対応する CLI ベースのプロバイダのリリースノートへのリンクをメニューに追加します。"; "show_credits_extra_usage_title" = "クレジットと追加使用量を表示"; diff --git a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings index 5a5c19785b..f0ef284d77 100644 --- a/Sources/CodexBar/Resources/ko.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ko.lproj/Localizable.strings @@ -596,6 +596,11 @@ "show_quota_warning_markers_subtitle" = "할당량 경고가 구성된 경우 사용량 막대에 임계값 눈금 표시를 그립니다."; "weekly_progress_work_days_title" = "주간 진행률 근무일"; "weekly_progress_work_days_subtitle" = "주간 사용량 막대 눈금과 페이스 계산에 사용할 근무일을 설정합니다."; +"workday_tick_appearance_title" = "근무일 눈금 모양"; +"workday_tick_appearance_subtitle" = "주간 사용량 막대에 근무일 경계를 표시하는 방식을 선택합니다."; +"workday_tick_appearance_hidden" = "숨김"; +"workday_tick_appearance_subtle" = "은은하게"; +"workday_tick_appearance_high_contrast" = "고대비"; "show_provider_changelog_links_title" = "공급자 변경 로그 링크 표시"; "show_provider_changelog_links_subtitle" = "지원되는 CLI 기반 공급자의 릴리스 노트 링크를 메뉴에 추가합니다."; "show_credits_extra_usage_title" = "크레딧 + 추가 사용량 표시"; diff --git a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings index 3f386b640c..9998dec11b 100644 --- a/Sources/CodexBar/Resources/nl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/nl.lproj/Localizable.strings @@ -607,6 +607,11 @@ "show_quota_warning_markers_subtitle" = "Teken drempelmarkeringen op gebruiksbalken wanneer quotawaarschuwingen zijn geconfigureerd."; "weekly_progress_work_days_title" = "Wekelijkse voortgang werkdagen"; "weekly_progress_work_days_subtitle" = "Stel werkdagen in voor markeringen op wekelijkse gebruiksbalken en tempoberekeningen."; +"workday_tick_appearance_title" = "Weergave van werkdagmarkeringen"; +"workday_tick_appearance_subtitle" = "Kies hoe werkdaggrenzen op wekelijkse gebruiksbalken worden weergegeven."; +"workday_tick_appearance_hidden" = "Verborgen"; +"workday_tick_appearance_subtle" = "Subtiel"; +"workday_tick_appearance_high_contrast" = "Hoog contrast"; "show_provider_changelog_links_title" = "Toon provider changelog-links"; "show_provider_changelog_links_subtitle" = "Voegt koppelingen naar release-opmerkingen voor ondersteunde CLI-ondersteunde providers toe aan het menu."; "show_credits_extra_usage_title" = "Toon credits + extra gebruik"; diff --git a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings index 2eb1478c74..80adcd292a 100644 --- a/Sources/CodexBar/Resources/pl.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pl.lproj/Localizable.strings @@ -615,6 +615,11 @@ "show_quota_warning_markers_subtitle" = "Rysuje znaczniki progów na paskach użycia, gdy skonfigurowano ostrzeżenia limitu."; "weekly_progress_work_days_title" = "Dni robocze postępu tygodniowego"; "weekly_progress_work_days_subtitle" = "Rysuje znaczniki granic dni na tygodniowych paskach użycia."; +"workday_tick_appearance_title" = "Wygląd znaczników dni roboczych"; +"workday_tick_appearance_subtitle" = "Wybierz sposób wyświetlania granic dni roboczych na tygodniowych paskach użycia."; +"workday_tick_appearance_hidden" = "Ukryte"; +"workday_tick_appearance_subtle" = "Subtelne"; +"workday_tick_appearance_high_contrast" = "Wysoki kontrast"; "show_provider_changelog_links_title" = "Pokaż linki do changelogów dostawców"; "show_provider_changelog_links_subtitle" = "Dodaje linki do informacji o wydaniach dla obsługiwanych dostawców CLI w menu."; "show_credits_extra_usage_title" = "Pokaż kredyty + dodatkowe użycie"; diff --git a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings index 09cdd79c12..8ec9586423 100644 --- a/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/pt-BR.lproj/Localizable.strings @@ -604,6 +604,11 @@ "show_quota_warning_markers_subtitle" = "Desenha marcas de limite nas barras de uso quando os alertas de cota estão configurados."; "weekly_progress_work_days_title" = "Dias úteis no progresso semanal"; "weekly_progress_work_days_subtitle" = "Define os dias úteis para marcadores das barras de uso semanal e cálculos de ritmo."; +"workday_tick_appearance_title" = "Aparência das marcas de dias úteis"; +"workday_tick_appearance_subtitle" = "Escolha como os limites dos dias úteis aparecem nas barras de uso semanal."; +"workday_tick_appearance_hidden" = "Ocultas"; +"workday_tick_appearance_subtle" = "Sutis"; +"workday_tick_appearance_high_contrast" = "Alto contraste"; "show_provider_changelog_links_title" = "Mostrar links de changelog dos provedores"; "show_provider_changelog_links_subtitle" = "Adiciona links de notas de versão para provedores baseados em CLI compatíveis no menu."; "show_credits_extra_usage_title" = "Mostrar créditos + uso extra"; diff --git a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings index 3d9c80387e..7bc9737c23 100644 --- a/Sources/CodexBar/Resources/ru.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/ru.lproj/Localizable.strings @@ -608,6 +608,11 @@ "show_quota_warning_markers_subtitle" = "Рисует отметки порогов на индикаторах использования, если настроены предупреждения о квотах."; "weekly_progress_work_days_title" = "Рабочие дни для недельного прогресса"; "weekly_progress_work_days_subtitle" = "Задаёт рабочие дни для недельных отметок использования и расчёта темпа."; +"workday_tick_appearance_title" = "Вид отметок рабочих дней"; +"workday_tick_appearance_subtitle" = "Выберите, как отображать границы рабочих дней на недельных полосах использования."; +"workday_tick_appearance_hidden" = "Скрыты"; +"workday_tick_appearance_subtle" = "Ненавязчивые"; +"workday_tick_appearance_high_contrast" = "Высокий контраст"; "show_provider_changelog_links_title" = "Показывать ссылки на журналы изменений провайдеров"; "show_provider_changelog_links_subtitle" = "Добавляет в меню ссылки на заметки к релизам для поддерживаемых CLI-провайдеров."; "show_credits_extra_usage_title" = "Показывать кредиты и доп. использование"; diff --git a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings index ec237d5528..2a616615d3 100644 --- a/Sources/CodexBar/Resources/sv.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/sv.lproj/Localizable.strings @@ -606,6 +606,11 @@ "show_quota_warning_markers_subtitle" = "Rita tröskelmarkeringar på användningsstaplar när kvotvarningar är konfigurerade."; "weekly_progress_work_days_title" = "Arbetsdagar i veckoförlopp"; "weekly_progress_work_days_subtitle" = "Ställ in arbetsdagar för markeringar i veckostaplar och tempoberäkningar."; +"workday_tick_appearance_title" = "Utseende för arbetsdagsmarkeringar"; +"workday_tick_appearance_subtitle" = "Välj hur arbetsdagsgränser visas i veckans användningsstaplar."; +"workday_tick_appearance_hidden" = "Dolda"; +"workday_tick_appearance_subtle" = "Diskreta"; +"workday_tick_appearance_high_contrast" = "Hög kontrast"; "show_provider_changelog_links_title" = "Visa länkar till leverantörers ändringsloggar"; "show_provider_changelog_links_subtitle" = "Lägger till länkar till utgåvekommentarer för stödda CLI-baserade leverantörer i menyn."; "show_credits_extra_usage_title" = "Visa krediter och extra användning"; diff --git a/Sources/CodexBar/Resources/th.lproj/Localizable.strings b/Sources/CodexBar/Resources/th.lproj/Localizable.strings index cb4b5a04d1..f65a7a4ec5 100644 --- a/Sources/CodexBar/Resources/th.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/th.lproj/Localizable.strings @@ -613,6 +613,11 @@ "show_quota_warning_markers_subtitle" = "วาดเครื่องหมายถูกเกณฑ์บนแถบการใช้งานเมื่อมีการกําหนดค่าคําเตือนโควต้า"; "weekly_progress_work_days_title" = "วันทํางานความคืบหน้ารายสัปดาห์"; "weekly_progress_work_days_subtitle" = "กําหนดวันทํางานสําหรับเครื่องหมายแถบการใช้งานรายสัปดาห์และการคํานวณความเร็ว"; +"workday_tick_appearance_title" = "ลักษณะขีดวันทำงาน"; +"workday_tick_appearance_subtitle" = "เลือกวิธีแสดงขอบเขตวันทำงานบนแถบการใช้งานรายสัปดาห์"; +"workday_tick_appearance_hidden" = "ซ่อน"; +"workday_tick_appearance_subtle" = "ละเอียดอ่อน"; +"workday_tick_appearance_high_contrast" = "คอนทราสต์สูง"; "show_provider_changelog_links_title" = "แสดงลิงก์บันทึกการเปลี่ยนแปลงของผู้ให้บริการ"; "show_provider_changelog_links_subtitle" = "เพิ่มลิงก์บันทึกประจํารุ่นสําหรับผู้ให้บริการที่ได้รับการสนับสนุน CLI ในเมนู"; "show_credits_extra_usage_title" = "แสดงเครดิต + การใช้งานเพิ่มเติม"; diff --git a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings index 4f2faec2da..4987f2cf0c 100644 --- a/Sources/CodexBar/Resources/tr.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/tr.lproj/Localizable.strings @@ -613,6 +613,11 @@ "show_quota_warning_markers_subtitle" = "Kota uyarıları yapılandırıldığında kullanım çubuklarına eşik çizgileri çizer."; "weekly_progress_work_days_title" = "Haftalık ilerleme iş günleri"; "weekly_progress_work_days_subtitle" = "Haftalık kullanım çubuğu işaretleri ve tempo hesaplamaları için iş günlerini ayarlar."; +"workday_tick_appearance_title" = "İş günü işaretlerinin görünümü"; +"workday_tick_appearance_subtitle" = "İş günü sınırlarının haftalık kullanım çubuklarında nasıl görüneceğini seçin."; +"workday_tick_appearance_hidden" = "Gizli"; +"workday_tick_appearance_subtle" = "Hafif"; +"workday_tick_appearance_high_contrast" = "Yüksek kontrast"; "show_provider_changelog_links_title" = "Sağlayıcı değişiklik günlüğü bağlantılarını göster"; "show_provider_changelog_links_subtitle" = "Desteklenen CLI destekli sağlayıcılar için menüye sürüm notları bağlantıları ekler."; "show_credits_extra_usage_title" = "Krediler + ekstra kullanımı göster"; diff --git a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings index 3b471f9a92..28cf720950 100644 --- a/Sources/CodexBar/Resources/uk.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/uk.lproj/Localizable.strings @@ -607,6 +607,11 @@ "show_quota_warning_markers_subtitle" = "Малюйте порогові позначки на панелях використання, коли налаштовано попередження про квоту."; "weekly_progress_work_days_title" = "Щотижневі робочі дні"; "weekly_progress_work_days_subtitle" = "Задайте робочі дні для позначок на смугах тижневого використання та розрахунків темпу."; +"workday_tick_appearance_title" = "Вигляд позначок робочих днів"; +"workday_tick_appearance_subtitle" = "Виберіть, як відображати межі робочих днів на смугах тижневого використання."; +"workday_tick_appearance_hidden" = "Приховані"; +"workday_tick_appearance_subtle" = "Ненав’язливі"; +"workday_tick_appearance_high_contrast" = "Високий контраст"; "show_provider_changelog_links_title" = "Показати посилання журналу змін провайдера"; "show_provider_changelog_links_subtitle" = "Додає в меню посилання на примітки до випуску для підтримуваних постачальників, що підтримують CLI."; "show_credits_extra_usage_title" = "Показати кредити + додаткове використання"; diff --git a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings index 0d43da6353..6e1fb5d596 100644 --- a/Sources/CodexBar/Resources/vi.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/vi.lproj/Localizable.strings @@ -603,6 +603,11 @@ "show_quota_warning_markers_subtitle" = "Vẽ dấu kiểm ngưỡng trên thanh Mức sử dụng khi cảnh báo Hạn mức được định cấu hình."; "weekly_progress_work_days_title" = "Tiến độ ngày làm việc hàng tuần"; "weekly_progress_work_days_subtitle" = "Đặt ngày làm việc cho các vạch trên thanh sử dụng hằng tuần và phép tính nhịp độ."; +"workday_tick_appearance_title" = "Kiểu vạch ngày làm việc"; +"workday_tick_appearance_subtitle" = "Chọn cách hiển thị ranh giới ngày làm việc trên thanh sử dụng hằng tuần."; +"workday_tick_appearance_hidden" = "Ẩn"; +"workday_tick_appearance_subtle" = "Nhẹ"; +"workday_tick_appearance_high_contrast" = "Tương phản cao"; "show_provider_changelog_links_title" = "Hiển thị Nhà cung cấp liên kết nhật ký thay đổi"; "show_provider_changelog_links_subtitle" = "Thêm liên kết ghi chú phát hành cho các nhà cung cấp được hỗ trợ CLI vào menu."; "show_credits_extra_usage_title" = "Hiển thị tín dụng + phần Mức sử dụng"; diff --git a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings index 3ed80d8bcf..c7ec764107 100644 --- a/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hans.lproj/Localizable.strings @@ -968,6 +968,11 @@ "%d unreadable item(s) skipped" = "已跳过 %d 个不可读项目"; "weekly_progress_work_days_title" = "工作日刻度线"; "weekly_progress_work_days_subtitle" = "设置用于每周用量条刻度和进度计算的工作日。"; +"workday_tick_appearance_title" = "工作日刻度外观"; +"workday_tick_appearance_subtitle" = "选择工作日边界在每周用量条上的显示方式。"; +"workday_tick_appearance_hidden" = "隐藏"; +"workday_tick_appearance_subtle" = "柔和"; +"workday_tick_appearance_high_contrast" = "高对比度"; "copilot_device_code" = "设备代码已复制到剪贴板:%1$@\n\n请在以下地址验证:%2$@"; "copilot_waiting_text" = "请在浏览器中完成登录。\n登录完成后,此窗口会自动关闭。"; "vertex_ai_login_instructions" = "要跟踪 Vertex AI 用量,请通过 Google Cloud 进行认证。\n\n1. 打开终端\n2. 运行:gcloud auth application-default login\n3. 按照浏览器提示登录\n4. 设置你的项目:gcloud config set project PROJECT_ID\n\n是否现在打开终端?"; diff --git a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings index e8b803e593..c7104bd917 100644 --- a/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings +++ b/Sources/CodexBar/Resources/zh-Hant.lproj/Localizable.strings @@ -606,6 +606,11 @@ "show_quota_warning_markers_subtitle" = "設定配額提醒後,在使用量條上繪製門檻刻度標記。"; "weekly_progress_work_days_title" = "每週進度工作日標記"; "weekly_progress_work_days_subtitle" = "設定用於每週用量條刻度與進度計算的工作日。"; +"workday_tick_appearance_title" = "工作日刻度外觀"; +"workday_tick_appearance_subtitle" = "選擇工作日邊界在每週用量條上的顯示方式。"; +"workday_tick_appearance_hidden" = "隱藏"; +"workday_tick_appearance_subtle" = "柔和"; +"workday_tick_appearance_high_contrast" = "高對比度"; "show_provider_changelog_links_title" = "顯示提供者版本資訊連結"; "show_provider_changelog_links_subtitle" = "在選單中為支援的 CLI 提供者新增發行說明連結。"; "show_credits_extra_usage_title" = "顯示額度 + 額外使用量"; diff --git a/Sources/CodexBar/SettingsStore+Defaults.swift b/Sources/CodexBar/SettingsStore+Defaults.swift index 4c7f74c01f..f01ace9659 100644 --- a/Sources/CodexBar/SettingsStore+Defaults.swift +++ b/Sources/CodexBar/SettingsStore+Defaults.swift @@ -269,6 +269,14 @@ extension SettingsStore { } } + var workdayTickAppearance: WorkdayTickAppearance { + get { WorkdayTickAppearance(rawValue: self.defaultsState.workdayTickAppearanceRaw) ?? .subtle } + set { + self.defaultsState.workdayTickAppearanceRaw = newValue.rawValue + self.userDefaults.set(newValue.rawValue, forKey: "workdayTickAppearance") + } + } + var usageBarsShowUsed: Bool { get { self.defaultsState.usageBarsShowUsed } set { diff --git a/Sources/CodexBar/SettingsStore+MenuObservation.swift b/Sources/CodexBar/SettingsStore+MenuObservation.swift index 89e5234dd0..a0a9736502 100644 --- a/Sources/CodexBar/SettingsStore+MenuObservation.swift +++ b/Sources/CodexBar/SettingsStore+MenuObservation.swift @@ -23,6 +23,7 @@ extension SettingsStore { _ = self.quotaWarningOnScreenAlertEnabled _ = self.quotaWarningMarkersVisible _ = self.weeklyProgressWorkDays + _ = self.workdayTickAppearance _ = self.usageBarsShowUsed _ = self.resetTimesShowAbsolute _ = self.providerChangelogLinksEnabled diff --git a/Sources/CodexBar/SettingsStore+Sync.swift b/Sources/CodexBar/SettingsStore+Sync.swift index 9a7de1966b..058fe63fed 100644 --- a/Sources/CodexBar/SettingsStore+Sync.swift +++ b/Sources/CodexBar/SettingsStore+Sync.swift @@ -57,6 +57,7 @@ extension SettingsStore { quotaWarningOnScreenAlertEnabled: self.quotaWarningOnScreenAlertEnabled, quotaWarningMarkersVisible: self.quotaWarningMarkersVisible, weeklyProgressWorkDays: self.weeklyProgressWorkDays, + workdayTickAppearance: self.workdayTickAppearance.rawValue, usageBarsShowUsed: self.usageBarsShowUsed, resetTimesShowAbsolute: self.resetTimesShowAbsolute, costUsageEnabled: self.costUsageEnabled, @@ -89,6 +90,11 @@ extension SettingsStore { self.quotaWarningOnScreenAlertEnabled = preferences.quotaWarningOnScreenAlertEnabled self.quotaWarningMarkersVisible = preferences.quotaWarningMarkersVisible self.weeklyProgressWorkDays = preferences.weeklyProgressWorkDays + if let rawAppearance = preferences.workdayTickAppearance, + let appearance = WorkdayTickAppearance(rawValue: rawAppearance) + { + self.workdayTickAppearance = appearance + } self.usageBarsShowUsed = preferences.usageBarsShowUsed self.resetTimesShowAbsolute = preferences.resetTimesShowAbsolute self.costUsageEnabled = preferences.costUsageEnabled diff --git a/Sources/CodexBar/SettingsStore.swift b/Sources/CodexBar/SettingsStore.swift index ef5eaa3d7b..6eda71615c 100644 --- a/Sources/CodexBar/SettingsStore.swift +++ b/Sources/CodexBar/SettingsStore.swift @@ -131,6 +131,24 @@ enum MultiAccountMenuLayout: String, CaseIterable, Identifiable { } } +enum WorkdayTickAppearance: String, CaseIterable, Identifiable { + case hidden + case subtle + case highContrast + + var id: String { + self.rawValue + } + + var label: String { + switch self { + case .hidden: L("workday_tick_appearance_hidden") + case .subtle: L("workday_tick_appearance_subtle") + case .highContrast: L("workday_tick_appearance_high_contrast") + } + } +} + enum CostSummaryDisplayStyle: String, CaseIterable, Identifiable { case inlineSummary case costSubmenu @@ -450,6 +468,8 @@ extension SettingsStore { userDefaults.set(true, forKey: "quotaWarningMarkersVisible") } let weeklyProgressWorkDays = userDefaults.object(forKey: "weeklyProgressWorkDays") as? Int + let workdayTickAppearanceRaw = userDefaults.string(forKey: "workdayTickAppearance") + ?? WorkdayTickAppearance.subtle.rawValue let usageBarsShowUsed = userDefaults.object(forKey: "usageBarsShowUsed") as? Bool ?? false let resetTimesShowAbsolute = userDefaults.object(forKey: "resetTimesShowAbsolute") as? Bool ?? false let providerChangelogLinksEnabled = userDefaults.object( @@ -580,6 +600,7 @@ extension SettingsStore { quotaWarningOnScreenAlertEnabled: quotaWarnings.onScreenAlertEnabled, quotaWarningMarkersVisible: quotaWarningMarkersVisible, weeklyProgressWorkDays: weeklyProgressWorkDays, + workdayTickAppearanceRaw: workdayTickAppearanceRaw, usageBarsShowUsed: usageBarsShowUsed, resetTimesShowAbsolute: resetTimesShowAbsolute, providerChangelogLinksEnabled: providerChangelogLinksEnabled, diff --git a/Sources/CodexBar/SettingsStoreState.swift b/Sources/CodexBar/SettingsStoreState.swift index e83b03b1e9..6fee78682b 100644 --- a/Sources/CodexBar/SettingsStoreState.swift +++ b/Sources/CodexBar/SettingsStoreState.swift @@ -24,6 +24,7 @@ struct SettingsDefaultsState { var quotaWarningOnScreenAlertEnabled: Bool var quotaWarningMarkersVisible: Bool var weeklyProgressWorkDays: Int? + var workdayTickAppearanceRaw: String var usageBarsShowUsed: Bool var resetTimesShowAbsolute: Bool var providerChangelogLinksEnabled: Bool diff --git a/Sources/CodexBar/StatusItemController+MenuCardModel.swift b/Sources/CodexBar/StatusItemController+MenuCardModel.swift index 6b5ba0f84e..ad726ec8ca 100644 --- a/Sources/CodexBar/StatusItemController+MenuCardModel.swift +++ b/Sources/CodexBar/StatusItemController+MenuCardModel.swift @@ -156,6 +156,7 @@ extension StatusItemController { .weekly: self.quotaWarningMarkerThresholds(provider: target, window: .weekly), ], workDaysPerWeek: self.settings.weeklyProgressWorkDays, + workdayTickAppearance: self.settings.workdayTickAppearance, usesLiveSubtitle: surface == .liveCard, preferredCurrencyCode: self.settings.preferredCurrencyCode, now: now) diff --git a/Sources/CodexBar/UsageProgressBar.swift b/Sources/CodexBar/UsageProgressBar.swift index de00f0486a..9b89b5774b 100644 --- a/Sources/CodexBar/UsageProgressBar.swift +++ b/Sources/CodexBar/UsageProgressBar.swift @@ -39,6 +39,7 @@ struct UsageProgressBar: View { let paceOnTop: Bool let warningMarkerPercents: [Double] let workdayMarkerPercents: [Double] + let workdayTickAppearance: WorkdayTickAppearance @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.displayScale) private var displayScale @@ -49,7 +50,8 @@ struct UsageProgressBar: View { pacePercent: Double? = nil, paceOnTop: Bool = true, warningMarkerPercents: [Double] = [], - workdayMarkerPercents: [Double] = []) + workdayMarkerPercents: [Double] = [], + workdayTickAppearance: WorkdayTickAppearance = .subtle) { self.percent = percent self.tint = tint @@ -58,6 +60,7 @@ struct UsageProgressBar: View { self.paceOnTop = paceOnTop self.warningMarkerPercents = warningMarkerPercents self.workdayMarkerPercents = workdayMarkerPercents + self.workdayTickAppearance = workdayTickAppearance } private var clamped: Double { @@ -80,7 +83,8 @@ struct UsageProgressBar: View { let showTip = self.pacePercent != nil && tipWidth > 0.5 let markers = Self.resolvedMarkers( warningPercents: self.warningMarkerPercents, - workdayPercents: self.workdayMarkerPercents) + workdayPercents: self.workdayMarkerPercents, + workdayAppearance: self.workdayTickAppearance) let cornerRadius = size.height / 2 let cornerSize = CGSize(width: cornerRadius, height: cornerRadius) @@ -122,10 +126,16 @@ struct UsageProgressBar: View { markerStripePath, with: .color(Self.warningMarkerColor(isHighlighted: self.isHighlighted))) case .workdayBoundary: - let markerRect = Self.workdayMarkerRect(x: x, size: size, scale: scale) + let markerRect = Self.workdayMarkerRect( + x: x, + size: size, + scale: scale, + appearance: self.workdayTickAppearance) context.fill( Path(markerRect), - with: .color(Self.workdayMarkerColor(isHighlighted: self.isHighlighted))) + with: .color(Self.workdayMarkerColor( + isHighlighted: self.isHighlighted, + appearance: self.workdayTickAppearance))) } } @@ -163,7 +173,8 @@ struct UsageProgressBar: View { var parts = [L("%d percent", Self.displayPercent(self.clamped))] let markers = Self.resolvedMarkers( warningPercents: self.warningMarkerPercents, - workdayPercents: self.workdayMarkerPercents) + workdayPercents: self.workdayMarkerPercents, + workdayAppearance: self.workdayTickAppearance) let warnings = markers.filter { $0.kind == .quotaWarning }.map(Self.markerPercentText) let workdays = markers.filter { $0.kind == .workdayBoundary }.map(Self.markerPercentText) if !warnings.isEmpty { @@ -177,10 +188,11 @@ struct UsageProgressBar: View { nonisolated static func resolvedMarkers( warningPercents: [Double], - workdayPercents: [Double]) -> [Marker] + workdayPercents: [Double], + workdayAppearance: WorkdayTickAppearance = .subtle) -> [Marker] { let warnings = Self.normalizedMarkerPercents(warningPercents) - let workdays = Self.normalizedMarkerPercents(workdayPercents) + let workdays = workdayAppearance == .hidden ? [] : Self.normalizedMarkerPercents(workdayPercents) .filter { workday in !warnings.contains { abs($0 - workday) < 0.001 } } return ( warnings.map { Marker(percent: $0, kind: .quotaWarning) } + @@ -288,10 +300,25 @@ struct UsageProgressBar: View { height: markerRect.height) } - nonisolated static func workdayMarkerRect(x: CGFloat, size: CGSize, scale rawScale: CGFloat) -> CGRect { + nonisolated static func workdayMarkerRect( + x: CGFloat, + size: CGSize, + scale rawScale: CGFloat, + appearance: WorkdayTickAppearance = .subtle) -> CGRect + { let scale = max(rawScale, 1) - let width = 1 / scale - let height = max(1 / scale, size.height * 0.5) + let width: CGFloat + let height: CGFloat + switch appearance { + case .hidden: + return .zero + case .subtle: + width = 1 / scale + height = max(1 / scale, size.height * 0.5) + case .highContrast: + width = max(1 / scale, 1.96) + height = size.height + } let align: (CGFloat) -> CGFloat = { value in (value * scale).rounded() / scale } @@ -311,8 +338,26 @@ struct UsageProgressBar: View { isHighlighted ? .white.opacity(0.96) : .primary.opacity(0.68) } - nonisolated static func workdayMarkerColor(isHighlighted: Bool) -> Color { - isHighlighted ? .white.opacity(0.55) : .primary.opacity(0.30) + nonisolated static func workdayMarkerColor( + isHighlighted: Bool, + appearance: WorkdayTickAppearance = .subtle) -> Color + { + switch appearance { + case .hidden: + .clear + case .subtle: + if isHighlighted { + .white.opacity(0.55) + } else { + .primary.opacity(0.30) + } + case .highContrast: + if isHighlighted { + .white.opacity(0.96) + } else { + .primary.opacity(0.85) + } + } } private nonisolated static func displayPercent(_ percent: Double) -> Int { diff --git a/Sources/CodexBarCore/Sync/SyncModels.swift b/Sources/CodexBarCore/Sync/SyncModels.swift index 2b03fe8db4..d7b39c0f90 100644 --- a/Sources/CodexBarCore/Sync/SyncModels.swift +++ b/Sources/CodexBarCore/Sync/SyncModels.swift @@ -231,6 +231,7 @@ public struct SyncedPreferences: Codable, Sendable { public var quotaWarningOnScreenAlertEnabled: Bool public var quotaWarningMarkersVisible: Bool public var weeklyProgressWorkDays: Int? + public var workdayTickAppearance: String? public var usageBarsShowUsed: Bool public var resetTimesShowAbsolute: Bool public var costUsageEnabled: Bool @@ -262,6 +263,7 @@ public struct SyncedPreferences: Codable, Sendable { quotaWarningOnScreenAlertEnabled: Bool, quotaWarningMarkersVisible: Bool, weeklyProgressWorkDays: Int?, + workdayTickAppearance: String? = nil, usageBarsShowUsed: Bool, resetTimesShowAbsolute: Bool, costUsageEnabled: Bool, @@ -292,6 +294,7 @@ public struct SyncedPreferences: Codable, Sendable { self.quotaWarningOnScreenAlertEnabled = quotaWarningOnScreenAlertEnabled self.quotaWarningMarkersVisible = quotaWarningMarkersVisible self.weeklyProgressWorkDays = weeklyProgressWorkDays + self.workdayTickAppearance = workdayTickAppearance self.usageBarsShowUsed = usageBarsShowUsed self.resetTimesShowAbsolute = resetTimesShowAbsolute self.costUsageEnabled = costUsageEnabled diff --git a/Tests/CodexBarTests/CloudSyncSettingsTests.swift b/Tests/CodexBarTests/CloudSyncSettingsTests.swift index 267cb289a3..c1bc8404cb 100644 --- a/Tests/CodexBarTests/CloudSyncSettingsTests.swift +++ b/Tests/CodexBarTests/CloudSyncSettingsTests.swift @@ -36,6 +36,7 @@ struct CloudSyncSettingsTests { remote.costUsageEnabled = true remote.preferredCurrencyCode = "EUR" remote.refreshFrequency = RefreshFrequency.thirtyMinutes.rawValue + remote.workdayTickAppearance = WorkdayTickAppearance.highContrast.rawValue store.applySyncedPreferences(remote) @@ -44,10 +45,27 @@ struct CloudSyncSettingsTests { #expect(store.costUsageEnabled) #expect(store.preferredCurrencyCode == "EUR") #expect(store.refreshFrequency == .thirtyMinutes) + #expect(store.workdayTickAppearance == .highContrast) #expect(store.debugMenuEnabled) #expect(store.iCloudSyncEnabled) } + @Test + func `legacy synced preferences without workday appearance decode compatibly`() throws { + let fixture = try self.makeFixture("legacy-workday-appearance") + let payload = PreferencesSyncPayload(preferences: fixture.store.syncedPreferences) + let encoded = try CanonicalSyncJSON.encode(payload) + var object = try #require(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + var preferences = try #require(object["preferences"] as? [String: Any]) + preferences.removeValue(forKey: "workdayTickAppearance") + object["preferences"] = preferences + let legacyData = try JSONSerialization.data(withJSONObject: object) + + let decoded = try CanonicalSyncJSON.decode(PreferencesSyncPayload.self, from: legacyData) + + #expect(decoded.preferences.workdayTickAppearance == nil) + } + @Test func `config watcher suppresses self writes and observes external atomic replacement`() async throws { let directory = FileManager.default.temporaryDirectory diff --git a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift index 9114bf1e91..992eb6c58f 100644 --- a/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift +++ b/Tests/CodexBarTests/LocalizationLanguageCatalogTests.swift @@ -370,9 +370,20 @@ struct LocalizationLanguageCatalogTests { let title = catalog["weekly_progress_work_days_title"]?.trimmingCharacters(in: .whitespacesAndNewlines) let subtitle = catalog["weekly_progress_work_days_subtitle"]? .trimmingCharacters(in: .whitespacesAndNewlines) + let appearanceKeys = [ + "workday_tick_appearance_title", + "workday_tick_appearance_subtitle", + "workday_tick_appearance_hidden", + "workday_tick_appearance_subtle", + "workday_tick_appearance_high_contrast", + ] #expect(title?.isEmpty == false, "Missing workday title in \(catalogURL.lastPathComponent)") #expect(subtitle?.isEmpty == false, "Missing workday subtitle in \(catalogURL.lastPathComponent)") + for key in appearanceKeys { + let value = catalog[key]?.trimmingCharacters(in: .whitespacesAndNewlines) + #expect(value?.isEmpty == false, "Missing \(key) in \(catalogURL.lastPathComponent)") + } } } diff --git a/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift b/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift index 0423a79e16..0fbc786b16 100644 --- a/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift +++ b/Tests/CodexBarTests/MenuCardModelCodexProjectionTests.swift @@ -103,35 +103,46 @@ struct MenuCardModelCodexProjectionTests { dashboardRequiresLogin: false, now: now)) - let model = UsageMenuCardView.Model.make(.init( - provider: .codex, - metadata: metadata, - snapshot: snapshot, - codexProjection: projection, - credits: nil, - creditsError: nil, - dashboard: nil, - dashboardError: nil, - tokenSnapshot: nil, - tokenError: nil, - account: AccountInfo(email: "user@example.com", plan: "Pro"), - isRefreshing: false, - lastError: nil, - usageBarsShowUsed: false, - resetTimeDisplayStyle: .countdown, - tokenCostUsageEnabled: false, - showOptionalCreditsAndExtraUsage: true, - hidePersonalInfo: false, - quotaWarningThresholds: [.session: [], .weekly: []], - workDaysPerWeek: 5, - now: now)) + func makeModel(appearance: WorkdayTickAppearance) -> UsageMenuCardView.Model { + UsageMenuCardView.Model.make(.init( + provider: .codex, + metadata: metadata, + snapshot: snapshot, + codexProjection: projection, + credits: nil, + creditsError: nil, + dashboard: nil, + dashboardError: nil, + tokenSnapshot: nil, + tokenError: nil, + account: AccountInfo(email: "user@example.com", plan: "Pro"), + isRefreshing: false, + lastError: nil, + usageBarsShowUsed: false, + resetTimeDisplayStyle: .countdown, + tokenCostUsageEnabled: false, + showOptionalCreditsAndExtraUsage: true, + hidePersonalInfo: false, + quotaWarningThresholds: [.session: [], .weekly: []], + workDaysPerWeek: 5, + workdayTickAppearance: appearance, + now: now)) + } + + let model = makeModel(appearance: .highContrast) let weekly = try #require(model.metrics.first { $0.id == "secondary" }) #expect(weekly.warningMarkerPercents.isEmpty) #expect(weekly.workdayMarkerPercents == [20.0, 40.0, 60.0, 80.0]) + #expect(weekly.workdayTickAppearance == .highContrast) let session = try #require(model.metrics.first { $0.id == "primary" }) #expect(session.warningMarkerPercents.isEmpty) + + let hiddenModel = makeModel(appearance: .hidden) + let hiddenWeekly = try #require(hiddenModel.metrics.first { $0.id == "secondary" }) + #expect(hiddenWeekly.workdayMarkerPercents.isEmpty) + #expect(hiddenWeekly.workdayTickAppearance == .hidden) } @Test diff --git a/Tests/CodexBarTests/MenuCardQuotaWarningMarkerTests.swift b/Tests/CodexBarTests/MenuCardQuotaWarningMarkerTests.swift index 744f6fa7c0..c36766fc27 100644 --- a/Tests/CodexBarTests/MenuCardQuotaWarningMarkerTests.swift +++ b/Tests/CodexBarTests/MenuCardQuotaWarningMarkerTests.swift @@ -71,6 +71,34 @@ struct MenuCardQuotaWarningMarkerTests { #expect(abs(rect.midX - 50) <= 0.5) } + @Test + func `high contrast workday boundary is full height and ninety six percent wider`() { + let scales: [CGFloat] = [1, 2, 3] + + for scale in scales { + let rect = UsageProgressBar.workdayMarkerRect( + x: 50, + size: CGSize(width: 100, height: 6), + scale: scale, + appearance: .highContrast) + + #expect(rect.width == 1.96) + #expect(rect.height == 6) + #expect(rect.minY == 0) + #expect(abs(rect.midX - 50) <= 1 / scale) + } + } + + @Test + func `hidden workday appearance removes boundary markers`() { + let markers = UsageProgressBar.resolvedMarkers( + warningPercents: [80], + workdayPercents: [20, 40, 60, 80], + workdayAppearance: .hidden) + + #expect(markers == [.init(percent: 80, kind: .quotaWarning)]) + } + @Test func `quota warning wins when marker kinds overlap`() { let markers = UsageProgressBar.resolvedMarkers( diff --git a/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift b/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift index 0353846677..de55440feb 100644 --- a/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift +++ b/Tests/CodexBarTests/PreferencesPaneSmokeTests.swift @@ -98,6 +98,7 @@ struct PreferencesPaneSmokeTests { #expect(MenuBarSettingsMenuOptions.switcherRows == SwitcherRowsOption.allCases) #expect(MenuSettingsMenuOptions.weeklyProgressWorkDays == [nil, 4, 5, 7]) #expect(MenuSettingsMenuOptions.weeklyProgressWorkDaysLabel(nil) == L("Automatic")) + #expect(MenuSettingsMenuOptions.workdayTickAppearances == WorkdayTickAppearance.allCases) #expect(MenuSettingsMenuOptions.multiAccountLayouts == MultiAccountMenuLayout.allCases) #expect(MenuSettingsMenuOptions.usageBarsFill == UsageBarsFillOption.allCases) #expect(MenuSettingsMenuOptions.resetTimes == ResetTimesOption.allCases) @@ -108,12 +109,14 @@ struct PreferencesPaneSmokeTests { let settings = Self.makeSettingsStore(suite: suite) settings.menuBarDisplayMode = .resetTime settings.weeklyProgressWorkDays = 7 + settings.workdayTickAppearance = .highContrast settings.multiAccountMenuLayout = .stacked settings.costSummaryDisplayStyle = .costSubmenu let reloaded = Self.makeSettingsStore(suite: suite, reset: false) #expect(reloaded.menuBarDisplayMode == .resetTime) #expect(reloaded.weeklyProgressWorkDays == 7) + #expect(reloaded.workdayTickAppearance == .highContrast) #expect(reloaded.multiAccountMenuLayout == .stacked) #expect(reloaded.costSummaryDisplayStyle == .costSubmenu) } diff --git a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift index 54eeb5affa..b4b766a62d 100644 --- a/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift +++ b/Tests/CodexBarTests/ProviderArchitectureGatekeeperTests.swift @@ -884,13 +884,13 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact provider-owned construct passes a fixed identity to shared infrastructure."), SuppressedProviderReference( path: "Sources/CodexBar/SettingsStore+MenuObservation.swift", - line: 96, + line: 97, anchor: "_ = self[providerConfig: .synthetic, field: .apiKey]", expectedProviderIDs: ["synthetic"], reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), SuppressedProviderReference( path: "Sources/CodexBar/SettingsStore+MenuObservation.swift", - line: 115, + line: 116, anchor: "_ = self[providerConfig: .warp, field: .apiKey]", expectedProviderIDs: ["warp"], reason: "This observation touchpoint reads a fixed provider field so UI invalidation tracks that setting."), @@ -1825,7 +1825,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 250, + line: 251, anchor: "if input.provider == .kiro {", expectedProviderIDs: ["kilo", "kiro"], expectedReferenceCount: 2, @@ -1833,7 +1833,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 268, + line: 269, anchor: "if input.provider == .mimo, input.snapshot != nil {", expectedProviderIDs: ["claude", "mimo"], expectedReferenceCount: 2, @@ -1841,7 +1841,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 472, + line: 473, anchor: "if input.provider == .factory, snapshot.tertiary != nil {", expectedProviderIDs: ["alibabatokenplan", "amp", "crof", "cursor", "doubao", "factory", "grok", "sub2api"], expectedReferenceCount: 12, @@ -1862,7 +1862,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 633, + line: 634, anchor: "case .minimax:", expectedProviderIDs: ["codex", "minimax", "poe"], expectedReferenceCount: 3, @@ -1870,7 +1870,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 837, + line: 838, anchor: "if input.provider == .codex, !input.showOptionalCreditsAndExtraUsage {", expectedProviderIDs: ["claude", "codex", "copilot"], expectedReferenceCount: 4, @@ -1878,7 +1878,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 862, + line: 863, anchor: "let resetText = input.provider == .sub2api && namedWindow.window.resetsAt == nil", expectedProviderIDs: ["doubao", "sub2api"], expectedReferenceCount: 3, @@ -1886,7 +1886,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 933, + line: 934, anchor: "if input.provider == .antigravity,", expectedProviderIDs: ["antigravity"], expectedReferenceCount: 1, @@ -1894,7 +1894,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 967, + line: 968, anchor: "if provider == .claude, window.windowMinutes != 10080 {", expectedProviderIDs: ["antigravity", "claude", "codex"], expectedReferenceCount: 4, @@ -1902,7 +1902,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView+ModelHelpers.swift", - line: 999, + line: 1000, anchor: "guard input.provider == .antigravity else { return nil }", expectedProviderIDs: ["antigravity"], expectedReferenceCount: 1, @@ -1910,7 +1910,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 191, + line: 194, anchor: "if provider == .openrouter, metric.id == \"primary\" {", expectedProviderIDs: ["openrouter"], expectedReferenceCount: 1, @@ -1918,7 +1918,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 489, + line: 492, anchor: "if self.provider != .codex || self.showsCodexHint,", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -1926,7 +1926,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 670, + line: 674, anchor: "guard self.model.provider == .doubao else { return nil }", expectedProviderIDs: ["doubao"], expectedReferenceCount: 1, @@ -1934,7 +1934,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1040, + line: 1044, anchor: "if input.provider == .sub2api {", expectedProviderIDs: ["sub2api"], expectedReferenceCount: 1, @@ -1942,7 +1942,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "The sub2api menu card localizes and groups provider-owned usage detail rows for display."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1100, + line: 1104, anchor: "if provider == .kiro,", expectedProviderIDs: ["kilo", "kiro"], expectedReferenceCount: 2, @@ -1950,7 +1950,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1123, + line: 1127, anchor: "if provider == .minimax {", expectedProviderIDs: ["codex", "minimax"], expectedReferenceCount: 2, @@ -1958,7 +1958,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1158, + line: 1162, anchor: "guard let loginMethod = snapshot?.loginMethod(for: .kilo) else {", expectedProviderIDs: ["kilo"], expectedReferenceCount: 1, @@ -1966,7 +1966,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1242, + line: 1246, anchor: "if input.provider == .antigravity {", expectedProviderIDs: ["antigravity", "mistral"], expectedReferenceCount: 2, @@ -1974,7 +1974,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1262, + line: 1266, anchor: "if input.provider == .codex, let codexProjection = input.codexProjection {", expectedProviderIDs: ["codex"], expectedReferenceCount: 1, @@ -1982,7 +1982,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1278, + line: 1282, anchor: "if input.provider != .codex, let weekly = snapshot.secondary {", expectedProviderIDs: ["alibaba", "alibabatokenplan", "codex", "perplexity", "sub2api"], expectedReferenceCount: 5, @@ -1996,7 +1996,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1317, + line: 1321, anchor: "if input.provider == .kilo || input.provider == .kimi,", expectedProviderIDs: ["kilo", "kimi"], expectedReferenceCount: 2, @@ -2004,7 +2004,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1413, + line: 1417, anchor: "var paceDetail = if input.provider == .kimi {", expectedProviderIDs: ["kimi"], expectedReferenceCount: 1, @@ -2012,7 +2012,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1429, + line: 1433, anchor: "if input.provider == .warp,", expectedProviderIDs: ["chutes", "kilo", "kiro", "litellm", "sub2api", "warp"], expectedReferenceCount: 6, @@ -2020,7 +2020,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1462, + line: 1466, anchor: "if input.provider == .alibaba || input.provider == .alibabatokenplan,", expectedProviderIDs: ["alibaba", "alibabatokenplan", "copilot", "crof", "manus", "perplexity", "zenmux"], expectedReferenceCount: 8, @@ -2037,7 +2037,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact shared renderer maps provider-owned presentation data into the generic UI model."), AllowedProviderConstruct( path: "Sources/CodexBar/MenuCardView.swift", - line: 1503, + line: 1507, anchor: "if input.provider == .synthetic,", expectedProviderIDs: ["synthetic"], expectedReferenceCount: 1, @@ -2281,7 +2281,7 @@ struct ProviderArchitectureGatekeeperTests { reason: "This exact app-runtime bridge coordinates provider-owned state through the shared controller."), AllowedProviderConstruct( path: "Sources/CodexBar/SettingsStore.swift", - line: 1027, + line: 1048, anchor: "if !seen.contains(.factory), let zaiIndex = ordered.firstIndex(of: .zai) {", expectedProviderIDs: ["factory", "minimax", "zai"], expectedReferenceCount: 8, diff --git a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift index ee48c1f852..9a21605189 100644 --- a/Tests/CodexBarTests/SettingsStoreCoverageTests.swift +++ b/Tests/CodexBarTests/SettingsStoreCoverageTests.swift @@ -847,6 +847,27 @@ struct SettingsStoreCoverageTests { #expect(reloaded4.weeklyProgressWorkDays == nil) } + @Test + func `workday tick appearance defaults to subtle and persists valid choices`() throws { + let suite = "SettingsStoreCoverageTests-workday-tick-appearance" + let defaults = try #require(UserDefaults(suiteName: suite)) + defaults.removePersistentDomain(forName: suite) + let configStore = testConfigStore(suiteName: suite) + + let fresh = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) + #expect(fresh.workdayTickAppearance == .subtle) + + fresh.workdayTickAppearance = .highContrast + #expect(defaults.string(forKey: "workdayTickAppearance") == WorkdayTickAppearance.highContrast.rawValue) + + let reloaded = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) + #expect(reloaded.workdayTickAppearance == .highContrast) + + defaults.set("unknown", forKey: "workdayTickAppearance") + let invalid = Self.makeSettingsStore(userDefaults: defaults, configStore: configStore) + #expect(invalid.workdayTickAppearance == .subtle) + } + @Test func `preferred currency defaults to USD and persists an explicit selection`() throws { let suite = "SettingsStoreCoverageTests-preferred-currency" diff --git a/Tests/CodexBarTests/SettingsStoreTests.swift b/Tests/CodexBarTests/SettingsStoreTests.swift index 0155c6397c..27fe7c2e70 100644 --- a/Tests/CodexBarTests/SettingsStoreTests.swift +++ b/Tests/CodexBarTests/SettingsStoreTests.swift @@ -1507,7 +1507,7 @@ struct SettingsStoreTests { } @Test - func `menu observation token updates on weekly progress work days changes`() async throws { + func `menu observation token updates on workday display changes`() async throws { let suite = "SettingsStoreTests-observation-weekly-progress-work-days" let defaults = try #require(UserDefaults(suiteName: suite)) defaults.removePersistentDomain(forName: suite) @@ -1519,18 +1519,20 @@ struct SettingsStoreTests { zaiTokenStore: NoopZaiTokenStore(), syntheticTokenStore: NoopSyntheticTokenStore()) - let didChange = ObservationFlag() - - withObservationTracking { - _ = store.menuObservationToken - } onChange: { - didChange.set() + func expectObservation(_ update: () -> Void) async { + let didChange = ObservationFlag() + withObservationTracking { + _ = store.menuObservationToken + } onChange: { + didChange.set() + } + update() + try? await Task.sleep(nanoseconds: 50_000_000) + #expect(didChange.get() == true) } - store.weeklyProgressWorkDays = 5 - try? await Task.sleep(nanoseconds: 50_000_000) - - #expect(didChange.get() == true) + await expectObservation { store.weeklyProgressWorkDays = 5 } + await expectObservation { store.workdayTickAppearance = .highContrast } } @Test From 2fb2a2248746b443a1cfa2eeb7247126107d761f Mon Sep 17 00:00:00 2001 From: dylanstieri Date: Sat, 15 Aug 2026 00:53:31 -0700 Subject: [PATCH 2/2] docs: add workday tick appearance changelog entry --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 796c11ba10..bccc95ad08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 0.50.1 — Unreleased + +### Added +- Usage bars: make workday tick marks configurable with hidden, subtle, and high-contrast appearances. + ## 0.50.0 — 2026-08-15 ### Added