import SwiftUI enum SheetView { case tab(TaskTab) case image(PlatformImage) case diagram(BoxLayout) } struct Sheet { enum Owner: Equatable { case lead case task(String?) } let owner: Owner let view: SheetView var tab: TaskTab? { if case .tab(let t) = view { return t } return nil } } private let stripWidth: CGFloat = 56 private let corner: CGFloat = 8 struct SheetHost: View { let sheet: Sheet @Environment(Store.self) private var store var body: some View { let overLeft = sheet.owner != .lead HStack(spacing: 0) { if overLeft { strip } SheetPanel(sheet: sheet, overLeft: overLeft) .onGeometryChange(for: CGRect.self) { g in g.frame(in: .global) } action: { frame in store.paneFocus.setSheetFrame(frame, lead: !overLeft) } .onDisappear { store.paneFocus.setSheetFrame(.zero, lead: !overLeft) } if !overLeft { strip } } } private var strip: some View { Palette.bg.opacity(0.55) .frame(width: stripWidth) .contentShape(Rectangle()) .onTapGesture { store.sheet = nil } .help("Close") } } private struct SheetPanel: View { let sheet: Sheet let overLeft: Bool @Environment(Store.self) private var store var body: some View { let task = store.selectedTaskId.flatMap { store.findTask($0) } let accent = overLeft ? task.map { taskPalette[$0.color] } ?? Palette.accent : Palette.accent let clip = UnevenRoundedRectangle(topLeadingRadius: overLeft ? corner : 0, bottomLeadingRadius: overLeft ? corner : 0, bottomTrailingRadius: overLeft ? 0 : corner, topTrailingRadius: overLeft ? 0 : corner) VStack(spacing: 0) { HStack(spacing: 8) { Text(title(task)) .font(Fonts.font(13, weight: 700)) .foregroundStyle(accent) .lineLimit(1) .truncationMode(.tail) Spacer(minLength: 0) if sheet.tab == .page, let url = store.currentPage()?.url(store) { Button { openExternal(url) } label: { Text("↗ Browser") .font(Fonts.font(13, weight: 700)) .foregroundStyle(accent) .padding(.horizontal, 6) .contentShape(Rectangle()) } .buttonStyle(.plain) .help("Open the page in the default browser") } Button { store.sheet = nil } label: { Text("✕") .font(Fonts.font(13, weight: 700)) .foregroundStyle(accent) .padding(.horizontal, 6) .contentShape(Rectangle()) } .buttonStyle(.plain) .help("Close (Esc)") } .padding(.horizontal, 12) .padding(.vertical, 7) .background(accent.opacity(0.12)) .overlay(alignment: .bottom) { Rectangle().fill(accent).frame(height: 1) } SheetContent(view: sheet.view) .frame(maxWidth: .infinity, maxHeight: .infinity) } .background(Palette.bg) .clipShape(clip) .overlay(SheetEdge(openTrailing: overLeft).stroke(accent, lineWidth: 1.5)) .shadow(color: Palette.overlayShadow, radius: 14, x: overLeft ? -6 : 6) .environment(\.paneAccent, accent) .environment(\.inTask, overLeft) } private func title(_ task: ByreTask?) -> String { let label = switch sheet.view { case .tab(let t): tabLabels[t]! case .image: "Image" case .diagram: "Diagram" } if !overLeft { return "Lead · \(label)" } let project = store.focusedProject()?.name ?? "" guard let task else { return "\(project) · \(label)" } return "\(project) ▸ \(task.name) · \(label)" } } struct SheetContent: View { let view: SheetView var body: some View { switch view { case .tab(let t): TaskTabBody(tab: t) case .image(let image): ScrollView([.horizontal, .vertical]) { Image(platformImage: image) .resizable() .scaledToFit() .frame(maxWidth: image.size.width, maxHeight: image.size.height) .padding(16) } .defaultScrollAnchor(.center) case .diagram(let layout): DiagramViewer(layout: layout) .id(layout.id) .padding(12) } } } private struct SheetEdge: Shape { let openTrailing: Bool func path(in r: CGRect) -> Path { let closed = openTrailing ? r.minX : r.maxX let open = openTrailing ? r.maxX : r.minX let inward: CGFloat = openTrailing ? corner : -corner var p = Path() p.move(to: CGPoint(x: open, y: r.minY)) p.addLine(to: CGPoint(x: closed + inward, y: r.minY)) p.addArc(tangent1End: CGPoint(x: closed, y: r.minY), tangent2End: CGPoint(x: closed, y: r.minY + corner), radius: corner) p.addLine(to: CGPoint(x: closed, y: r.maxY - corner)) p.addArc(tangent1End: CGPoint(x: closed, y: r.maxY), tangent2End: CGPoint(x: closed + inward, y: r.maxY), radius: corner) p.addLine(to: CGPoint(x: open, y: r.maxY)) return p } }