| index fac4088..3dbab40 100644 |
| --- a/native/apple/Sources/CalendarKit/CalendarWorkspace.swift |
| +++ b/native/apple/Sources/CalendarKit/CalendarWorkspace.swift |
| @@ -10,7 +10,8 @@ import SwiftUI |
| @State private var inspecting: CalendarEvent? |
| @State private var filter: Int64? = 0 |
| @State private var column: NavigationSplitViewColumn = .detail |
| - @Environment(\.openURL) private var openURL |
| + @Environment(\.dynamicTypeSize) private var typeSize |
| + private let accent = Color(red: 0.78, green: 0.22, blue: 0.29) |
| public init(client: NativeHTTP, model: CalendarModel? = nil, signOut: @escaping () -> Void) { self.client = client; self.signOut = signOut; _model = StateObject(wrappedValue: model ?? CalendarModel()) } |
| |
| public var body: some View { |
| @@ -30,9 +31,13 @@ import SwiftUI |
| } |
| }.navigationTitle("Calendar").navigationSplitViewColumnWidth(min: 220, ideal: 260, max: 340) |
| } detail: { |
| - VStack(spacing: 0) { |
| - monthHeader.padding() |
| - monthGrid.padding(.horizontal) |
| + List { |
| + Section { |
| + VStack(spacing: 0) { |
| + monthHeader |
| + monthGrid |
| + }.listRowSeparator(.hidden) |
| + } |
| if !model.notice.isEmpty { Label(model.notice, systemImage: "bell.badge").font(.callout).padding() } |
| if model.loading { ProgressView().padding(8) } |
| if !model.error.isEmpty { |
| @@ -40,7 +45,6 @@ import SwiftUI |
| description: Text(model.error)) |
| Button("Try again") { Task { await model.load(client) } }.padding() |
| } else { |
| - List { |
| Section(model.display(model.date, template: "EEEE d MMMM")) { |
| let events = model.dayEvents(model.date) |
| if events.isEmpty { |
| @@ -52,7 +56,7 @@ import SwiftUI |
| inspecting = event |
| } label: { |
| HStack(alignment: .top, spacing: 14) { |
| - RoundedRectangle(cornerRadius: 2).fill(.tint).frame(width: 4) |
| + RoundedRectangle(cornerRadius: 2).fill(calendarColor(event.calendar_id)).frame(width: 4) |
| VStack(alignment: .leading, spacing: 5) { |
| Text(event.title).font(.headline).foregroundStyle(.primary).strikethrough(event.status == "cancelled") |
| if event.status == "cancelled" { Label("Cancelled", systemImage: "xmark.circle").font(.caption).foregroundStyle(.secondary) } |
| @@ -67,10 +71,12 @@ import SwiftUI |
| }.buttonStyle(.plain) |
| } |
| } |
| - }.listStyle(.plain).refreshable { await model.load(client) } |
| } |
| - } |
| - .navigationTitle("Calendar") |
| + }.listStyle(.plain).refreshable { await model.load(client) } |
| + .navigationTitle(model.calendars.first(where: { $0.id == model.selectedCalendar })?.name ?? "Calendar") |
| + #if os(iOS) |
| + .navigationBarTitleDisplayMode(.inline) |
| + #endif |
| .toolbar { |
| ToolbarItem(placement: .primaryAction) { |
| Button("New event", systemImage: "plus") { editing = model.newEvent() } |
| @@ -92,8 +98,9 @@ import SwiftUI |
| if event.status == "tentative" { Section { Label("Tentative", systemImage: "questionmark.circle") } } |
| Section("When") { |
| LabeledContent("Starts", value: event.all_day ? event.day_start : model.display(Date(timeIntervalSince1970: Double(event.start)), template: event.all_day ? "d MMM yyyy" : "d MMM yyyy jm", zone: event.tz)) |
| - LabeledContent("Ends", value: event.all_day ? event.day_end : model.display(Date(timeIntervalSince1970: Double(event.end)), template: event.all_day ? "d MMM yyyy" : "d MMM yyyy jm", zone: event.tz)) |
| - LabeledContent("Time zone", value: event.tz) |
| + LabeledContent(event.all_day ? "Ends before" : "Ends", value: event.all_day ? event.day_end : model.display(Date(timeIntervalSince1970: Double(event.end)), template: event.all_day ? "d MMM yyyy" : "d MMM yyyy jm", zone: event.tz)) |
| + if event.all_day { Text("The end date is the first day after the event.").font(.callout).foregroundStyle(.secondary) } |
| + else { LabeledContent("Time zone", value: event.tz) } |
| if event.recurring { Label("Repeating event", systemImage: "repeat") } |
| } |
| if !event.location.isEmpty { Section("Location") { Text(event.location).textSelection(.enabled) } } |
| @@ -113,31 +120,64 @@ import SwiftUI |
| } |
| |
| private var monthHeader: some View { |
| + ViewThatFits(in: .horizontal) { |
| + HStack { |
| + Text(model.display(model.monthStart, template: "MMMM yyyy")).font(.title2.bold()) |
| + Spacer() |
| + monthControls |
| + } |
| + VStack(alignment: .leading) { |
| + Text(model.display(model.monthStart, template: "MMMM yyyy")).font(.title2.bold()) |
| + monthControls |
| + } |
| + }.padding(.vertical) |
| + } |
| + private var monthControls: some View { |
| HStack { |
| - Text(model.display(model.monthStart, template: "MMMM yyyy")).font(.title2.bold()) |
| - Spacer() |
| Button("Previous month", systemImage: "chevron.left") { move(-1) }.labelStyle(.iconOnly).frame(minWidth: 44, minHeight: 44) |
| - Button("Today") { model.date = Date() } |
| + Button("Today") { model.date = Date() }.frame(minHeight: 44) |
| Button("Next month", systemImage: "chevron.right") { move(1) }.labelStyle(.iconOnly).frame(minWidth: 44, minHeight: 44) |
| - }.buttonStyle(.borderless).controlSize(.large) |
| + }.buttonStyle(.borderless) |
| + } |
| + private func calendarColor(_ id: Int64) -> Color { |
| + guard let calendar = model.calendars.first(where: { $0.id == id }) else { return accent } |
| + return Color(hue: Double(calendar.hue % 360) / 360, saturation: 0.6, brightness: 0.75) |
| } |
| private func move(_ amount: Int) { model.date = model.calendar.date(byAdding: .month, value: amount, to: model.date) ?? model.date } |
| private var monthGrid: some View { |
| - LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 4), count: 7), spacing: 4) { |
| - ForEach(model.weekdayLabels, id: \.self) { Text($0).font(.caption).foregroundStyle(.secondary) } |
| - ForEach(model.days, id: \.self) { day in |
| - let selected = model.calendar.isDate(day, inSameDayAs: model.date) |
| - let current = model.calendar.isDate(day, equalTo: model.monthStart, toGranularity: .month) |
| - Button { model.date = day } label: { |
| - VStack(spacing: 3) { |
| - Text("\(model.calendar.component(.day, from: day))").font(.body.monospacedDigit()) |
| - Circle().fill(model.dayEvents(day).isEmpty ? .clear : selected ? .white : Color.accentColor).frame(width: 4, height: 4) |
| - }.frame(maxWidth: .infinity, minHeight: 44) |
| - .foregroundStyle(selected ? .white : current ? Color.primary : Color.secondary) |
| - .background(selected ? Color.accentColor : .clear, in: RoundedRectangle(cornerRadius: 9)) |
| - }.buttonStyle(.plain).accessibilityLabel(model.display(day, template: "EEEE d MMMM yyyy")) |
| - .accessibilityAddTraits(selected ? .isSelected : []) |
| + Grid(horizontalSpacing: 4, verticalSpacing: 4) { |
| + GridRow { |
| + ForEach(Array(model.weekdayLabels.enumerated()), id: \.offset) { _, label in |
| + Text(typeSize.isAccessibilitySize ? String(label.prefix(1)) : label) |
| + .font(.caption).foregroundStyle(.secondary).accessibilityHidden(true) |
| + } |
| + } |
| + ForEach(0..<6) { week in |
| + GridRow { |
| + ForEach(Array(model.days.dropFirst(week * 7).prefix(7)), id: \.self) { day in |
| + dayButton(day) |
| + } |
| + } |
| } |
| }.padding(.bottom, 12) |
| } |
| + private func dayButton(_ day: Date) -> some View { |
| + let selected = model.calendar.isDate(day, inSameDayAs: model.date) |
| + let today = model.calendar.isDate(day, inSameDayAs: Date()) |
| + let count = model.dayEvents(day).count |
| + let current = model.calendar.isDate(day, equalTo: model.monthStart, toGranularity: .month) |
| + return Button { model.date = day } label: { |
| + VStack(spacing: 3) { |
| + Text("\(model.calendar.component(.day, from: day))") |
| + .font(.body.monospacedDigit()).bold(today).lineLimit(1).minimumScaleFactor(0.7) |
| + Circle().fill(count == 0 ? .clear : selected ? .white : accent).frame(width: 4, height: 4) |
| + }.frame(maxWidth: .infinity, minHeight: 44) |
| + .foregroundStyle(selected ? .white : current ? Color.primary : Color.secondary) |
| + .background(selected ? accent : .clear, in: RoundedRectangle(cornerRadius: 9)) |
| + .overlay { RoundedRectangle(cornerRadius: 9).strokeBorder(today ? accent : .clear, lineWidth: 2) } |
| + }.buttonStyle(.plain) |
| + .accessibilityLabel(model.display(day, template: "EEEE d MMMM yyyy")) |
| + .accessibilityValue("\(today ? "Today. " : "")\(count) events") |
| + .accessibilityAddTraits(selected ? .isSelected : []) |
| + } |
| } |