Native SwiftUI app providing full visibility into the Hermes AI agent: - Dashboard with system health, token usage, and cost tracking - Sessions browser with conversation detail and FTS5 search - Activity feed with tool call inspector (read/edit/execute/fetch/browser) - Embedded terminal chat via SwiftTerm with full ANSI/Rich rendering - Memory viewer/editor with live file-watching refresh - Skills browser by category with file content viewer - Cron job viewer with output display - Real-time log tailing with level filtering - Settings display with raw config and Finder path links - Menu bar status icon with quick actions Architecture: MVVM-Feature, zero dependencies beyond SwiftTerm, read-only SQLite access, Swift 6 strict concurrency. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.3 KiB
06 — Editor Patterns
Modern Liquid Glass tabbed editor architecture for all add/edit sheets.
Overview
All add/edit sheets follow a consistent tabbed architecture with:
- Wide canvas (800-900px x 600-700px)
- Tabbed navigation for progressive disclosure
- Smart natural language input prioritized as Tab 0
- Consistent spacing and visual hierarchy
- Instant tab switching with smooth transitions
Architecture
Sheet Presentation
.sheet(isPresented: $showEditor) {
NavigationStack {
EditorView(mode: editorMode, onDismiss: { showEditor = false })
}
.frame(minWidth: 800, idealWidth: 900, minHeight: 600, idealHeight: 700)
}
- Always wrap in
NavigationStackfor toolbar support - Use
minWidth/idealWidthandminHeight/idealHeight
Tab Structure
private let tabs: [DSEditorTab] = [
DSEditorTab(title: "Quick Entry", icon: "sparkles"), // Tab 0: Always NL input
DSEditorTab(title: "Details", icon: "info.circle"), // Tab 1: Core fields
DSEditorTab(title: "Financial", icon: "dollarsign.circle"), // Tab 2+: Domain-specific
DSEditorTab(title: "Terms", icon: "doc.text")
]
@State private var selectedTab: Int = 0
Body Structure
var body: some View {
VStack(spacing: 0) {
DSEditorTabBar(tabs: tabs, selectedTab: $selectedTab)
Divider()
ZStack {
if selectedTab == 0 { quickEntryTab }
if selectedTab == 1 { detailsTab }
if selectedTab == 2 { financialTab }
if selectedTab == 3 { termsTab }
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle(mode.isCreate ? "New Entity" : "Edit Entity")
.toolbar { /* cancel + save */ }
.onAppear { loadExisting() }
}
Rules:
VStackwithspacing: 0for tab bar + contentZStackwith conditionalifrendering (NOTswitchstatement)- Each tab gets
.transition(.opacity.animation(.easeInOut(duration: 0.15)))
Tab Content Template
private var tabName: some View {
ScrollView {
VStack(spacing: DS.Spacing.lg) {
DSFormSection("Section Title", icon: "icon.name") {
VStack(spacing: DS.Spacing.md) {
// Fields
}
}
}
.frame(maxWidth: .infinity)
.padding(DS.Spacing.xl)
.padding(.top, DS.Spacing.md)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
Tab Types
Tab 0: Quick Entry (All Editors)
Natural language input with live parsing preview.
DSSmartInputField(
placeholder: "Entity description example...",
text: $smartInput
)
Examples by entity type:
- Estimate: "Estimate for BKSI for $5000 due March 15 with Net 30 terms"
- Person: "John Doe at Apple, john@apple.com, (555) 123-4567"
- Project: "$50k website redesign for Acme Corp, starting March 1"
- Expense: "$250 client dinner at Restaurant with Jane from Acme"
Tab 1: Details
Core entity fields. Use DSTwoColumnRow for related pairs (number/status, date/expiry). Keep single-column for complex fields (text editors, long pickers).
Tab 2+: Domain-Specific
Financial (discount, tax, budget), Terms (payment, T&C), Contacts (related people/orgs), Time (tracking, estimates), Attachments, etc.
Design System Components
| Component | Purpose | Key Features |
|---|---|---|
DSEditorTabBar |
Custom tab bar | 56pt height, full-area clickable, accent highlight, 0.15s animation |
DSFormSection |
Grouped form fields | DSPlate styling, 16pt corners, 16pt padding, icon + title header |
DSTextField |
Single-line input | Label above, 8pt padding, quinary background, 10pt corners |
DSTextEditor |
Multi-line input | Configurable height (default 100pt), scrollable |
DSPickerRow |
Menu-style picker | Menu style, hidden redundant label, full width |
DSDatePickerRow |
Compact date picker | Supports .date, .hourAndMinute, or both |
DSToggleRow |
Toggle switch | System switch style |
DSTwoColumnRow |
Side-by-side layout | Equal-width columns, 24pt gap, top alignment |
DSSmartInputField |
NL input | 120pt height, accent border, ready for parsing |
DSAmountField |
Currency input | Label + prefix ($) + input + optional suffix |
DSNumericField |
Numeric input | Label + input + suffix (%) |
Spacing Standards
| Context | Token | Value |
|---|---|---|
| Outer padding (horizontal) | DS.Spacing.xl |
32pt |
| Top padding (additional) | DS.Spacing.md |
16pt |
| Between sections | DS.Spacing.lg |
24pt |
| Between fields | DS.Spacing.md |
16pt |
| Label-to-input | DS.Spacing.xs |
4pt |
| Internal section padding | DS.Spacing.md |
16pt |
| Tab bar height | Fixed | 56pt |
| Two-column gap | DS.Spacing.lg |
24pt |
Common Patterns
Create vs Edit Mode
let mode: EditorMode<Entity>
.navigationTitle(mode.isCreate ? "New Entity" : "Edit Entity")
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button(mode.isCreate ? "Create" : "Save") { save() }
.buttonStyle(.borderedProminent)
}
}
Loading Existing Data
private func loadExisting() {
if case .edit(let entity) = mode {
field1 = entity.field1
field2 = entity.field2 ?? ""
} else {
number = Entity.generateNumber()
}
}
Width Consistency
Apply .frame(maxWidth: .infinity) at every level:
DSFormSectioncontent VStackDSPickerRowPicker- Tab content VStack (before padding)
- Tab content ScrollView (at end)
Checklist for New Editor
- 3+ tabs with
DSEditorTabdefinitions ZStackwith conditionalifrendering (notswitch)- Tab 0: Quick Entry with
DSSmartInputField - Tab 1: Details with entity-specific fields
- All tabs: consistent padding (
xl+ topmd) - All tabs:
maxWidth/maxHeightframes DSFormSectionfor all content groupsDSTwoColumnRowfor related field pairs.borderedProminenton save/create button- Sheet frame: 800-900px x 600-700px
- Smooth 0.15s tab transitions
- Width consistency cascade
- Full tab area clickable
- Icons on all tabs and sections
Migration from Old Form Style
Before (Form-based):
Form {
Section("Title") {
TextField("Field", text: $value)
}
}
.formStyle(.grouped)
After (Tabbed editor):
VStack(spacing: 0) {
DSEditorTabBar(tabs: tabs, selectedTab: $selectedTab)
Divider()
ZStack {
if selectedTab == 0 { /* Quick Entry */ }
if selectedTab == 1 {
ScrollView {
VStack(spacing: DS.Spacing.lg) {
DSFormSection("Title", icon: "icon") {
DSTextField("Field", text: $value)
}
}
.frame(maxWidth: .infinity)
.padding(DS.Spacing.xl)
.padding(.top, DS.Spacing.md)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
Note
Each project implements these DS components in its own design system namespace (e.g., InControlDS, ShabuBoxTheme). The pattern and API surface should be consistent across projects even if the concrete implementation differs.