feat: floating tab bar, NLP date parsing, and task date picker

- Tab bar: TickTick-style floating pill (cornerRadius 28, shadow, systemGray5 active highlight)
- NLP: month+day parsing (8th June, June 8), recurrence (every year/week/etc), intent stripping (remind me of)
- Recurrence chip in add-task toolbar; isRecurring saved with task
- Date chip defaults to Today on new tasks; tapping opens full date picker
- Date picker: calendar grid, inline time wheel, Repeat menu (Date tab)
- Duration tab: Start/End column selector, live duration counter, All Day toggle
- TaskItem: added endDate and isAllDay fields
- Fixed hidden Unicode character corrupting + operator in TodayView

Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
Robin Kutesa
2026-05-30 23:49:30 +03:00
parent a11258dea5
commit 68efa02803
6 changed files with 1601 additions and 300 deletions

View File

@@ -77,6 +77,7 @@ struct ContentView: View {
.preferredColorScheme(preferredScheme)
.fullScreenCover(isPresented: $showOnboarding) {
OnboardingView(isPresented: $showOnboarding)
.environmentObject(workoutVM)
.onDisappear { hasOnboarded = true }
}
.onAppear {
@@ -101,6 +102,8 @@ struct ContentView: View {
showQuickAddTask = true
case .today:
withAnimation(KisaniSpring.micro) { selectedTab = 0 }
case .next3:
withAnimation(KisaniSpring.micro) { selectedTab = 0 }
case .calendar:
withAnimation(KisaniSpring.micro) { selectedTab = 1 }
case .workout:
@@ -165,11 +168,8 @@ struct ContentView: View {
selection: $selectedTab,
calIcon: calIconImage,
showMatrix: showMatrixTab,
showWorkout: showWorkoutTab,
isCollapsed: tabState.isCollapsed
showWorkout: showWorkoutTab
)
.padding(.horizontal, 24)
.padding(.bottom, 8)
}
.onAppear { UITabBar.appearance().isHidden = true }
}
@@ -202,12 +202,10 @@ struct ContentView: View {
// MARK: - Custom Tab Bar
// Adds a transparent bottom safe-area inset equal to the floating tab bar height,
// so scroll content and FABs inside each tab clear the bar.
private extension View {
func tabBarInset() -> some View {
safeAreaInset(edge: .bottom, spacing: 0) {
Color.clear.frame(height: 62)
Color.clear.frame(height: 64)
}
}
}
@@ -217,7 +215,6 @@ struct KisaniTabBar: View {
let calIcon: Image
var showMatrix: Bool
var showWorkout: Bool
var isCollapsed: Bool = false
@Environment(\.colorScheme) private var cs
private struct Entry: Identifiable {
@@ -244,33 +241,35 @@ struct KisaniTabBar: View {
} label: {
ZStack {
if selection == entry.id {
RoundedRectangle(cornerRadius: 14)
.fill(AppColors.surface2(cs))
.frame(width: 52, height: 44)
.transition(.opacity.combined(with: .scale(scale: 0.9)))
RoundedRectangle(cornerRadius: 16)
.fill(Color(uiColor: cs == .dark ? .tertiarySystemFill : .systemGray5))
.frame(width: 62, height: 42)
.transition(.opacity.combined(with: .scale(scale: 0.88)))
}
if let img = entry.custom {
img
.font(.system(size: isCollapsed ? 17 : 20))
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text(cs))
.font(.system(size: 22))
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text3(cs))
} else if let icon = entry.sfIcon {
Image(systemName: icon)
.font(.system(size: isCollapsed ? 15 : 17, weight: selection == entry.id ? .semibold : .regular))
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text(cs))
.font(.system(size: 22, weight: selection == entry.id ? .semibold : .regular))
.foregroundColor(selection == entry.id ? AppColors.accent : AppColors.text3(cs))
}
}
.frame(maxWidth: .infinity).frame(height: isCollapsed ? 38 : 50)
.frame(maxWidth: .infinity)
.frame(height: 52)
}
.buttonStyle(PressButtonStyle(scale: 0.88))
.buttonStyle(PressButtonStyle(scale: 0.90))
}
}
.padding(.horizontal, 10)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 28)
.fill(AppColors.surface(cs))
.shadow(color: .black.opacity(isCollapsed ? 0.06 : 0.10), radius: 20, x: 0, y: 6)
.shadow(color: .black.opacity(0.04), radius: 4, x: 0, y: 1)
.fill(Color(uiColor: cs == .dark ? .secondarySystemBackground : .systemBackground))
.shadow(color: .black.opacity(cs == .dark ? 0.40 : 0.10), radius: 20, x: 0, y: 6)
)
.animation(KisaniSpring.snappy, value: isCollapsed)
.padding(.horizontal, 20)
.padding(.bottom, 10)
}
}