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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 19 additions & 8 deletions Bubo/Presentation/Views/Components/Backlog/SmartActions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,31 @@ struct SmartActions: View {
// expires, `chipRow` takes over again.
if let applied = recentApplied, applied.isFresh {
reasoningRow(applied)
} else {
} else if hasMeaningfulContent {
chipRow
}
// else: collapse the row entirely. With no primary chip
// (calm state), no ranked actions, and no trailing slot,
// the only thing left was an orphan «More» pill floating
// mid-popover — which is more chrome than signal. Hide it;
// the same actions are still reachable via ⌘K.
}
// `DS.Animation.machineWork` — slow ease-out, no bounce. State
// transitions here read as «machine reasoning»: forecast flips
// from .fits to .over because the user added a long task, the
// calm `Plan day…` row is replaced by the hard «Schedule
// overflow» chip. Bounce would feel playful when the goal is a
// calm reasoning beat. The hash key combines the three signals
// that drive `resolvedState`.
.animation(DS.Animation.machineWork, value: stateHash)
}

/// True when `chipRow` will produce something other than a lone
/// «More» pill. Drives the empty-state collapse in `body` so the
/// chip row vanishes when the popover is calm and unranked, instead
/// of leaving a single floating button.
private var hasMeaningfulContent: Bool {
switch resolvedState {
case .hard, .soft:
return true
case .calm:
return !rankedCalmActions.isEmpty || trailing != nil
}
}

/// Single horizontal-scrolling chip row that absorbs the legacy
/// hard/soft/calm card variants. Each state contributes at most one
/// «primary» chip up front; ranked top-N calm actions follow when
Expand Down
48 changes: 0 additions & 48 deletions Bubo/Presentation/Views/Components/Banner/StatusBannerView.swift

This file was deleted.

10 changes: 6 additions & 4 deletions Bubo/Presentation/Views/Components/Event/DaySectionView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,16 @@ struct DaySectionHeader<Trailing: View>: View {
.frame(maxWidth: .infinity, alignment: .leading)
.background(
// `.day-header` translucent surface-window backdrop.
// Use a hairline darker overlay so the strip reads as a
// banner regardless of the skin's base colour.
// Tuned for the dark skins — at 0.04 the strip was visually
// identical to the popover bg. 0.10 on white over a dark
// base produces the same "slightly darker translucent
// banner" the prototype carries via `surface-window 92%`.
Rectangle()
.fill(skin.resolvedTextPrimary.opacity(0.04))
.fill(skin.resolvedTextPrimary.opacity(0.10))
)
.overlay(alignment: .bottom) {
Rectangle()
.fill(skin.resolvedTextPrimary.opacity(0.06))
.fill(skin.resolvedTextPrimary.opacity(0.14))
.frame(height: 0.5)
}
.accessibilityElement(children: .combine)
Expand Down
32 changes: 8 additions & 24 deletions Bubo/Presentation/Views/Components/Event/EventRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -556,11 +556,11 @@ struct EventRowView: View {
// `.bb-event .stripe`: a 3pt-wide vertical bar that
// `align-self: stretch` spans the full row height, so a
// 3-line event reads as one coloured block from top to bottom.
// Past rows fade, upcoming and «now» rows stay opaque.
// Opacity floor of 0.7 — at 0.45 the stripe disappeared into
// the dark popover bg on past / colorless rows (visible bug
// on the live screenshot).
let baseColor: Color = event.colorTag?.color ?? skin.resolvedTextTertiary
let opacity: Double = event.colorTag == nil
? 0.45
: (event.isUpcoming || isHappeningNow ? 0.95 : 0.55)
let opacity: Double = (event.isUpcoming || isHappeningNow) ? 0.95 : 0.75
RoundedRectangle(cornerRadius: 1.5, style: .continuous)
.fill(baseColor.opacity(opacity))
.frame(width: 3)
Expand All @@ -569,24 +569,10 @@ struct EventRowView: View {
.accessibilityLabel(isHappeningNow ? "Happening now" : "")
}

// MARK: - Now pill (trailing badge)

/// Filled orange capsule rendered in the row's trailing slot when
/// the current time falls inside this event. Mirrors the prototype's
/// `.bb-badge[data-style="filled"]` with `--system-orange`. Hidden
/// for past / upcoming rows.
@ViewBuilder
private var nowPill: some View {
if isHappeningNow {
Text("Now")
.font(.system(size: 10.5, weight: .bold, design: skin.resolvedFontDesign))
.foregroundStyle(.white)
.padding(.horizontal, DS.Spacing.sm)
.padding(.vertical, DS.Spacing.xxs + 1)
.background(Capsule().fill(Color.orange))
.accessibilityLabel("Happening now")
}
}
// The «happening now» signal is carried by `TimelineNowRule` —
// the red NOW · HH:MM hairline rendered in the day-group switch
// between past and current rows. An orange Now pill on the row
// itself was redundant and competed with the same cue.

// MARK: - Time Column

Expand Down Expand Up @@ -625,8 +611,6 @@ struct EventRowView: View {
timeColumn(now)
eventDetails
Spacer(minLength: DS.Spacing.sm)
nowPill
.padding(.top, DS.Spacing.xxs)
}
.contentShape(Rectangle())
}
Expand Down
94 changes: 79 additions & 15 deletions Bubo/Presentation/Views/MenuBar/MenuBarView+MainContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -95,34 +95,98 @@ extension MenuBarView {

// MARK: - Inline status row

/// Single thin status row — highest-priority issue only.
/// Replaces the stack of mutually-exclusive `StatusBanner`s and the
/// `PermissionBannersCarousel`. Hidden when everything is healthy.
/// Single thin status row — highest-priority issue only. Renders
/// as a quiet one-line caption with a small leading icon instead
/// of a full-width capsule banner: the «something's off» signal
/// shouldn't outshout the timeline it sits above. Hidden when
/// everything is healthy.
///
/// Priority order (highest first):
/// 1. Calendar permission missing — actionable, blocks the
/// core data source.
/// 2. Reminders permission missing — same shape, lower
/// priority since reminders are an optional source.
/// 3. Offline — global system state, may resolve on its own.
/// 4. Cached data — showing data but it might be stale.
/// 5. Sync error — surfaced from the service.
@ViewBuilder
var inlineStatusRow: some View {
if !networkMonitor.isConnected {
StatusBanner(
if settings.isCalendarSyncEnabled, !calendarHasAccess {
inlineStatusLine(
icon: "calendar.badge.exclamationmark",
text: "Calendar access needed",
actionLabel: "Open Settings",
action: { openSettingsPane(.calendars) }
)
} else if settings.isRemindersSyncEnabled, !remindersHasAccess {
inlineStatusLine(
icon: "checklist",
text: "Reminders access needed",
actionLabel: "Open Settings",
action: { openSettingsPane(.appleReminders) }
)
} else if !networkMonitor.isConnected {
inlineStatusLine(
icon: "wifi.slash",
text: "No internet — calendar data may be outdated",
color: skin.resolvedWarningColor
text: "Offline — calendar may be outdated"
)
} else if reminderService.isUsingCache {
StatusBanner(
inlineStatusLine(
icon: "arrow.triangle.2.circlepath",
text: "Showing cached data",
color: skin.resolvedWarningColor
text: "Showing cached data"
)
.frame(maxWidth: .infinity, alignment: .center)
} else if let error = reminderService.syncError, settings.isCalendarSyncEnabled {
StatusBanner(
inlineStatusLine(
icon: "exclamationmark.triangle.fill",
text: error,
color: skin.resolvedWarningColor
text: error
)
.frame(maxWidth: .infinity, alignment: .center)
}
}

/// Route the popover to a specific Settings pane and bring the
/// app to the front. Same path the empty state uses for
/// «Adjust calendars».
private func openSettingsPane(_ pane: SettingsView.SettingsPane) {
Haptics.tap()
SettingsViewModel.pendingPane = pane
openSettings()
NSApp.activate()
}

@ViewBuilder
private func inlineStatusLine(
icon: String,
text: String,
actionLabel: String? = nil,
action: (() -> Void)? = nil
) -> some View {
HStack(spacing: DS.Spacing.xs) {
Image(systemName: icon)
.font(.system(size: 10, weight: .medium))
.foregroundStyle(skin.resolvedTextTertiary)
Text(text)
.font(.system(size: 11, weight: .medium, design: skin.resolvedFontDesign))
.foregroundStyle(skin.resolvedTextTertiary)
.lineLimit(1)
.truncationMode(.tail)
Spacer(minLength: DS.Spacing.xs)
if let actionLabel, let action {
Button(action: action) {
Text(actionLabel)
.font(.system(size: 11, weight: .semibold, design: skin.resolvedFontDesign))
.foregroundStyle(skin.accentColor)
}
.buttonStyle(.plain)
.accessibilityLabel(actionLabel)
}
}
.padding(.horizontal, DS.Spacing.contentMargin)
.padding(.top, DS.Spacing.xxs)
.padding(.bottom, DS.Spacing.xs)
.accessibilityElement(children: .combine)
.accessibilityLabel(text)
}

// MARK: - Main Content

var mainContent: some View {
Expand Down