Skip to content

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 × N

Sheets (via NotesSheet enum, sheet(item:)):

SheetView
.newNoteNoteEditorView (isNew = true)
.editNote(note)NoteEditorView (isNew = false)
.liveNotesPromptLiveNotesConfirmSheet
.liveNotesTranscriptionView
.upgradeUpgradeView

NotesListView

State

PropertyTypeDescription
activeSheetNotesSheet?Currently presented sheet
searchTextStringLive search query
selectedCategoryNoteCategory?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 .onTapGesture on the parent LazyVGrid item
  • Swipe-to-delete and swipe-to-pin applied at the grid item level via .swipeActions

categoryColor maps NoteCategoryColor 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         → readModeContent

State

PropertyTypeDescription
titleStringEditable note title
noteBodyStringEditable body text (Markdown)
tags[String]Editable tag list
isEditingBoolToggled by Cancel/Edit buttons
isPinnedBoolMirrors existingNote.isPinned; saved via togglePin()
checkedAIItemsSet<Int>Session-only checked state for AI action items
bodyFocusedBool (FocusState)Controls keyboard visibility

Read Mode — readModeContent

Renders five cards in a VStack:

  1. heroHeader — title, category badge (.textCase(.uppercase) + .tracking(0.6)), metadata strip (creation date, source label, reading time), pin toggle button
  2. aiSummaryCard — shown only when existingNote?.aiSummary != nil; orange gradient strokeBorder
  3. bodyReadCard — iterates parsedBodyLines and dispatches to bodyLineView(_:lineIndex:)
  4. aiActionItemsCard — shown only when existingNote?.aiActionItems.isEmpty == false; uses CheckboxRow with checkedAIItems state
  5. tagsCardFlowTagLayout in 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):

PrefixMaps 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 dismissing

saveImmediate() 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 TextField for title
  • TextEditor for body with a placeholder ZStack (SwiftUI TextEditor has no built-in placeholder)
  • Word count + character count + reading time in a status strip
  • tagsEditSection with 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.

CategoryauroraColor
.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:

  • ultraThinMaterial background
  • LinearGradient strokeBorder — color derived from stateColor
  • stateGradient: LinearGradient fills the state icon circle
NotePreviewStatestateColorIcon
.created.orangenote.text.badge.plus
.updated.bluesquare.and.pencil
.deleted.redtrash
.searchResult.purplemagnifyingglass

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 RoundedRectangle border, accentColor.opacity(0.4)
  • Checked: filled LinearGradient, white checkmark, strikethrough on text
  • Animated with .spring(duration: 0.25) on isChecked

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

Internal — not for distribution