Files
TUIkit/Sources/TUIkitExample/Pages/SplitViewPage.swift
T
phranck cd2968e345 Feat: Redesign control styling and add foregroundQuaternary palette color
- Redesign Button rendering with half-block caps (U+2590/U+258C) and accent-tinted background
- Apply matching cap style and background to TextField and SecureField
- Add foregroundQuaternary to Palette for subtle UI elements like spinner tracks
- Fix border saturation in all palette presets to preserve hue instead of going gray
- Rework Spinner bouncing trail to interpolate from highlight to quaternary via Color.lerp
- Fix Spinner label to use palette foreground color
- Fix FrameDiffWriter to use ESC[2K for reliable line clearing
- Add Layoutable conformance to NavigationSplitView for height-flexible layout
- Update tests for new button cap style
2026-02-13 16:18:15 +01:00

211 lines
7.3 KiB
Swift

// TUIKit - Terminal UI Kit for Swift
// SplitViewPage.swift
//
// Created by LAYERED.work
// License: MIT
import TUIkit
// MARK: - Demo Data
/// A mail folder for the sidebar.
private struct Folder: Identifiable {
let id: String
let name: String
let icon: String
let unreadCount: Int
static let samples: [Self] = [
Self(id: "inbox", name: "Inbox", icon: "[>]", unreadCount: 12),
Self(id: "starred", name: "Starred", icon: "[*]", unreadCount: 3),
Self(id: "sent", name: "Sent", icon: "[^]", unreadCount: 0),
Self(id: "drafts", name: "Drafts", icon: "[~]", unreadCount: 2),
Self(id: "archive", name: "Archive", icon: "[=]", unreadCount: 0),
Self(id: "trash", name: "Trash", icon: "[x]", unreadCount: 0),
]
}
/// A mail message for the content list.
private struct Message: Identifiable {
let id: String
let from: String
let subject: String
let body: String
let date: String
let isRead: Bool
static func samples(for folder: String) -> [Self] {
switch folder {
case "inbox":
return [
Self(id: "1", from: "Alice", subject: "Meeting Tomorrow",
body: "Hi,\n\nJust wanted to confirm our meeting tomorrow at 2pm.\n\nBest,\nAlice",
date: "10:30", isRead: false),
Self(id: "2", from: "Bob", subject: "Code Review",
body: "Hey,\n\nI've reviewed your PR and left some comments.\n\nLooks good overall!",
date: "09:15", isRead: false),
Self(id: "3", from: "Carol", subject: "Project Update",
body: "Team,\n\nHere's the latest status on the project.\n\nWe're on track for launch.",
date: "Yesterday", isRead: true),
Self(id: "4", from: "David", subject: "Quick Question",
body: "Hi,\n\nDo you have a moment to discuss the API design?\n\nThanks!",
date: "Yesterday", isRead: true),
Self(id: "5", from: "Eve", subject: "New Feature Idea",
body: "Hello,\n\nI was thinking we could add dark mode support.\n\nThoughts?",
date: "Monday", isRead: true),
]
case "starred":
return [
Self(id: "s1", from: "Frank", subject: "Important: Deadline",
body: "Reminder:\n\nThe deadline is next Friday.\n\nPlease submit your work.",
date: "Tuesday", isRead: true),
Self(id: "s2", from: "Grace", subject: "Contract Review",
body: "Hi,\n\nPlease review the attached contract.\n\nLet me know if you have questions.",
date: "Last week", isRead: true),
]
case "drafts":
return [
Self(id: "d1", from: "Me", subject: "Re: Meeting",
body: "Thanks for the invite.\n\nI'll be there at 2pm.\n\nSee you then!",
date: "Draft", isRead: true),
]
default:
return []
}
}
}
// MARK: - SplitView Page
/// NavigationSplitView demo page.
///
/// Shows a three-column mail client layout with interactive Lists:
/// - Sidebar: Folder list (Tab to focus)
/// - Content: Message list for selected folder
/// - Detail: Full message content
struct SplitViewPage: View {
@State private var selectedFolder: String? = "inbox"
@State private var selectedMessage: String? = "1"
@State private var visibility: NavigationSplitViewVisibility = .all
var body: some View {
VStack(spacing: 0) {
NavigationSplitView(columnVisibility: $visibility) {
// Sidebar: Folder list
List("Folders", selection: $selectedFolder) {
ForEach(Folder.samples) { folder in
HStack(spacing: 1) {
Text(folder.icon)
Text(folder.name)
}
.badge(folder.unreadCount)
}
}
} content: {
// Content: Message list
messageListContent
} detail: {
// Detail: Message content
detailColumn
}
.navigationSplitViewStyle(.balanced)
}
.appHeader {
HStack {
Text("NavigationSplitView Demo").bold().foregroundStyle(.palette.accent)
Spacer()
Text("TUIkit v\(tuiKitVersion)").foregroundStyle(.palette.foregroundTertiary)
}
}
}
}
// MARK: - Column Views
private extension SplitViewPage {
@ViewBuilder
var messageListContent: some View {
let messages = Message.samples(for: selectedFolder ?? "inbox")
if messages.isEmpty {
VStack {
Spacer()
Text("No messages in this folder").dim()
Spacer()
}
} else {
List(folderTitle, selection: $selectedMessage) {
ForEach(messages) { message in
HStack(spacing: 1) {
if message.isRead {
Text(" ")
} else {
Text("*").foregroundStyle(.palette.accent)
}
if message.isRead {
Text(message.from)
} else {
Text(message.from).bold()
}
Text("-").dim()
Text(message.subject)
}
}
}
}
}
var detailColumn: some View {
VStack(alignment: .leading, spacing: 1) {
if let message = currentMessage {
// Header
Text(message.subject).bold().foregroundStyle(.palette.accent)
Spacer(minLength: 1)
HStack(spacing: 1) {
Text("From:").foregroundStyle(.palette.foregroundSecondary)
Text(message.from)
}
HStack(spacing: 1) {
Text("Date:").foregroundStyle(.palette.foregroundSecondary)
Text(message.date)
}
Spacer(minLength: 1)
// Message body
Text(message.body)
Spacer()
} else {
Spacer()
HStack {
Spacer()
Text("Select a message to view").dim()
Spacer()
}
Spacer()
}
}
.padding(.horizontal, 1)
}
}
// MARK: - Private Helpers
private extension SplitViewPage {
var folderTitle: String {
Folder.samples.first { $0.id == selectedFolder }?.name ?? "Messages"
}
var currentMessage: Message? {
guard let messageId = selectedMessage else { return nil }
return Message.samples(for: selectedFolder ?? "inbox").first { $0.id == messageId }
}
var visibilityLabel: String {
switch visibility {
case .all: return "All"
case .doubleColumn: return "Double"
case .detailOnly: return "Detail"
default: return "Auto"
}
}
}