Files
phranck 5ad97132b8 Feat: Add @Observable support with Observation framework
- Replace custom Observable protocol and @Published with Apple's @Observable macro
- Add withObservationTracking in renderToBuffer for automatic per-property dependency tracking
- Add type-based @Environment(Type.self) and .environment(object) for observable objects
- Add ObjectEnvironmentModifier for injecting observable objects into the environment
- Add needsCacheClear flag to AppState for thread-safe cache invalidation
- Add cross-platform test script (scripts/test-linux.sh) for Docker-based Linux verification
- Add DemoAppHeader with system info display (OS, version, architecture)
- Consolidate Example App: extract ImageDemoHelpers, KeyboardHelpSection, ValueDisplayRow
- Add pre-push verification rule to CLAUDE.md
- Verified on both macOS and Linux (swift:6.0 container), 1155 tests passing
2026-02-15 23:49:34 +01:00

207 lines
7.1 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 {
DemoAppHeader("NavigationSplitView Demo")
}
}
}
// 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"
}
}
}