24 — iOS UX Refinement Backlog
Active. Governed by
00-ROADMAP.md. A running list of UX/polish details noticed during review, captured so they're not lost in chat. These are not blockers — the MVP review loop and Phase 2 browse work today. Pull items into slices as phases allow.Tags:
[quick]cheap, high-polish — pull in whenever a slice touches that screen ·[P2]/[P3]waits for that phase (usually needs a dependency like image upload).
Player & PlayerBar
Player-as-product envisioning →
57-PLAYER-AS-PRODUCT.md(2026-07-05): handoff behavior (browse ≠ play) · favorites queue · lock-screen/motion artwork · synced lyrics · full-screen "listening mode". The waveform-centric lock below (no big cover frame on the player) stands — doc-57 §6 is a separate, explicitly-entered surface, not a reversal.
Play button over the cover✅ shipped — cover thumb in[quick]PlayerBaris now the play/pause control (artworkButton+.ultraThinMaterialdisc + glyph).Portals button icon✅ shipped — now[quick]rectangle.righthalf.inset.filled."V#" as a bubble✅ shipped — accent-tinted capsule pill, separated from the filename, on both[quick]PlayerViewcurrent-version line andVersionCard.Version switching via the track-name line✅ shipped — the "V# {name} ⌄" line in[P2]PlayerViewis now a tappable control that opens a glass sheet ofVersionCards (slice 1 of25-IOS-CRUD-FIRST-RELEASE.md). The separate version-rail /TrackDetailViewwas dropped; tracks now open straight to the player from Recents, Browse, and Project → Tracks.- Cover frame on the player
[P3 — needs image upload]— the large cover/disc frame on the player is confusing as a primary element. Cover selection belongs in the Portal (pick among uploaded images → set as the track cover); the player should then just display the chosen cover. (No image upload on iOS in MVP, so this waits.) → The display home for big artwork is now envisioned as doc-57 §6's listening mode (Portal stays the manage home). Waveform pinned on scroll✅ shipped — pinned in[P2]PlayerView(header VStack sits above the commentsScrollView), folded into the waveform-as-scrubber slice below. Real PCM waveform rendering is still pending (placeholder is a sine pattern today).- Swipe-back from the top edge too
[owner-raised 2026-06-26]✅ v1 shipped (Claude Code;xcodebuildgreen; ⏳ on-device tuning). AsimultaneousGestureDragGestureonPlayerView: a drag starting near the top (startLocation.y < 90), mostly vertical, with enough downward travel (> 70) callsdismiss()— coexists with the waveform scrub + comment scroll. Tune thresholds / band height on device. - Misc
[later]— marquee for long titles; shuffle / prev / next / queue / share controls.
Performance & audio session (owner-raised 2026-06-20)
Opening a Track from Home — flash
[P1 — owner-raised 2026-06-26]✅ REAL fix 2026-06-26 (Claude Code;xcodebuildgreen; ⏳ on-device QA). First attempt (a "zoomless seeded push") was the wrong lever — owner wants the zoom, and the flash persisted; fully reverted (zoom +.matchedTransitionSourcerestored). Actual root cause: the player is waveform-centric (no big cover) — the cover lives in the bottomPlayerBar— yet the loading skeleton drew a big 180px cover hero. So the zoom grew that cover, thendetaillanded and the cover-less layout replaced it → cover grew then vanished = the flash. Fix = the skeleton now mirrors the loaded layout (seeded title → faint version/approve/timecode bars → waveform placeholder, no cover hero), so nothing vanishes when detail lands. Still open / needs device QA: residual first-open lag (push-time view construction — candidate for a Time Profiler trace), and whether the.animation(0.8, value: coverFile.id)body cross-fade should be trimmed.⭐ First audio byte is awful — load audio FIRST, before the player view
[P1 — perf, owner top priority 2026-06-24]. TheAVPlayerItemis created only afterGET /track/:idresolves, so nothing buffers until the player view has loaded its detail — the first play stalls badly. Fix = prefetch-on-tap with a known URL: (1) backend — expose the final audio file's signedblobUrlon the Track list payload (populateaudioin the list methods + addaudioFinalUrlinserializeTrack); (2) iOS — anAudioPrewarmersingleton that, the moment a row is tapped (or even appears), creates anAVPlayer+AVPlayerItemfor that URL and prerolls it, keyed by trackId; the player then adopts the already-buffered item instead of creating a cold one. Net: audio starts buffering at tap, in parallel with the detail fetch + view transition. (Builds on the shippedFile.duration+ prefetch-on-intent work below.) Updated by doc-56 (2026-07-05, owner: "still laggy and flashy on first load"): the S1 build found this mechanism never actually fired — step (1) above (the backend list-payload half) was never built, and the iOS decode expected a single File object where the API always sent theaudioarray; the permissivetry?nil'd silently, so prewarm + seeds no-oped on every surface (the June improvement the owner felt was the skeleton/de-hitch/duration work). Plus coverage decay: the surfaces shipped after 06-26 (approval rows, portals) never wired it at all. Doc-56 re-envisions this as a structural First-Play Pipeline (explicitaudioFinalFilewire contract — shipped in S1 — + route-enforced seeds/prewarm + TTFA metric + cache/renditions forks) — treat doc-56 as the source of truth for first-play performance from here.Opening the player stopped other apps' audio✅ fixed 2026-06-20 (Claude Code;[quick — bug]xcodebuildgreen). The doc-35-eraconfigureAudioSessionset the category and activated the session on the player VM's init, so merely viewing a wave took audio focus and stopped music playing in another app. Now init only sets the category (with.mixWithOthers, which doesn't interrupt), and a newactivateAudioSession()(setActive(true), exclusive) is called only when the user hits play —playPause(start) +selectVersion(resume). Other apps keep playing until you actually press play. Files:Features/Review/PlayerView.swift.First-load lag on a track✅ SHIPPED 2026-06-21 (Claude Code; backend[P2 — perf]nest buildand iOSxcodebuildgreen; ⏳ visual QA). Built (1) optimistic skeleton —TrackRoutecarries the track name + cover thumb (the list already has both), so the player hero + title paint instantly (the thumb is already in the decoded cache) with faint placeholder bars; no more bare spinner; (2) persistedFile.duration— the iOS uploader computes the localAVURLAssetlength (instant moov-atom parse) and sends it as a multipart text field; backend stores it (no server-side audio-probe dep, dodges the ESMmusic-metadatafriction); the player shows the timecode immediately and skips theasset.load(.duration)round-trip (falls back to network load on old/web versions — no backfill); (3) prefetch-on-intent — theAVPlayerItemis set current before play so AVPlayer warms the buffer while the user reads the screen. Not built: decoded-waveform cache per file id (deferred — waveform is synthesized, not fetched, so it's not the bottleneck). Original envisioning below. ↓First-load lag on a track
[P2 — perf](owner-raised 2026-06-20) — the first navigation to a track lags a beat (the waveform + the duration/time appear late); subsequent opens are fine. Likely the waveform data +AVPlayerItemduration resolve synchronously/late on first appear with no warm cache. Approach (best-practice, envision before building): (1) prefetch on intent — when a track row/tile is about to be shown (or on long-press/hover), kick off theAVAssetloadValues/duration+ waveform fetch so the data is warm by tap; (2) optimistic skeleton — render the player chrome immediately with a placeholder waveform + a known/estimated duration (fromTrack/Filemetadata if available) and swap in the real data when ready, so navigation never blocks; (3) cache the decoded waveform + duration per file id so a re-open is instant (mirrors the doc-34 image disk-cache thinking). The duration specifically should come fromFilemetadata server-side if we store it (cheap), avoiding the on-deviceAVAssetround-trip on first paint. Mostly iOS; a small backend assist (persist audiodurationonFile) would remove the worst of it — flag that as the cheap backend ride-along.
TestFlight-spotted (2026-06-10)
Play button stuck after track ends✅ shipped —[quick — bug]PlayerViewModelnow observesAVPlayerItem.didPlayToEndTimeNotificationon each loaded item (re-registered on version swap, torn down via a smallObserverHoldersidecar): on end it pauses, setsisPlaying = false, and seeks to.zeroso the control returns to the play glyph and the track is immediately re-playable.Accent colors out of sync across screens✅ implemented in-house by Claude Code (2026-06-11, build green — pending owner visual QA). Root cause: index-seeding in lists vs launch-randomized[quick — bug]String.hashValuein details. Fix:Theme.accent(forKey:)(stable FNV-1a over the entity id) now colors artist + project + track identity on every screen (Artists grid ↔ detail, Recents rows, Project header ↔ track rows, player halo / PlayerBar / empty-state hero).versionColor(for: index)retained for V1/V2/V3 version-scoped elements — the semantic is now identity color = who/what it is; version color = which revision.- The cover model
[partly shipped 2026-06-12 — Portal upload-new + PlayerBar artwork done]— supersedes both "Version switcher cover" and the older "Cover frame on the player" items. Manage in the Portal, display on the wave.- Manage (add / review / change): the Track Portal gets a Cover section — current cover + "Change cover" → photo picker → upload via the existing multipart plumbing (
POST /track/:id/file,image/*; backend already pushes toimage[]and flips the final image, andGET /track/:idalready populatesimage).Upload-new works today.✅ shipped 2026-06-12 (Xcode agent) —TrackDetaildecodesimage[]+imageFinal;TrackPortalContenthas the Cover section withPhotosPicker(.images); upload reusesAPIClient.uploadFilewith a realimage/*MIME resolved from the pickedPhotosPickerItem.supportedContentTypes.Pick-among-already-uploaded images still needs a small backend set-cover verb.✅ shipped 2026-06-12 — backendPATCH /track/:trackId/file/:fileId/set-cover(Claude Code) + iOS "Previous covers" rail in the Portal (Xcode agent): whendetail.image.count > 1, a horizontal rail of ~56ptAsyncImagethumbs renders under the current cover (skipping the active one); tap →confirmationDialog("Use this as the cover? / The current cover stays in history.") →setCoverPATCH → refetch — rail, main preview, and PlayerBar artwork all update. Rail disables during upload-new or set-cover so the user can't double-fire. ⚠️ TestFlight needs the next container redeploy. - Display (wave page): cover appears only as supporting elements — the
PlayerBarthumbnail (already the play/pause control;real artwork replaces the gradient✅ shipped 2026-06-12 — newartworkURL: URL?param,AsyncImagerenders under the existing material disc + glyph; gradient falls through onnil/ loading / failure) andthe screen's adaptive gradient halo derived from the artwork✅ shipped 2026-06-12 (Claude Code, build green) —PlayerViewModel.artworkAccentdownloads the cover once per file id, samplesAdaptiveColor.average(from:), and theScreenBackgroundhalo crossfades at hero pace (0.8s) from identity accent to the artwork color (doc 22 §6's personalization signature, finally wired). No big cover frame on the player. - Brand images (Artist / Project) — same pattern: each node's image is managed in its own Portal and displayed on tiles/cards/heroes. Lands with Portals-everywhere in the doc-26 build (the next major phase per doc 31) — captured here so it's on the roadmap, not built piecemeal.
- Cover versioning + release ownership envisioned (owner questions 2026-06-12) → see
28-RECORDINGS-VARIANTS-RELEASES.md§"Cover art & releases": covers already version viaimage[]/imageFinal(history UI ✅ shipped 2026-06-12); singles = track art, albums/compilations = Project art via the Project Portal, display preference track → project → gradient.
- Manage (add / review / change): the Track Portal gets a Cover section — current cover + "Change cover" → photo picker → upload via the existing multipart plumbing (
- PlayerBar artwork control refined
[shipped 2026-06-12 — pending QA]— owner QA on the cover slice: the material disc covered the artwork. Now: paused = artwork + small shadowed play glyph only; playing = glyph fades out and a breathing light ring (hairline stroke + slow sonar pulse, ~2.4s) carries the playback signal — the owner's "wave out of the circle" idea. Reduce Motion → static stroke. Comment composer hidden by keyboard✅ shipped —[quick — bug]CommentComposertakes an externalFocusState<Bool>.Binding;PlayerViewdrives one (isComposerFocused) and, while focused, hides the siblingApproveBar+PlayerBarso the composer is the only bar above the keyboard. The bottomsafeAreaInsetlifts via default SwiftUI keyboard avoidance, and the commentsScrollViewnow uses.scrollDismissesKeyboard(.interactively)so the user can swipe to dismiss.Tap a comment → seek to its time✅ shipped —[quick]CommentCardrows inPlayerViewhave a.onTapGesturethat callsviewModel.seek(to: comment.timeStart ?? 0), the inverse of the existing waveform-dot → scroll-to-comment direction.Player layout: waveform-as-scrubber, keep it visible✅ shipped — the separate[P2]Slideris gone;Waveformnow exposesonScrubChanged/onScrubEndedcallbacks driven by aDragGesture(minimumDistance: 0)(taps and continuous drags share one recognizer).PlayerViewsuppresses periodic-time-observer writes mid-drag viaisScrubbingand seeks on release. Layout was restructured so the header (title / version / timecode / waveform) sits pinned above the commentsScrollView, keeping the waveform — and the playhead — visible while comments scroll. Per-comment dots + section strips still work. ApproveBar reposition was deliberately held (see next item).Waveform: scroll-then-stick (collapsing header) + native title header✅ implemented in-house by Claude Code (2026-06-11, build green — pending owner visual QA), after a first Xcode-agent build of this item was reverted (it broke on the waveform's[P2].gesturelosing to the ScrollView pan). Now: singleScrollView+LazyVStack(pinnedViews: [.sectionHeaders]), waveform as the pinned section header (glass backing + hairline appear when pinned), title/version/timecode scroll away, brand-font title fades into the nav bar via a principal toolbar item. Scrubbing fixed with.highPriorityGesture; comment-marker taps hit-tested inside the same recognizer. ApproveBar untouched (still held, next item).- Comment timing precision
[P2]— capturing a comment uses the instantaneous playhead, which is too coarse; let users review/adjust the exact second of a comment (a stepper / fine-scrub on the timestamp at compose time, and ideally edit-after-the-fact). Future phase — flag the design (nudge ± / type the timecode) before building.- Owner clarification (2026-06-20) — freeze the timestamp at compose-open
[quick — bug-ish]. TodayPlayerView.postComment()reads the livecurrentTimeat send time (line ~358), so if playback keeps moving while the user types, the comment anchors to where the playhead drifted to, not where they were when they opened the box. Fix: capturecurrentTimeinto apendingCommentTimewhen the composer opens / first focuses, use that fortimeStart, and show it frozen in the composer. Open UX call (envision): on compose-open, (a) freeze the captured time only (playback continues — least disruptive) vs (b) also pause playback at that moment (owner's literal "the time should stop") vs (c) pause + show a scrub-to-fine-tune handle. Recommend (b) as the default (matches the mental model "I'm marking this spot") with the nudge±/edit-after from the parent item as the precision layer. Pairs with the existing edit-after-the-fact ask. Mostly iOS; no backend change.
- Owner clarification (2026-06-20) — freeze the timestamp at compose-open
Title/Version/Cover bar reposition✅ decided + implemented (2026-06-11, build green — pending visual QA). Owner picked status chips in the scrolling header (under the version line, scrolls away with the title; approval is an occasional act, not a constant control).[design — hold]ApproveBargained an.inlinestyle; the bottom stack is now just composer + PlayerBar. The "reads as status, not action" affordance concern (Discoverability section) stays open — revisit after QA.
QA round 3 (2026-06-11) — addressed in-house, pending re-QA
- Pinned-waveform backing, take 3 — the gradient scrim still produced a visible "section" edge below the title (reads white in light mode). Now a feathered halo owned by the waveform itself: a blurred rounded
snBackgroundfill hugging the bars — no hard edges in any direction, no section band. If this still isn't right, next iteration is no backing at all + stronger bar contrast/shadow. - Waveform gesture intent — touching the waveform then scrolling no longer jumps the timeline. A touch is undecided until ~10pt of travel: horizontal → scrub; vertical → cancelled (user wanted the list); a large vertical pull mid-scrub bails out (slide-off-to-cancel). Taps still seek / hit comment markers.
- Background audio ✅ shipped —
UIBackgroundModes: audio+ Now Playing (lock screen / Control Center show track + position) + remote transport (play/pause/scrub from lock screen & headphones). Scoped to the wave page for now — leaving it stops the session by design until the global player lands. - Global mini player / music-player behaviors — envisioned in
30-GLOBAL-PLAYBACK.md: Phase 2 =PlaybackManager+ floating glass mini player (zero chrome when idle); Phase 3 = favorites queue / loop / continuous play. Future feature, per owner. Behavior spec now in57-PLAYER-AS-PRODUCT.md(browse ≠ play handoff, favorites model, release-preview parity).
Naming
- The future grouping above ArtistSpace is "Roster" (not "Label"). Not a backend entity yet — see
23-IOS-PHASE2-IA.md.
Shared surface — parked (re-envision)
- The Shared tab shipped then was pulled (2026-06-07): "Shared" isn't a tab, it's a consequence of how Roster / workspaces / actors are modeled, which isn't decided. No Shared surface ships until the actors/Roster question in
25-IOS-CRUD-FIRST-RELEASE.mdis answered.
Approval & metadata (raised after slice 5 — first-release CRUD complete)
Version-scoped approval✅ designed + implemented end-to-end (2026-06-11, builds green — pending visual QA; spec in[P2]29-VERSION-SCOPED-APPROVAL.md). Owner confirmed: the unit of approval is the File —File.reviewStatus(+by/at), per-filePATCH /track/:trackId/file/:fileId/review-status,releaseStatus.versionmirrors the final version, new upload → Under review. The player's Version chip now binds to the loaded file. Data backfilled. Cheap follow-ups open: status dot perVersionCardin the switcher; "Approved by {who}, {when}" in the Portal. ⚠️ Needs a backend redeploy (doc 27) before the chip works on TestFlight.Metadata fields — completeness + sync✅ shipped 2026-06-12 (Xcode agent, build green) — Portal editor now covers[P2]ISRC,UPC,notes,mainArtistsin addition to name / label / genre / releaseDate. Wire keys match the backendTrackDTO(uppercaseISRC/UPC);UpdateTrackRequeststill omitsnilso unrelated fields aren't clobbered. Read mode hides empties; ISRC/UPC use uppercase / no-autocorrect input; Notes is a multi-lineTextField(axis: .vertical)with a label-on-top block in read mode.creditsremains parked (structured — needs a contributors editor; later).
Cover art (raised on TestFlight 2026-06-13 — grounded in 32-APPROVAL-MODEL.md §7)
- Cover not cropped to frame
[P2]— an oversized image overflows the artwork frame on upload. Near-term: client-side aspect-fill + a manual crop affordance at upload time (no backend — Sharp already emits the variants). Future phase (deferred — beta): iOS 27 on-device generative smart resize / outpaint / cover generation — real and wanted, not now; revisit post-beta. - Cover not shown in the hero
[quick]— the cover uploads but the hero still draws the gradient. Backend healthy (image[]/imageFinalserialize); slice-9 wired artist/project images →ArtworkHerobut the track-cover → hero (and project-cover → project-hero) path wasn't threaded. iOS wiring item (innext-brief.md); cascade = track cover → project art → identity gradient (doc 28). - Cover history = CRUD
[P2]—image[]is the history;setCoverImagecovers change (promote); the delete-from-history verb (backend) + a Portal cover-history browser (tap-to-promote, swipe-to-delete; never the current final without re-pointing) are the missing CRUD halves. - Cover approval home
[P2]— move the Cover sign-off off the Wave to the Project/Track Portal (cover = release art = a project/CD concern, doc 28).releaseStatus.coveralready exists — a surface move; optionally upgrade to the doc-29 per-image pattern. Full model in doc 32.
Image picker (raised on TestFlight 2026-06-13)
- Confirm the selected picture
[quick]— after picking an image (artist / project / track cover), show a preview + confirm step before upload (don't upload silently on pick). Cancel re-opens the picker; Confirm uploads. Quiet, on-brand (the chosen art is a brand moment). - Source: Files, not only Photos
[quick]— let the user pick from the Files app / document picker in addition to the Photos library (designers keep cover art in Files/iCloud Drive, not the camera roll). Add a.fileImporter(image content types) path alongside the existingPhotosPicker; both feed the same confirm step above. - Crop component — deferred (owner-confirmed 2026-06-13: "ok as a component/phase for later")
[P2]— a reusable manual-crop component (pan/zoom to the frame) is its own later phase; aspect-fill +.clipped()already prevents overflow today. On-device generative resize is further out (doc 32 §7, post-beta). - Photos source broken — regression
[quick](innext-brief.mdQA slice) — choosing "Photos" in the newImagePickerButtonmenu doesn't open the library. Likely thePhotosPicker-inside-Menugotcha; fix via.photosPicker(isPresented:)toggled by a@Stateflag. Blocks Photos upload — priority. - Track cover missing in the project tracklist — RE-OPENED 2026-06-13
[quick](innext-brief.mdQA slice) — first fix showed the project cover on every row, not each track's own. Root cause: the iOSTrackmodel decodes no image field; backend already serves per-trackimage[]viaGET /track/project/:id. Fix = addimage/coverURLtoTrack+ thread per-row with cascade track cover → project art → gradient.
Identity halo (owner-locked 2026-06-13 — refines 22-IOS-DESIGN-SYSTEM.md §6)
- Intelligent accent halo, not averaged
[P2](innext-brief.mdQA slice) — the artwork halo /AdaptiveGradientmust read the image's most vibrant accent color (highest-chroma dominant cluster, dropping near-grey/black/white), never the mean (which muddies to grey). Cache per image; cover-less nodes keep the identity-accent gradient. Owner: the halo should feel pulled from the art. RE-OPENED 2026-06-13:AdaptiveColor.vibrantshipped but is wired only to the version/player — apply it to all heroes (artist/project/track), retiring theaveragepath so no halo muddies. Re-opened again 2026-06-13 (round 2): (a) still missing on the Artists/Roster home (ArtistsHomeView); (b) intro flash — halo paints identity first then crossfades to the art color on every appear (animation keyed to color). Fix: key the crossfade to the cover identity (animate only on cover change), cache accent per URL, never show identity in front of a real cover. Full direction: doc 22 §6. Round 3 (owner 2026-06-14): (a) home halo wrongly shared — keyed to the first artist's cover and applied to the whole home + every artist; the home must have its own (identity) halo and each artist tile its own (cover-derived). (b) double halo — vibrant layer + an identity/base layer render together; when a cover exists the identity layer must not render (one layer only). Round 4 (owner 2026-06-14, deprioritized — after Wave 3 + variant slice): (a) home still double — screensnAccenthalo + per-tile vibrantArtworkHero.shadoware two different colors → make the home screen wash neutral/ambient, per-tile glow is the only halo. (b) nav flash — destination startsartworkAccent = nil→ identity (Theme.accent) shows for one frame before the cached vibrant resolves; seed synchronously fromAdaptiveColor.cachedColor(for:)via the route's cover URL. Diagnoses + fixes in doc 22 §6; the doc-34 disk image cache compounds (image also appears instantly on nav). - Halo transition should start from the parent's color, not the node's own default
[P2](owner 2026-06-19) — when a Track has a cover but its Project does not, the track halo currently crossfades from the track's own identity default → the track cover color. It should crossfade from the Project's default (identity) color → the Track cover color, for continuity (you arrived from a Project that was showing that default). Generalize the existing route-seed pattern (ArtistDetailViewalready seeds the halo from the navigating tile's cached cover color): carry the parent's effective color through the…Route— parent's cover color if it has one, else the parent's identityTheme.accent(forKey: parentId)— and use it as the child's initial halo, then animate to the child's own cover color. Applies down the chain (Artist→Project→Track→Version). doc 22 §6 continuity.
Image delivery / performance (owner 2026-06-14 — 34-IMAGE-DELIVERY.md)
- Slow cover loads — right-size + cache, no CDN
[P2]— Sharp variants (thumb/medium/original WebP) already exist but are unused: cover uploads store originals only andFilekeeps justblobUrl, so a 150px tile downloads a full original. Backend: wireuploadImageWithVariants+ persist/signthumbnailUrl/mediumUrl. iOS: request thumb for tiles/rows, medium for heroes, + a disk image cache (AsyncImagehas none). Fixes the late/wrong halo too. Full plan in doc 34. - Thumbnails look low-res on high-density / large screens
[P2](owner 2026-06-19) — e.g. Artist images in the Roster grid render blurry on iPhone 17 Pro Max (@3x) and iPads. The fixed 150²thumbis smaller than the rendered pixels (a 60 pt avatar @3x = 180 px; bigger tiles / iPad larger). Fix = density-aware variant selection (choose by target-points ×UIScreen.scale: use medium (600) whenever rendered px > ~150, reserve thumb for genuinely tiny avatars) and/or a larger thumb variant (e.g. 300²) so small contexts still get a small-but-crisp image. See34-IMAGE-DELIVERY.md§"Device-aware variant selection".
Comments / notes visible in Portals — "eagle-level" (owner-raised 2026-06-13 — 33-NOTES-ANNOTATIONS.md §Eagle-level)
- Annotation band in every Portal
[P2]— comments "disappeared" because the Portal renders no annotation band (comments still live only in the Wave timeline). Give each Portal Anchored-here + Rolled-up-from-below notes/comments, filtered to what matters at that altitude (unresolved · tagged · status-bearing): version = today's comments (leaf, enough); track/project/artist/roster = a subtree digest — less but louder the higher you fly. Needs a backend subtree-aggregate read; rides the Portals-everywhere + Notes phase. Full model in doc 33.
Notes → first-class annotations (raised 2026-06-13 — grounded in 33-NOTES-ANNOTATIONS.md)
- Notes = node-anchored
Comments[P2]— the singleTrack.notesstring becomes authored, dated, threaded, resolvable notes by reusing the existingCommententity (type=NOTE, notimeStart); it already carries owner / timestamps / responses /isResolved/ project+version anchors. Net-new is small: anartistSpaceanchor, aNOTEkind,tags[]for cross-hierarchy surfacing (doc-26 lens), per-node read endpoints. Surfaced in the Portal's Notes section (rides Portals-everywhere). Status stays opt-in resolve, not a default; doc-32 approval elevation only for true sign-off notes. Full model in doc 33.
Validation / QA coverage
- Shared inbound/outbound needs a multi-account matrix
[P2](owner 2026-06-13) — the Shared activity (doc-26 slices 5–6) can't be confirmed from one account. Needs seeded test accounts + a written run-through across: existing ↔ existing, invite to a brand-new account (signup → auto-add), and guest/link access (no account). A test-plan gap, not a code bug — flag for when QA tooling lands. Also tracked in26-IDENTITY-SHARING-NAVIGATION.md§Build plan.
Toolbar & image UX (owner 2026-06-14)
- Consolidate the 3 top-right buttons → fold
•••into the Portal[P2]— detail screens now show three trailing items (ⓘ Portal · + add ·•••overflow). The•••menu (Rename/Delete inArtistDetailView) is redundant with the Portal that ⓘ already opens (Rename is already there as "Edit"). Move Delete (and any other overflow actions) into the Portal and drop the•••→ cleaner two-button toolbar (ⓘ + +). This is the doc-26 Layer-4 principle: the Portal is the actions+metadata surface. Apply across Artist/Project/Track detail toolbars. - ProjectCard not cropped to a square
[quick]— a portrait project cover makes a portrait card (seen from the artist page). Cause: inProjectCard,AsyncImage { …scaledToFill() }is aZStackchild with no frame, so the image's intrinsic size drives the card and.aspectRatio(1,.fit)can't override it. Fix = mirrorArtworkHero: the gradientRoundedRectangleowns the square (.aspectRatio(1,.fit)), the image is an.overlay(inherits that size,scaledToFill→ crops), then.clipShape. (ArtworkHerois already correct; this is the one component that regressed.) - Image-picker confirm sheet doesn't cover the screen
[quick]— after picking, the confirm step (ImagePickerButton) presents at a partial detent, so the "Use this image" / "Choose again" controls sit below the fold and the flow reads as stuck. Fix: present the confirm step at.presentationDetents([.large])(or afullScreenCover) so the controls are visible without scrolling; keep it calm/on-brand.
Discoverability / general
- Approve affordance
[quick]— the three pills (Title · Version · Cover) are the approve control, but it reads as status, not action. Consider a clearer label/hint or a primary "Approve" treatment. - Row ellipsis → Portal, on every hierarchy item
[P2]— the•••button on list rows (Artist / Project / Track / Version) should be actionable (or removed if it adds no value), opening that item's Portal — the same metadata/inspector focus, adapted per node. This is the concrete UI hook for the "Portals everywhere" model in26-IDENTITY-SHARING-NAVIGATION.md(Layer 4). Build with the Portals-everywhere work, not piecemeal.
Roster & long-press polish (owner-raised 2026-06-19)
- Roster can't be changed from the Artist Portal
[quick]— the existing-roster picker (shipped inRosterEditorSheet, 2026-06-19) only appears where the caller passesexistingRosters.ArtistDetailViewopensArtistPortalContent(...)without that list, so the Portal's roster editor shows only create-new / remove — you can't move the artist to an existing roster from there (you can from the Home long-press, where the list is available). Fix: thread the distinct-roster list into the detail context and pass it toArtistPortalContent. Backend ready:GET /artist/rosters→[{name,color}]shipped 2026-06-19 (distinct rosters across the caller's artists) — the iOS half is just: fetch it inArtistDetailView, passexistingRostersintoArtistPortalContent'sRosterEditorSheet. (iOS only now.) - Long-press halo becomes a square mask, and sticks
[P2 — bug]— long-pressing an artist tile (context menu) snapshots the tile for the menu preview, clipping it to its bounds so the vibrant halo/shadow glow is lost (hard square); the squared state persists until another long-press elsewhere or a navigation. Classic SwiftUI.contextMenupreview gotcha (the preview host clips the shadow/blur layer). Fix: supply an explicit.contextMenu(menuItems:) { } preview: { }whose preview renders the tile with its halo (or a clean rounded card that intentionally drops the glow), so the implicit snapshot doesn't square-mask the live tile. Verify the squared state no longer lingers after dismiss. (iOS.)
Home header & typography (owner-raised 2026-06-21)
Home header redesign — full-bleed bubbles + brand-font title + liquid-glass on scroll
[design slice — Claude Code]. From the Home screenshot: a top banner/frame clips the artist bubbles, and theSonnancetitle uses the system large-title font. The owner wants: (1) artist bubbles always fully visible — no chrome cutting the cover circles; (2) theSonnancetitle in the brand display font (sonnanceDisplay= PPMonumentExtended, the same face as artist/project names) instead of the system large title; (3) the title rides up / turns Liquid-Glass on scroll (doc-22 §4 scroll-driven motion —pinProgress-style, the same mechanism as the player's waveform-halo/nav-title fade). Pairs with the deferred Home dashboard/menu redesign (doc-20 one-dashboard) — envision them together so this isn't reworked. Likely the "banner" = the nav-bar material + theScreenBackground0.10 wash edge; the redesign replaces the large nav title with an in-content brand title + a glass header that collapses on scroll. Grounded in doc-22 §4.ArtistsHomeView.Updated by doc-54 (2026-07-04): this item folds into Home v2 (the non-scrollable one-screen redesign, doc-54 §3.1) — envisioned together exactly as this item requested; the build lands with the Home v2 slice. (With no vertical scroll, the "rides up on scroll" behaviour applies to the See-all screens, not Home itself.)
Reject the section-label font
[design — owner decision](stakeholder rejected). The font for literals like "Projects" (theSectionHeadertitle) is.sonnanceTitle(Lexend-Bold) — but Lexend is NOT bundled (Typography.swiftnotes the .ttf isn't added), so it currently renders as the system font (SF Pro). So the rejected face is effectively SF Pro standing in for Lexend across allsonnanceTitle/sonnanceBody/sonnanceCaptionusages (section headers, row titles, body, captions — used widely). Options (owner to pick): (a) bundle the intended Lexend (Resources/Fonts/+UIAppFonts) and see if the real face is acceptable; (b) switch the section labels to PPMonumentExtended (the display face we already bundle — heavier, very on-brand for labels); (c) choose a new secondary face and rebindsonnanceTitle/Body/Captionto it. Recommend (a) first (cheapest — it may just be the SF fallback the stakeholder dislikes), then (c) if Lexend itself is rejected. A design-system change in22-IOS-DESIGN-SYSTEM.md§Typography — needs the owner's call before building.
How to use this doc
Items are noted, not scheduled. When a slice touches a screen, pull its [quick] items in for free polish; [P2]/[P3] items wait for their phase. Log to CHANGELOG.md when shipped and strike them here.