native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

Small UI improvements: detail toolbar, loading overlay, empty states (#258)

* Align empty-state layouts with detail placeholder

* Normalize sidebar icon sizing and repo add button

* Redesign worktree loading overlay

Match the detail-placeholder layout (large spinner, title, caption,
5-line animated tail) and replace the flat `state: .creating` /
`statusLines:` bag with a `WorktreeLoadingInfo.Kind` enum carrying
a `Progress` payload. Dead `.archiving` case dropped since archiving
routes via the terminal.

* Redesign detail title with avatar and folder support

Replace the plain branch label with a Label-based button carrying the
GitHub owner avatar (same layout used in Settings), a rename popover
for git worktrees, and a disabled folder-glyph label for folder
repositories. `WorktreeToolbarState` now carries a `Kind` enum so
"folder with a pull request" is unrepresentable.

* Inline notification bell with status toolbar item

* Hide window-toolbar background and center placeholder text

authored by

Stefano Bertagno and committed by
GitHub
3af3a164 c4e9be3b

+292 -162
+31 -5
supacode/Domain/WorktreeLoadingInfo.swift
··· 1 1 struct WorktreeLoadingInfo: Hashable { 2 2 let name: String 3 3 let repositoryName: String? 4 - let state: WorktreeLoadingState 5 - let statusTitle: String? 6 - let statusDetail: String? 7 - let statusCommand: String? 8 - let statusLines: [String] 4 + let kind: Kind 5 + 6 + // `.creating` is git-only — folder creation is rejected upstream 7 + // in the reducer. Archiving never surfaces here: it routes through 8 + // the terminal, so no `.archiving` case is needed. 9 + enum Kind: Hashable { 10 + case creating(Progress) 11 + case removing(isFolder: Bool) 12 + } 13 + 14 + struct Progress: Hashable { 15 + let statusTitle: String? 16 + let statusDetail: String? 17 + let statusCommand: String? 18 + let statusLines: [String] 19 + } 20 + 21 + var isFolder: Bool { 22 + kind == .removing(isFolder: true) 23 + } 24 + 25 + var progress: Progress? { 26 + if case .creating(let progress) = kind { progress } else { nil } 27 + } 28 + 29 + var actionLabel: String { 30 + switch kind { 31 + case .creating: "Creating" 32 + case .removing: "Removing" 33 + } 34 + } 9 35 }
-5
supacode/Domain/WorktreeLoadingState.swift
··· 1 - enum WorktreeLoadingState { 2 - case creating 3 - case archiving 4 - case removing 5 - }
+17 -12
supacode/Features/Repositories/Views/EmptyStateView.swift
··· 9 9 10 10 var body: some View { 11 11 let openRepo = AppShortcuts.openRepository.effective(from: settingsFile.global.shortcutOverrides) 12 - VStack { 12 + 13 + VStack(spacing: 12) { 13 14 Image(systemName: "tray") 14 - .font(.title2) 15 + .font(.title) 16 + .imageScale(.large) 15 17 .accessibilityHidden(true) 16 - Text("Open a repository or folder") 17 - .font(.headline) 18 - Text( 19 - "Press \(openRepo?.display ?? AppShortcuts.openRepository.display) " 20 - + "or click Open Repository or Folder to choose one." 21 - ) 22 - .font(.subheadline) 23 - .foregroundStyle(.secondary) 18 + .foregroundStyle(.secondary) 19 + VStack(spacing: 4) { 20 + Text("Open a repository or folder") 21 + .font(.title3) 22 + Text( 23 + "Press \(openRepo?.display ?? AppShortcuts.openRepository.display) " 24 + + "or click Open Repository or Folder to choose one." 25 + ) 26 + .font(.subheadline) 27 + .foregroundStyle(.secondary) 28 + } 24 29 Button("Open Repository or Folder...") { 25 30 store.send(.setOpenPanelPresented(true)) 26 31 } 27 32 .appKeyboardShortcut(openRepo) 28 33 .help("Open Repository or Folder (\(openRepo?.display ?? "none"))") 29 34 } 30 - .frame(maxWidth: .infinity, maxHeight: .infinity) 31 - .background(Color(nsColor: .windowBackgroundColor)) 32 35 .multilineTextAlignment(.center) 36 + .background(Color(nsColor: .windowBackgroundColor)) 37 + .frame(maxWidth: .infinity, maxHeight: .infinity) 33 38 } 34 39 }
+6
supacode/Features/Repositories/Views/SidebarItemView.swift
··· 300 300 Group { 301 301 if isSystemImage { 302 302 Image(systemName: resolvedName) 303 + .resizable() 304 + .aspectRatio(contentMode: .fit) 303 305 .fontWeight(.semibold) 304 306 } else { 305 307 Image(resolvedName) 306 308 .renderingMode(.template) 309 + .resizable() 310 + .aspectRatio(contentMode: .fit) 307 311 } 308 312 } 309 313 .foregroundStyle(resolvedColor) 314 + .frame(width: 16, height: 16) 310 315 .overlay(alignment: .bottomTrailing) { 311 316 if let checkBadgeState, !isSystemImage { 312 317 let badgeColor = AnyShapeStyle(checkBadgeState.color) 313 318 let background = AnyShapeStyle(.windowBackground) 314 319 Image(systemName: checkBadgeState.symbolName) 315 320 .resizable() 321 + .aspectRatio(contentMode: .fit) 316 322 .symbolVariant(.circle.fill) 317 323 .symbolRenderingMode(.palette) 318 324 .fontWeight(.black)
+8 -3
supacode/Features/Repositories/Views/SidebarView.swift
··· 26 26 Button { 27 27 store.send(.setOpenPanelPresented(true)) 28 28 } label: { 29 - Image(systemName: "folder.badge.plus") 30 - .offset(y: -1) 31 - .accessibilityLabel("Add Repository or Folder") 29 + Label { 30 + Text("Add…") 31 + } icon: { 32 + Image(systemName: "folder.badge.plus") 33 + .offset(y: -1) 34 + .accessibilityHidden(true) 35 + } 32 36 } 37 + .labelStyle(.iconOnly) 33 38 .help("Add Repository or Folder (\(openRepo?.display ?? "none"))") 34 39 } 35 40 }
+90 -21
supacode/Features/Repositories/Views/WorktreeDetailTitleView.swift
··· 1 + import Kingfisher 1 2 import SwiftUI 2 3 4 + /// Detail toolbar title: rename-popover button for git worktrees; 5 + /// static folder label for non-git folder repositories. 3 6 struct WorktreeDetailTitleView: View { 4 - let branchName: String 5 - let onSubmit: (String) -> Void 7 + let title: String 8 + let rootURL: URL 9 + let isFolder: Bool 10 + let onRenameBranch: (String) -> Void 6 11 7 12 @State private var isPresented = false 8 - @State private var isHovered = false 9 13 @State private var draftName = "" 10 14 11 15 var body: some View { 12 16 Button { 13 - draftName = branchName 17 + draftName = title 14 18 isPresented = true 15 19 } label: { 16 - HStack(spacing: 6) { 17 - Image(systemName: "arrow.trianglehead.branch") 18 - .foregroundStyle(.secondary) 19 - .accessibilityHidden(true) 20 - Text(branchName) 21 - if isHovered { 22 - Image(systemName: "pencil") 23 - .foregroundStyle(.secondary) 20 + Label { 21 + Text(title) 22 + } icon: { 23 + if isFolder { 24 + Image(systemName: "folder") 24 25 .accessibilityHidden(true) 26 + } else { 27 + RepositoryOwnerAvatar(rootURL: rootURL) 25 28 } 26 29 } 27 - .font(.headline) 30 + .labelStyle(.titleAndIcon) 28 31 } 29 - .help("Rename branch (⌘M)") 30 - .keyboardShortcut("m", modifiers: .command) 31 - .onHover { hovering in 32 - isHovered = hovering 33 - } 32 + .help("Rename \(isFolder ? "folder" : "branch")") 33 + .disabled(isFolder) 34 34 .popover(isPresented: $isPresented) { 35 35 RenameBranchPopover( 36 36 draftName: $draftName, 37 37 onCancel: { isPresented = false }, 38 38 onSubmit: { newName in 39 39 isPresented = false 40 - if newName != branchName { 41 - onSubmit(newName) 42 - } 40 + guard newName != title else { return } 41 + onRenameBranch(newName) 43 42 } 44 43 ) 45 44 } 46 45 } 47 46 } 48 47 48 + /// Falls back to a branch glyph while loading or when the remote 49 + /// doesn't resolve to a GitHub owner. 50 + private struct RepositoryOwnerAvatar: View { 51 + let rootURL: URL 52 + @State private var avatarURL: URL? 53 + 54 + var body: some View { 55 + KFImage(avatarURL) 56 + .placeholder { 57 + Image(systemName: "arrow.trianglehead.branch") 58 + .accessibilityHidden(true) 59 + } 60 + .resizable() 61 + .aspectRatio(1, contentMode: .fit) 62 + .frame(width: 20, height: 20) 63 + .clipShape(RoundedRectangle(cornerRadius: 8)) 64 + .task(id: rootURL) { avatarURL = await Self.ownerAvatarURL(for: rootURL) } 65 + } 66 + 67 + private static func ownerAvatarURL(for rootURL: URL) async -> URL? { 68 + guard let info = await GitClient().remoteInfo(for: rootURL) else { 69 + return nil 70 + } 71 + // 64 px covers retina rendering of the 20 pt icon frame. 72 + return URL(string: "https://github.com/\(info.owner).png?size=64") 73 + } 74 + } 75 + 49 76 private struct RenameBranchPopover: View { 50 77 @Binding var draftName: String 51 78 let onCancel: () -> Void ··· 89 116 onSubmit(trimmed) 90 117 } 91 118 } 119 + 120 + #Preview("supabitapp/supacode") { 121 + // Walk up from this source file to the repo root so the live preview 122 + // resolves the real supabitapp/supacode origin. 123 + let supacodeRepoRoot: URL = URL(fileURLWithPath: #filePath) 124 + .deletingLastPathComponent() // Views 125 + .deletingLastPathComponent() // Repositories 126 + .deletingLastPathComponent() // Features 127 + .deletingLastPathComponent() // supacode 128 + .deletingLastPathComponent() // repo root 129 + 130 + Text("").toolbar { 131 + WorktreeDetailTitleView( 132 + title: "sbertix/small-ui-improvements", 133 + rootURL: supacodeRepoRoot, 134 + isFolder: false, 135 + onRenameBranch: { _ in } 136 + ) 137 + }.frame(width: 600, height: 600) 138 + } 139 + 140 + #Preview("Folder") { 141 + Text("").toolbar { 142 + WorktreeDetailTitleView( 143 + title: "Documents", 144 + rootURL: URL(fileURLWithPath: "/Users/stefanobertagno/Documents"), 145 + isFolder: true, 146 + onRenameBranch: { _ in } 147 + ) 148 + }.frame(width: 600, height: 600) 149 + } 150 + 151 + #Preview("Missing repo") { 152 + Text("").toolbar { 153 + WorktreeDetailTitleView( 154 + title: "ghost-branch", 155 + rootURL: URL(fileURLWithPath: "/tmp/supacode-preview-no-such-repo"), 156 + isFolder: false, 157 + onRenameBranch: { _ in } 158 + ) 159 + }.frame(width: 600, height: 600) 160 + }
+56 -31
supacode/Features/Repositories/Views/WorktreeDetailView.swift
··· 56 56 if showsToolbarPlaceholder { 57 57 ToolbarPlaceholderContent() 58 58 } else if hasActiveWorktree, let selectedWorktree { 59 - let pullRequest = repositories.worktreeInfo(for: selectedWorktree.id)?.pullRequest 60 - let matchesBranch = 61 - if let pullRequest { 62 - pullRequest.headRefName == nil || pullRequest.headRefName == selectedWorktree.name 63 - } else { 64 - false 65 - } 66 59 let toolbarState = WorktreeToolbarState( 67 - branchName: selectedWorktree.name, 60 + title: selectedWorktree.name, 61 + rootURL: selectedWorktree.repositoryRootURL, 62 + kind: toolbarKind(for: selectedWorktree, repositories: repositories), 68 63 statusToast: repositories.statusToast, 69 - pullRequest: matchesBranch ? pullRequest : nil, 70 64 notificationGroups: notificationGroups, 71 65 unseenNotificationWorktreeCount: unseenNotificationWorktreeCount, 72 66 openActionSelection: openActionSelection, ··· 101 95 ) 102 96 } 103 97 } 98 + .toolbarBackgroundVisibility(.hidden, for: .windowToolbar) 104 99 let hasRunningRunScript = state.hasRunningRunScript 105 100 let actions = makeFocusedActions( 106 101 hasActiveWorktree: hasActiveWorktree, ··· 290 285 } 291 286 292 287 fileprivate struct WorktreeToolbarState { 293 - let branchName: String 288 + // Folders have no git remote, so the PR payload is scoped to 289 + // `.git` — this makes "folder with a pull request" unrepresentable. 290 + enum Kind { 291 + case git(pullRequest: GithubPullRequest?) 292 + case folder 293 + } 294 + 295 + let title: String 296 + let rootURL: URL 297 + let kind: Kind 294 298 let statusToast: RepositoriesFeature.StatusToast? 295 - let pullRequest: GithubPullRequest? 296 299 let notificationGroups: [ToolbarNotificationRepositoryGroup] 297 300 let unseenNotificationWorktreeCount: Int 298 301 let openActionSelection: OpenWorktreeAction 299 302 let showExtras: Bool 300 303 let scripts: [ScriptDefinition] 301 304 let runningScriptIDs: Set<UUID> 305 + 306 + var isFolder: Bool { 307 + if case .folder = kind { true } else { false } 308 + } 309 + 310 + var pullRequest: GithubPullRequest? { 311 + if case .git(let pullRequest) = kind { pullRequest } else { nil } 312 + } 302 313 303 314 /// The first `.run`-kind script, if any. 304 315 var primaryScript: ScriptDefinition? { ··· 340 351 var body: some ToolbarContent { 341 352 ToolbarItem { 342 353 WorktreeDetailTitleView( 343 - branchName: toolbarState.branchName, 344 - onSubmit: onRenameBranch 354 + title: toolbarState.title, 355 + rootURL: toolbarState.rootURL, 356 + isFolder: toolbarState.isFolder, 357 + onRenameBranch: onRenameBranch 345 358 ) 346 359 } 347 360 ··· 353 366 pullRequest: toolbarState.pullRequest 354 367 ) 355 368 .padding(.horizontal) 356 - } 357 - 358 - if !toolbarState.notificationGroups.isEmpty { 359 - ToolbarSpacer(.fixed) 360 - ToolbarItemGroup { 369 + if !toolbarState.notificationGroups.isEmpty { 361 370 ToolbarNotificationsPopoverButton( 362 371 groups: toolbarState.notificationGroups, 363 372 unseenWorktreeCount: toolbarState.unseenNotificationWorktreeCount, ··· 387 396 onManageScripts: onManageScripts 388 397 ) 389 398 } 390 - 391 399 } 392 400 393 401 @ViewBuilder ··· 433 441 } 434 442 } 435 443 444 + private func toolbarKind( 445 + for selectedWorktree: Worktree, 446 + repositories: RepositoriesFeature.State 447 + ) -> WorktreeToolbarState.Kind { 448 + let selectedRow = repositories.selectedRow(for: selectedWorktree.id) 449 + guard selectedRow?.isFolder != true else { return .folder } 450 + guard let pullRequest = repositories.worktreeInfo(for: selectedWorktree.id)?.pullRequest else { 451 + return .git(pullRequest: nil) 452 + } 453 + // Only surface the PR when its head branch matches the current 454 + // worktree — otherwise stale info sticks around after a rename 455 + // or branch switch. 456 + let matches = pullRequest.headRefName == nil || pullRequest.headRefName == selectedWorktree.name 457 + return .git(pullRequest: matches ? pullRequest : nil) 458 + } 459 + 436 460 private func loadingInfo( 437 461 for selectedRow: SidebarItemModel?, 438 462 selectedWorktreeID: Worktree.ID?, ··· 445 469 return WorktreeLoadingInfo( 446 470 name: selectedRow.name, 447 471 repositoryName: repositoryName, 448 - state: .removing, 449 - statusTitle: nil, 450 - statusDetail: nil, 451 - statusCommand: nil, 452 - statusLines: [] 472 + kind: .removing(isFolder: selectedRow.isFolder) 453 473 ) 454 474 case .archiving, .deleting(inTerminal: true): 455 475 // The script runs in a terminal tab, so let the ··· 467 487 return WorktreeLoadingInfo( 468 488 name: displayName, 469 489 repositoryName: repositoryName, 470 - state: .creating, 471 - statusTitle: progress?.titleText ?? selectedRow.name, 472 - statusDetail: progress?.detailText ?? selectedRow.detail, 473 - statusCommand: progress?.commandText, 474 - statusLines: progress?.liveOutputLines ?? [] 490 + kind: .creating( 491 + WorktreeLoadingInfo.Progress( 492 + statusTitle: progress?.titleText ?? selectedRow.name, 493 + statusDetail: progress?.detailText ?? selectedRow.detail, 494 + statusCommand: progress?.commandText, 495 + statusLines: progress?.liveOutputLines ?? [] 496 + ) 497 + ) 475 498 ) 476 499 } 477 500 return nil ··· 522 545 .contentTransition(.numericText()) 523 546 .shimmer(isActive: true) 524 547 } 548 + .multilineTextAlignment(.center) 525 549 .frame(maxWidth: .infinity, maxHeight: .infinity) 526 550 .background(Color(nsColor: .windowBackgroundColor)) 527 551 .task { ··· 828 852 829 853 init() { 830 854 toolbarState = WorktreeDetailView.WorktreeToolbarState( 831 - branchName: "feature/toolbar-preview", 855 + title: "feature/toolbar-preview", 856 + rootURL: URL(fileURLWithPath: "/tmp/preview"), 857 + kind: .git(pullRequest: nil), 832 858 statusToast: nil, 833 - pullRequest: nil, 834 859 notificationGroups: [], 835 860 unseenNotificationWorktreeCount: 0, 836 861 openActionSelection: .finder,
+70 -79
supacode/Features/Repositories/Views/WorktreeLoadingView.swift
··· 3 3 struct WorktreeLoadingView: View { 4 4 let info: WorktreeLoadingInfo 5 5 @Environment(\.surfaceBackgroundOpacity) private var surfaceBackgroundOpacity 6 - private let bottomAnchorID = "worktree-loading-bottom" 7 6 8 7 var body: some View { 9 - let actionLabel = 10 - if info.state == .creating { 11 - "Creating" 12 - } else if info.state == .archiving { 13 - "Archiving" 14 - } else { 15 - "Removing" 16 - } 17 - let fallbackStatus = 18 - if let repositoryName = info.repositoryName { 19 - "\(actionLabel) worktree in \(repositoryName)" 20 - } else { 21 - "\(actionLabel) worktree..." 22 - } 23 - let statusLine = info.statusDetail ?? info.statusTitle ?? fallbackStatus 24 - let statusCommand = info.statusCommand 25 - VStack(spacing: 10) { 8 + let subtitle = subtitleText() 9 + VStack(spacing: 12) { 26 10 ProgressView() 27 - Text(info.name) 28 - .font(.headline) 29 - .multilineTextAlignment(.center) 30 - if info.statusLines.isEmpty { 31 - Text(statusLine) 32 - .font(.subheadline) 33 - .foregroundStyle(.secondary) 34 - .multilineTextAlignment(.center) 35 - if let statusCommand { 36 - Text(statusCommand) 37 - .font(.caption) 11 + .controlSize(.large) 12 + VStack(spacing: 4) { 13 + Text(info.name) 14 + .font(.title3) 15 + if let command = info.progress?.statusCommand { 16 + Text(command) 17 + .font(.subheadline) 38 18 .monospaced() 39 19 .foregroundStyle(.secondary) 40 - .multilineTextAlignment(.center) 20 + .lineLimit(1) 21 + .truncationMode(.middle) 41 22 } 42 - } else { 43 - ScrollViewReader { scrollProxy in 44 - ScrollView { 45 - VStack(alignment: .leading, spacing: 4) { 46 - if let statusCommand { 47 - Text(statusCommand) 48 - .font(.caption) 49 - .monospaced() 50 - .foregroundStyle(.secondary) 51 - .multilineTextAlignment(.leading) 52 - .frame(maxWidth: .infinity, alignment: .leading) 53 - } 54 - ForEach(Array(info.statusLines.enumerated()), id: \.offset) { _, line in 55 - Text(line) 56 - .font(.caption) 57 - .monospaced() 58 - .foregroundStyle(.secondary) 59 - .multilineTextAlignment(.leading) 60 - .frame(maxWidth: .infinity, alignment: .leading) 61 - } 62 - Color.clear 63 - .frame(height: 1) 64 - .id(bottomAnchorID) 65 - } 66 - .padding(12) 67 - } 68 - .frame(maxWidth: 560, minHeight: 180, maxHeight: 380) 69 - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous)) 70 - .overlay { 71 - RoundedRectangle(cornerRadius: 14, style: .continuous) 72 - .strokeBorder(.quaternary, lineWidth: 1) 73 - } 74 - .onAppear { 75 - scrollToBottom(using: scrollProxy, animated: false) 76 - } 77 - .onChange(of: info.statusLines) { _, _ in 78 - scrollToBottom(using: scrollProxy, animated: true) 79 - } 80 - } 23 + Text(subtitle) 24 + .font(.subheadline) 25 + .monospaced() 26 + .foregroundStyle(.tertiary) 27 + .lineLimit(5, reservesSpace: true) 28 + .truncationMode(.head) 29 + .contentTransition(.opacity) 30 + .animation(.easeInOut, value: subtitle) 81 31 } 82 32 } 83 - .frame(maxWidth: 640) 84 - .padding(.horizontal, 16) 85 - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) 33 + .multilineTextAlignment(.center) 34 + .frame(maxWidth: .infinity, maxHeight: .infinity) 86 35 .background(Color(nsColor: .windowBackgroundColor).opacity(surfaceBackgroundOpacity)) 87 36 } 88 37 89 - private func scrollToBottom(using proxy: ScrollViewProxy, animated: Bool) { 90 - if animated { 91 - withAnimation(.easeOut(duration: 0.12)) { 92 - proxy.scrollTo(bottomAnchorID, anchor: .bottom) 93 - } 94 - } else { 95 - proxy.scrollTo(bottomAnchorID, anchor: .bottom) 38 + private func subtitleText() -> String { 39 + if let progress = info.progress { 40 + let tail = progress.statusLines.suffix(5) 41 + guard tail.isEmpty else { return tail.joined(separator: "\n") } 42 + if let text = progress.statusDetail ?? progress.statusTitle { return text } 43 + } 44 + let noun = info.isFolder ? "folder" : "worktree" 45 + // Folder repositories are their own root — the repository name 46 + // duplicates the folder name, so skip the "in <name>" suffix. 47 + if !info.isFolder, let repositoryName = info.repositoryName { 48 + return "\(info.actionLabel) \(noun) in \(repositoryName)" 49 + } 50 + return "\(info.actionLabel) \(noun)…" 51 + } 52 + } 53 + 54 + #Preview("Streaming output") { 55 + @Previewable @State var statusLines: [String] = [] 56 + WorktreeLoadingView( 57 + info: WorktreeLoadingInfo( 58 + name: "sbertix/small-ui-improvements", 59 + repositoryName: "supacode", 60 + kind: .creating( 61 + WorktreeLoadingInfo.Progress( 62 + statusTitle: "Creating worktree", 63 + statusDetail: nil, 64 + statusCommand: "git worktree add", 65 + statusLines: statusLines 66 + ) 67 + ) 68 + ) 69 + ) 70 + .frame(width: 600, height: 400) 71 + .task { 72 + // Drip lines in so the preview exercises the trailing-lines 73 + // animation rather than showing a frozen tail. 74 + let pool = [ 75 + "Preparing worktree (new branch 'sbertix/small-ui-improvements')", 76 + "Enumerating objects: 1248, done.", 77 + "Counting objects: 100% (1248/1248), done.", 78 + "Compressing objects: 100% (512/512), done.", 79 + "Writing objects: 100% (1248/1248), 3.21 MiB | 5.40 MiB/s, done.", 80 + "Resolving deltas: 100% (842/842), done.", 81 + "HEAD is now at c4e9be3 bump v0.8.1", 82 + ] 83 + let clock = ContinuousClock() 84 + for line in pool { 85 + try? await clock.sleep(for: .milliseconds(600)) 86 + statusLines.append(line) 96 87 } 97 88 } 98 89 }
+14 -6
supacode/Features/Terminal/Views/EmptyTerminalPaneView.swift
··· 4 4 let message: String 5 5 6 6 var body: some View { 7 - VStack { 8 - Text(message) 9 - .font(.headline) 10 - Text("Use the plus button to open a terminal.") 11 - .font(.subheadline) 7 + VStack(spacing: 12) { 8 + Image(systemName: "apple.terminal.on.rectangle") 9 + .font(.title) 10 + .imageScale(.large) 11 + .accessibilityHidden(true) 12 12 .foregroundStyle(.secondary) 13 + VStack(spacing: 4) { 14 + Text(message) 15 + .font(.title3) 16 + Text("Use the \(Text("+").bold()) button to open a terminal.") 17 + .font(.subheadline) 18 + .foregroundStyle(.secondary) 19 + } 13 20 } 21 + .multilineTextAlignment(.center) 22 + .background(Color(nsColor: .windowBackgroundColor)) 14 23 .frame(maxWidth: .infinity, maxHeight: .infinity) 15 - .multilineTextAlignment(.center) 16 24 } 17 25 }