// SPDX-FileCopyrightText: 2026 Kaede Akino // SPDX-License-Identifier: GPL-3.0-only import SwiftUI import TermindPresentation extension View { func termindTransientOverlaySurface() -> some View { termindGlassSurface(cornerRadius: Metrics.Radius.large) .overlay { RoundedRectangle(cornerRadius: Metrics.Radius.large) .strokeBorder(.separator.opacity(0.6), lineWidth: 1) } .shadow(color: TermindColor.shadow.opacity(0.16), radius: 12, y: 5) .allowsHitTesting(false) } } enum TermindSearchFieldDensity { case compact, regular, prominent } struct TermindSearchField: View { @Binding var text: String let prompt: LocalizedStringResource var density: TermindSearchFieldDensity = .regular var externalFocus: FocusState.Binding? @FocusState private var localFocus: Bool var body: some View { fieldContent .termindGlassSurface( cornerRadius: density == .prominent ? Metrics.Radius.large : Metrics.Radius.medium, isInteractive: true ) } private var fieldContent: some View { HStack(spacing: Metrics.Space.s) { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) TextField(prompt, text: $text.singleLineText, axis: .horizontal).singleLinePaste().lineLimit(1) .textFieldStyle(.plain) .font(density == .prominent ? .title3 : .body) .focused(externalFocus ?? $localFocus) if !text.isEmpty { Button { text = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.secondary) } .buttonStyle(.plain) .accessibilityLabel(Text(AppStrings.Common.clearSearch)) } } .padding(.horizontal, horizontalPadding) .padding(.vertical, verticalPadding) } private var horizontalPadding: CGFloat { density == .prominent ? Metrics.Space.l : Metrics.Space.s } private var verticalPadding: CGFloat { switch density { case .compact: Metrics.Space.xs case .regular: Metrics.Space.s case .prominent: Metrics.Space.m } } } struct SheetScaffold: View { @ViewBuilder let header: Header @ViewBuilder let content: Content @ViewBuilder let footer: Footer var body: some View { VStack(spacing: 0) { header .frame(maxWidth: .infinity) .termindGlassBar() Divider().opacity(0.7) content .frame(maxWidth: .infinity, maxHeight: .infinity) .background(TermindColor.windowSurface) Divider().opacity(0.7) footer .termindGlassBar() } } } /// The stable chrome for work performed in Vault's third column. It deliberately /// resembles the manager toolbars rather than a floating sheet: a clear close /// affordance, a quiet title, scrollable work, and actions anchored at the foot. struct VaultDetailScaffold: View { let title: LocalizedStringResource let symbol: String let close: () -> Void var closeLabel: LocalizedStringResource = AppStrings.Common.close var closeSymbol = "xmark" var showsHeaderClose = true @ViewBuilder let content: Content @ViewBuilder let footer: Footer var body: some View { VStack(spacing: 0) { HStack(spacing: Metrics.Space.m) { if showsHeaderClose { Button(action: close) { Label(closeLabel, systemImage: closeSymbol) .labelStyle(.iconOnly) } .buttonStyle(.borderless) .keyboardShortcut(.cancelAction) .help(closeLabel) } Label(title, systemImage: symbol) .font(.headline) .lineLimit(1) .truncationMode(.tail) .help(title) Spacer(minLength: 0) } .padding(.horizontal, Metrics.Space.l) .padding(.vertical, Metrics.Space.m) .termindGlassBar() Divider().opacity(0.25) content .frame(maxWidth: .infinity, maxHeight: .infinity) .background(TermindColor.windowSurface) Divider().opacity(0.25) footer .termindGlassBar() } } } struct ManagedChooserHeader: View { let title: LocalizedStringResource let symbol: String let prompt: LocalizedStringResource @Binding var query: String var onSubmit: () -> Void = {} var moveCursor: (Int) -> KeyPress.Result = { _ in .ignored } var toggleCursor: () -> KeyPress.Result = { .ignored } @FocusState private var searchFocused: Bool var body: some View { VStack(alignment: .leading, spacing: Metrics.Space.m) { Label(title, systemImage: symbol) .font(.headline) .labelStyle(.titleOnly) searchRow } .padding(Metrics.Space.xl) .onAppear { searchFocused = true } } private var searchRow: some View { HStack(spacing: Metrics.Space.s) { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) TextField(prompt, text: $query.singleLineText, axis: .horizontal).singleLinePaste().lineLimit(1) .textFieldStyle(.plain) .focused($searchFocused) .onSubmit(onSubmit) .onKeyPress(.upArrow) { moveCursor(-1) } .onKeyPress(.downArrow) { moveCursor(1) } .onKeyPress(.space, action: toggleCursor) if !query.isEmpty { Button { query = "" searchFocused = true } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.secondary) } .buttonStyle(.plain) .accessibilityLabel(Text(AppStrings.Common.clearSearch)) } } .padding(Metrics.Space.s) .background( .quaternary.opacity(0.5), in: .rect(cornerRadius: Metrics.Radius.medium) ) } } struct SelectableRow: View { let isCursor: Bool let isSelected: Bool var isEnabled = true @ViewBuilder let content: Content @ViewBuilder let trailing: Trailing @State private var isHovering = false var body: some View { HStack(spacing: Metrics.Space.m) { content Spacer(minLength: Metrics.Space.s) trailing } .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, Metrics.Space.m) .padding(.vertical, Metrics.Space.s) .background(fill, in: .rect(cornerRadius: Metrics.Radius.medium)) .overlay(alignment: .leading) { if isCursor { Capsule() .fill(Color.accentColor) .frame(width: 2.5) .padding(.vertical, Metrics.Space.xs) } } .contentShape(.rect(cornerRadius: Metrics.Radius.medium)) .onHover { isHovering = $0 && isEnabled } .opacity(isEnabled ? 1 : 0.55) .accessibilityAddTraits(isSelected ? [.isSelected, .isButton] : .isButton) } private var fill: Color { if isCursor { return Color.accentColor.opacity(0.11) } if isSelected { return Color.primary.opacity(0.075) } if isHovering { return Color.primary.opacity(0.045) } return .clear } }