Appearance
Notes UI
Architecture of the Notes tab — NotesListView, NoteEditorView, and NoteCard.
View Hierarchy
NotesListView (NavigationStack)
│
├── AuroraView (orange / purple animated background)
│
├── [empty state] OR
│
└── ScrollView
├── PinnedNoteCard × N (horizontal carousel, only when pinned notes exist)
├── Category filter chips (LazyHStack of Capsule buttons)
├── Stats bar (note count / pinned count / enriched count)
└── LazyVGrid (2 columns, spacing 12)
└── NoteGridCard × NSheets (via NotesSheet enum, sheet(item:)):
| Sheet | View |
|---|---|
.newNote | NoteEditorView (isNew = true) |
.editNote(note) | NoteEditorView (isNew = false) |
.liveNotesPrompt | LiveNotesConfirmSheet |
.liveNotes | TranscriptionView |
.upgrade | UpgradeView |
NotesListView
State
| Property | Type | Description |
|---|---|---|
activeSheet | NotesSheet? | Currently presented sheet |
searchText | String | Live search query |
selectedCategory | NoteCategory? | Active filter chip |
NoteGridCard
private struct NoteGridCard — a standalone View (not a button) with its own @State private var appeared: Bool for the entrance animation.
- Tap handled via
.onTapGestureon the parentLazyVGriditem - Swipe-to-delete and swipe-to-pin applied at the grid item level via
.swipeActions
categoryColor maps NoteCategory → Color for the icon background tint.
PinnedNoteCard
private struct PinnedNoteCard — 176 pt wide fixed card with ultraThinMaterial background and an orange/yellow gradient border (strokeBorder with LinearGradient). Shown only when pinnedNotes (computed from viewModel.notes.filter { $0.isPinned }) is non-empty.
Category Chips
Orange LinearGradient fill + .shadow when selected; .ultraThinMaterial Capsule when unselected. Animated with .spring(duration: 0.25).
NoteEditorView
The editor has two distinct rendering paths depending on isEditing:
isEditing / isNew == true → editModeContent
isEditing == false → readModeContentState
| Property | Type | Description |
|---|---|---|
title | String | Editable note title |
noteBody | String | Editable body text (Markdown) |
tags | [String] | Editable tag list |
isEditing | Bool | Toggled by Cancel/Edit buttons |
isPinned | Bool | Mirrors existingNote.isPinned; saved via togglePin() |
checkedAIItems | Set<Int> | Session-only checked state for AI action items |
bodyFocused | Bool (FocusState) | Controls keyboard visibility |
Read Mode — readModeContent
Renders five cards in a VStack:
- heroHeader — title, category badge (
.textCase(.uppercase)+.tracking(0.6)), metadata strip (creation date, source label, reading time), pin toggle button - aiSummaryCard — shown only when
existingNote?.aiSummary != nil; orange gradientstrokeBorder - bodyReadCard — iterates
parsedBodyLinesand dispatches tobodyLineView(_:lineIndex:) - aiActionItemsCard — shown only when
existingNote?.aiActionItems.isEmpty == false; usesCheckboxRowwithcheckedAIItemsstate - tagsCard —
FlowTagLayoutin read-only mode
Body Line Parser
parsedBodyLines: [BodyLine] is a computed property. It splits noteBody by \n and maps each line to a BodyLine case:
swift
enum BodyLine {
case heading1(String)
case heading2(String)
case checkboxLine(Bool, String, Int) // checked, text, rawLineIndex
case bullet(String)
case divider
case paragraph(String)
}Parsing rules (evaluated in order):
| Prefix | Maps to |
|---|---|
# | .heading1 |
## | .heading2 |
- [ ] | .checkboxLine(false, …) |
- [x] / - [X] | .checkboxLine(true, …) |
- | .bullet |
--- (exact) | .divider |
| anything else | .paragraph |
Interactive Checkbox Toggle
toggleBodyCheckbox(rawLineIndex:) mutates noteBody in place:
swift
var lines = noteBody.components(separatedBy: "\n")
// toggle [ ] ↔ [x]
noteBody = lines.joined(separator: "\n")
saveImmediate() // writes to NotesStore without dismissingsaveImmediate() is identical to saveNote() but omits dismiss(). It is called after every checkbox toggle so state is never lost if the user dismisses without tapping Save.
Edit Mode — editModeContent
- Large
TextFieldfor title TextEditorfor body with a placeholderZStack(SwiftUITextEditorhas no built-in placeholder)- Word count + character count + reading time in a status strip
tagsEditSectionwith a gradient Add button
Formatting Toolbar
safeAreaInset(edge: .bottom) hosts the formatting toolbar when editing. Each FormatAction appends its insertion string to noteBody and sets bodyFocused = true to keep the keyboard visible.
swift
enum FormatAction: CaseIterable {
case heading1, heading2, bold, italic, task, bullet, divider
}task inserts \n- [ ] — producing a checkbox line that the parser picks up immediately when switching to Read Mode.
Aurora Color
auroraColor: Color is derived from existingNote?.aiCategory at init time. New notes default to .orange. The color feeds AuroraView(color1:color2:) to give the editor a per-category ambient feel.
| Category | auroraColor |
|---|---|
.idea | .yellow |
.task | .green |
.journal | .indigo |
.health | .red |
.goal | .orange |
.memory | .purple |
.finance | .teal |
nil / .other | .orange |
Pin Toggle
Tapping the pin button in heroHeader toggles @State var isPinned and immediately calls togglePin(), which constructs an updated NoteItem from the live store entry and calls notesStore.save(_:). This means pin state persists even if the user dismisses without tapping Save.
NoteCard (Chat Bubble)
NoteCard renders inline in a chat message when the LLM creates, updates, deletes, or searches for a note.
swift
struct NoteCard: View {
let preview: NotePreview // title + snippet + state
}Visual design:
ultraThinMaterialbackgroundLinearGradientstrokeBorder— color derived fromstateColorstateGradient: LinearGradientfills the state icon circle
NotePreviewState | stateColor | Icon |
|---|---|---|
.created | .orange | note.text.badge.plus |
.updated | .blue | square.and.pencil |
.deleted | .red | trash |
.searchResult | .purple | magnifyingglass |
State label text is .textCase(.uppercase) + .tracking(0.3) for a compact premium feel.
CheckboxRow
Shared between bodyReadCard (body-embedded checkboxes) and aiActionItemsCard (AI-extracted tasks).
swift
struct CheckboxRow: View {
let text: String
let isChecked: Bool
let accentColor: Color
let onTap: () -> Void
}- Unchecked: hollow
RoundedRectangleborder,accentColor.opacity(0.4) - Checked: filled
LinearGradient, white checkmark, strikethrough on text - Animated with
.spring(duration: 0.25)onisChecked
Body checkboxes call toggleBodyCheckbox(rawLineIndex:) → saveImmediate().
AI action item checkboxes mutate @State var checkedAIItems: Set<Int> (session-only, no persistence).
FlowTagLayout
private struct FlowTagLayout uses LazyVGrid with .adaptive(minimum: 60) columns to wrap tags into lines automatically. Each tag is an orange gradient Capsule with an optional ✕ remove button in edit mode.
Data Flow Summary
User taps note card
↓
NotesListView sets activeSheet = .editNote(note)
↓
NoteEditorView.onAppear → populateFromExisting()
↓
[Read Mode]
parsedBodyLines (computed from noteBody)
→ bodyLineView renders BodyLine values
→ CheckboxRow.onTap → toggleBodyCheckbox → saveImmediate
→ pin button → togglePin → notesStore.save
↓
[Edit Mode — user taps edit or taps body]
TextEditor mutates noteBody
FormatAction.insertFormat appends syntax
↓
User taps Save
→ saveNote() → notesStore.save → dismiss
↓
NoteEditorView.onDisappear → viewModel.reload()
NotesListViewModel publishes updated notes
NotesListView re-renders grid