Files
TUIkit/Sources/TUIkitExample/Pages/OverlaysPage.swift
T
phranck df4ee3253b Feat: AppHeader — framework-managed header bar rendered outside the view tree
AppHeader is rendered at the top of the terminal by RenderLoop, similar to
StatusBar at the bottom. Views declare header content via .appHeader { }
ViewBuilder modifier. Supports standard (thin divider) and block (half-block
with appHeaderBackground) appearance. Hidden when no content is set.
Diff cache invalidates on header height changes to prevent ghosting.
2026-02-03 21:21:41 +01:00

314 lines
11 KiB
Swift

// 🖥️ TUIKit — Terminal UI Kit for Swift
// OverlaysPage.swift
//
// Created by LAYERED.work
// CC BY-NC-SA 4.0
import TUIkit
// MARK: - Overlay Demo Variants
/// Available overlay demo variants.
private enum OverlayDemo: Int, CaseIterable {
case alertStandard
case alertWarning
case alertError
case alertInfo
case alertSuccess
case dialog
case dialogWithFooter
case modalCustom
/// Display label for the menu.
var label: String {
switch self {
case .alertStandard: "Alert (Standard)"
case .alertWarning: "Alert (Warning)"
case .alertError: "Alert (Error)"
case .alertInfo: "Alert (Info)"
case .alertSuccess: "Alert (Success)"
case .dialog: "Dialog"
case .dialogWithFooter: "Dialog with Footer"
case .modalCustom: "Modal (Custom)"
}
}
/// Description text for the detail panel.
var description: String {
switch self {
case .alertStandard:
"A standard alert with default theme colors. Uses .alert(isPresented:) modifier."
case .alertWarning:
"A warning-style alert with palette warning colors. Uses Alert.warning() preset."
case .alertError:
"An error-style alert with palette error colors. Uses Alert.error() preset."
case .alertInfo:
"An info-style alert with palette info colors. Uses Alert.info() preset."
case .alertSuccess:
"A success-style alert with palette success colors. Uses Alert.success() preset."
case .dialog:
"A Dialog view with custom content. More flexible than Alert — accepts any views."
case .dialogWithFooter:
"A Dialog with a footer section for action buttons, separated by a divider line."
case .modalCustom:
"A custom modal overlay using .modal(isPresented:). Accepts any view as content."
}
}
/// API usage example for the detail panel.
var apiUsage: String {
switch self {
case .alertStandard:
".alert(\"Title\", isPresented: $show) { actions } message: { Text(\"...\") }"
case .alertWarning:
".modal(isPresented: $show) { Alert.warning(message: \"...\") { actions } }"
case .alertError:
".modal(isPresented: $show) { Alert.error(message: \"...\") { actions } }"
case .alertInfo:
".modal(isPresented: $show) { Alert.info(message: \"...\") { actions } }"
case .alertSuccess:
".modal(isPresented: $show) { Alert.success(message: \"...\") { actions } }"
case .dialog:
".modal(isPresented: $show) { Dialog(title: \"...\") { content } }"
case .dialogWithFooter:
".modal(isPresented: $show) { Dialog(title: \"...\") { content } footer: { buttons } }"
case .modalCustom:
".modal(isPresented: $show) { VStack { ... } }"
}
}
}
// MARK: - Overlays Page
/// Interactive overlays and modals demo page.
///
/// Displays a menu of overlay variants on the left and a description
/// panel on the right. Pressing Enter shows the selected overlay
/// with dimmed background content.
struct OverlaysPage: View {
@State var menuSelection: Int = 0
@State var showOverlay: Bool = false
/// Callback to navigate back to the main menu.
let onBack: () -> Void
/// The currently selected demo variant.
private var selectedDemo: OverlayDemo {
OverlayDemo.allCases[menuSelection]
}
var body: some View {
backgroundContent
.modal(isPresented: $showOverlay) {
overlayContent(for: selectedDemo)
}
.statusBarItems(statusBarItems)
}
/// Status bar items change depending on whether a modal is open.
/// When a modal is presented, ESC closes the modal instead of navigating back.
private var statusBarItems: [any StatusBarItemProtocol] {
if showOverlay {
return [
StatusBarItem(shortcut: Shortcut.escape, label: "close") {
showOverlay = false
},
StatusBarItem(shortcut: Shortcut.enter, label: "dismiss"),
]
} else {
return [
StatusBarItem(shortcut: Shortcut.escape, label: "back") {
onBack()
},
StatusBarItem(shortcut: Shortcut.arrowsUpDown, label: "nav"),
StatusBarItem(shortcut: Shortcut.enter, label: "show"),
]
}
}
// MARK: - Background Content
/// The main background content with menu and description.
private var backgroundContent: some View {
VStack(spacing: 1) {
HStack(spacing: 3) {
// Left: Demo menu
Menu(
title: "Select a Demo",
items: OverlayDemo.allCases.map { demo in
MenuItem(label: demo.label, shortcut: nil)
},
selection: $menuSelection,
onSelect: { _ in
showOverlay = true
},
selectedColor: .palette.accent,
borderColor: .palette.border
)
// Right: Description of selected demo
descriptionPanel
}
DemoSection("How It Works") {
Text("All overlays use the SwiftUI-style presentation API:")
.foregroundColor(.palette.foregroundSecondary)
Text(" .alert(isPresented:) — for Alert views")
.foregroundColor(.palette.foregroundSecondary)
Text(" .modal(isPresented:) — for Dialog, custom content")
.foregroundColor(.palette.foregroundSecondary)
Text("The background is automatically dimmed. Status bar stays visible.")
.bold()
.foregroundColor(.palette.accent)
}
Spacer()
}
.appHeader {
HStack {
Text("Overlays & Modals Demo").bold().foregroundColor(.palette.accent)
Spacer()
Text("TUIkit v\(tuiKitVersion)").foregroundColor(.palette.foregroundTertiary)
}
}
}
// MARK: - Description Panel
/// Detail panel showing the selected demo's description and API usage.
private var descriptionPanel: some View {
Panel(selectedDemo.label, titleColor: .palette.accent) {
VStack(alignment: .leading, spacing: 1) {
Text(selectedDemo.description)
.foregroundColor(.palette.foreground)
Text("")
Text("API:")
.bold()
.foregroundColor(.palette.accent)
Text(" \(selectedDemo.apiUsage)")
.foregroundColor(.palette.foregroundSecondary)
}
}
.frame(width: 55)
}
// MARK: - Overlay Content
/// Builds the overlay content for the selected demo variant.
@ViewBuilder
private func overlayContent(for demo: OverlayDemo) -> some View {
switch demo {
case .alertStandard:
Alert(
title: "Standard Alert",
message: "This is a standard alert with default theme colors.",
borderColor: .palette.border,
titleColor: .palette.accent
) {
dismissButton
}
.frame(width: 50)
case .alertWarning:
Alert(
title: "Warning",
message: "Something might go wrong. Please check your input.",
titleColor: .palette.warning
) {
dismissButton
}
.frame(width: 50)
case .alertError:
Alert(
title: "Error",
message: "An unexpected error occurred. Please try again.",
titleColor: .palette.error
) {
dismissButton
}
.frame(width: 50)
case .alertInfo:
Alert(
title: "Info",
message: "This is an informational message for the user.",
titleColor: .palette.info
) {
dismissButton
}
.frame(width: 50)
case .alertSuccess:
Alert(
title: "Success",
message: "Operation completed successfully!",
titleColor: .palette.success
) {
dismissButton
}
.frame(width: 50)
case .dialog:
Dialog(title: "Settings", borderColor: .palette.border, titleColor: .palette.accent) {
VStack(alignment: .leading) {
Text("Theme: Dark")
.foregroundColor(.palette.foreground)
Text("Language: English")
.foregroundColor(.palette.foreground)
Text("Notifications: On")
.foregroundColor(.palette.foreground)
Text("")
dismissButton
}
}
.frame(width: 50)
case .dialogWithFooter:
Dialog(
title: "Confirm Action",
borderColor: .palette.border,
titleColor: .palette.accent
) {
Text("Are you sure you want to proceed?")
.foregroundColor(.palette.foreground)
Text("This action cannot be undone.")
.foregroundColor(.palette.foregroundSecondary)
} footer: {
dismissButton
}
.frame(width: 50)
case .modalCustom:
VStack(spacing: 1) {
Text("Custom Modal Content")
.bold()
.foregroundColor(.palette.accent)
Text("")
Text("This modal uses .modal(isPresented:)")
.foregroundColor(.palette.foreground)
Text("with completely custom view content.")
.foregroundColor(.palette.foregroundSecondary)
Text("No Alert or Dialog — just any View!")
.foregroundColor(.palette.foregroundSecondary)
Text("")
dismissButton
}
.padding(EdgeInsets(horizontal: 2, vertical: 1))
.border(color: .palette.border)
}
}
/// Reusable right-aligned dismiss button for all overlay variants.
private var dismissButton: some View {
HStack {
Spacer()
Button("Dismiss", style: .primary) {
showOverlay = false
}
}
}
}