Repository navigation
Conversation
…cisions yeti-surfaces.html draws the surfaces yeti-theme.html left out; yeti-decisions.html is the walkthrough Tim answered. CLAUDE.md records the rulings and the spec's load-bearing decisions (the spec itself is local-only). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds ThemeID.yeti ("Yeti") and Theme.yeti from the design's token panel,
with caps [.equalizerRows, .blisterPack, .stickers, .foilSweep, .plainVoice].
The four new capability flags gate every Yeti-only visual so later tasks
branch on caps, never on the theme id.
IdleCopy gains defaulted var fields (downloads empty title, search
no-results, the three Videos sections, the disabled live-edge label, the
Home lede, the ledger title). Defaults are today's strings verbatim, so the
five existing theme literals compile unchanged; Yeti overrides them. Not
wired into views yet.
Theme.rootTint(art:) returns snow under .plainVoice and effectiveAccent
otherwise; ThemedRoot uses it, so Yeti never paints a pink fill on
prominent buttons, tabs, toggles or links while the other five themes keep
their exact tint. labelPaper/labelInk comments now cover Yeti's card stock
and sticker ink. YetiPrimaryButtonStyle / YetiSecondaryButtonStyle (snow
capsule with night ink; frost outline with snow label; pressed, disabled,
controlSize-driven heights) are added for later tasks.
The Mac app now honors -UITestTheme under DEBUG + -UITEST like iOS, and
ThemeManager no longer persists the selection under -UITEST (the
PlaybackStateStore rule), so seeded launches on either platform can't
overwrite the user's saved theme.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the shared sticker components the Yeti theme is drawn with, in AppleNugs/Theme/Components/, translated from the design sheets' .stock, .sticker, .play-sticker, .rs and .tag rules. Nothing is wired into a screen yet; later tasks place them behind the .stickers capability. - StickerStock: the sale-yellow radial stock (#FBF1A6 -> #F2E27A 55% -> #E3D267, farthest-corner from 34%/30%) plus StickerLift, the design's two-layer box shadows drawn as blurred fills of the sticker's shape (stacked .shadow modifiers would shadow each other and have no spread). - PlaySticker (64/54/46/34): play/pause glyphs drawn from the sheet's 24-unit paths in labelInk, tilted -4 degrees, with the buffering spinner inside the same disc so the control never changes size. - PriceSticker (62/44/40/36/32): "Lossless" over "24/96", one fixed frame per size whether shown or hidden. PriceStickerLabel(player:) applies the presence rule: gated on player.current, never on nowPick/specs, which startCurrent() nils on every track change. - RowSticker (20/16): ink stair 100/75/50/25 on stock while playing, flat on idle-gray stock while paused, constant frame, no TimelineView. - LabelSticker (Live, Free) and PlainTag (4K, Audio, PPV). PriceStickerLabel is a pure Foundation policy compiled into AppleNugsTests: unknown pick reads "Lossless" with no spec line; known lossless prints depth/whole kHz (44.1 -> "44") once both are known and speaks the true rate; known lossy hides the sticker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the two remaining shared Yeti components to AppleNugs/Theme/Components/. They're translated from the design sheets' .card, .bubble, .bubble.sm, .bubble.empty and .sweep rules. Nothing is wired into a screen yet. Task 4 puts them on the now-playing card. - BlisterPack: labelPaper card stock with a frost keyline, the punched hang slot showing palette.base, and a BlisterBubble. Content can be printed on the card below the bubble, and a PriceSticker can sit on the bubble's corner (its size is picked from the bubble's side). The card takes the proposed width. The square bubble takes the height left after the printed content (which gets a higher layout priority), so the art yields height and is never a rigid tile (the iPhone Duo layout rule). There's an empty variant for sign-in and empty states, and two styles, regular and small. - BlisterBubble: the glass gradient, the rim, the top catch, the shade and glow inset shadows, the 7/8pt halo rings, a drop shadow drawn only outside the shape (the glass is translucent), the specular arc and two glare streaks over the art, and the saturate/brightness filter and glaze on the art. It can be used without a card (the Mac miniplayer). - PackArt: the cover filling the art square, or the MonogramTile scaled to the box when there's no cover. - foilSweep(identity:isPlaying:): a foil band masked by the title's own glyphs. It crosses once over 1,020 ms on cubic-bezier(.4,0,.2,1), runs on a keyframe animator, and only appears under .foilSweep. It reads no playback time and is skipped under Reduce Motion. Unlike the CSS, the band starts and ends fully off the title, so the title rests in plain snow with no pop. - FoilSweepTrigger: a pure policy, compiled into AppleNugsTests. It fires when playback is running for a track id other than the last one swept. So pause/resume never re-fires, and a launch-restored track fires on its first play. A track coming back fires again only after a different track played in between. When the view appears mid-playback, the track counts as already seen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under .blisterPack, StandardNowPlayingScreen (iPhone full screen, Duo columns, Mac Now Playing window) swaps its artwork + title block for NowPlayingPackCard: the blister pack with the price sticker on the bubble's corner, a 44pt heavy-rounded nameplate with the foil sweep, and the NowPlayingMetaText line (links intact). The stack trades its 22pt spacing for the mockup's tighter margins (NowPlayingCardMetrics) so the whole player fits the Duo's 433x635pt column; the bubble yields height first. The nameplate scales 44 -> 34pt before it wraps; only a title that still needs more than two lines at the 34pt floor gets a third, decided by two hidden floor-size probes measured at the full available width. Also under the caps: the 64pt play sticker (spinner kept inside its disc), "Track 3 of 24" in sentence case, AirPlay / star / queue bottom row with no format badge (the ⋯ queue menu moves to the header's trailing slot), volume slider and AirPlay glyph in slush and a saved star in snow under .plainVoice, no EqualizerBars beside the title under .stickers. The scrub slider already carried an explicit foil tint. MonogramTile prints slush initials on card stock under .plainVoice instead of accent letters. BlisterPack keeps its capped bubble slot square, so a narrow tall column no longer pads the card with dead space around the bubble. The five other themes keep their layout (offscreen renders pixel-match the pre-change code to within image-resampling noise). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under Yeti's caps, the four remaining playing surfaces take the stickers and the plain voice. The other five themes render unchanged (offscreen before/after renders differ by at most 2/255 on a handful of antialiased pixels). Expanded panel (iOS): 46pt play sticker in the transport row (new defaulted PillTransportControls.playControl; the pill keeps .glyph), 32pt price sticker where the format badge sat (held frame, presence on player.current), Queue as a frost-outlined .yetiSecondary button, AirPlay in slush, a saved star in snow, heavy 17pt title. The stack tightens 12 -> 6pt so the panel stays inside the fixed 250pt detent: measured 173pt ideal height on macOS vs 167pt for today's Soundboard panel; on iOS the Soundboard transport row grows with .title2 while Yeti's is pinned by the 46pt sticker, so the two land within ~1pt. Dashboard (shared): barker section headers as 17pt snow headers under .plainVoice; the price sticker on the quality header (44pt Mac, 40pt iOS; the section is already gated on player.current); RowSticker 16pt in the queue's playing slot, checked before .equalizerRows; the playing row heavy in sale yellow (activeEmphasis = playState). Playback errors (all three sites, now one PlaybackErrorLine) are said in snow with a slush mark on a shelf block instead of red. Signal rows, the leaves and Clear are untouched. Mac miniplayer: under .blisterPack a 100pt small blister bubble, a 24pt title with the foil sweep (identity + isPlaying only), the artist beside it and the venue on its own full-width line; under .stickers the triad's middle is the 46pt play sticker (new Style.sticker). Mac transport bar: StandardNowPlaying under .plainVoice draws a flat 42pt art chip, title, meta and "3 of 24" in textIdle; 34pt play sticker (plain button style so .borderless doesn't decorate it); the scrub slider tinted foil and the volume slider slush (the root tint is snow), via a tintIfSet helper so other themes keep the inherited tint; a 36pt price sticker replaces the format badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of tasks 1-4: a frame around an empty `if` reserves no space, so with no queue the card header lost its trailing 44pt slot and the centered line drifted ~22pt. A ZStack holds the slot. The header menu also drops the macOS pull-down bezel and chevron, which spilled past the slot (header only; the other themes' bottom-row menu is unchanged). PriceSticker's PlayerService-reading initializers move to their own file, so offscreen renders can compile the view without the player. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the Yeti caps, Home follows the design sheets:
- Greeting: no "YETI" eyebrow and no period under .plainVoice ("Late
set." keeps its stop); the lede now comes from theme.copy.homeLede for
every theme (the default is today's string).
- iPhone hero (new, .blisterPack only, when player.current != nil): the
cover in the blister pack with the price sticker, title, meta (links
on), a foil progress rule and a 54pt play sticker. The sticker toggles
playback; the card opens the full-screen player through a new
openNowPlaying environment action that IOSRootView wires to the same
nowPlayingPresented state the pill's tap sets, so there is still one
presentation. Other themes' iPhone Home doesn't change, and the
.blisterPack check comes first so their Home never reads
player.current.
- Mac resume card under .blisterPack: card stock, a flat 96pt cover (no
bubble, no hang slot, per ruling 4), no eyebrow, a foil rule and a
54pt sticker.
- ResumeProgressRule moved out of the macOS block into
HomeResumeHero.swift so both heroes share the one leaf that reads
playback time. A small host view reads the player for the Yeti heroes,
so Home's body subscribes only to current != nil.
- ListeningLedger under .plainVoice is "The nice list": a section
header, one totals line, and checked rows with plays and hours in two
right-aligned columns. The split value/unit strings come from new,
tested LedgerSummary figures. The hasAnyPlay gate and where the stats
are read stay the same.
- Favorites and crate headers are 22pt sentence case, and the chips get
a frost keyline under .plainVoice. BlisterPackStyle gains a .hero
size.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the Yeti caps, the browse surfaces follow the surfaces sheet (spec items 11–15). The five other themes take their old code paths unchanged. - Artists list (.plainVoice): names only (ruling 2), heavy 17 on frost rules, heavy slush letter headers instead of accent; the filter is a 38pt shelf field with a foil caret; no matches uses searchNoResults. - Artist page: CrateHeader drops the LCD pill and the bars (the EqualizerBars table: nothing here under .stickers) for a large-title name, a "340 shows · 12 videos · 5 albums" counts line, and Follow as a small frost-outlined button (check / plus). The counts row is as tall as the button, so paging in counts never moves the list. CrateRow: Live as LabelSticker, 4K as PlainTag, idle index; month headers heavy slush; the crate filter is a shelf field with a foil caret; thumb placeholder glyphs in slush. - Show page: Play all is the snow primary; Play next / Queue / Save / Download are 36pt frost rings on iPhone (new YetiIconButtonStyle) and frost-outlined buttons on the Mac; Downloaded is a filled snow disc; download progress is explicitly foil; set headers slush. The playing row gets the NEW 20pt RowSticker site in its number column plus a faint sale wash; every row's number column is 20pt tall so the sticker moving never resizes a row, and isPlaying is read inside TrackRow on the playing row only. - Search: a 40pt shelf field with a foil caret, heavy slush section headers (all-caps API headers set in sentence case), songs' play mark an outlined slush disc (.stickers), searchNoResults copy, and Yeti's night under the whole tab. - Library: Downloads and Favorites empty states on the empty blister pack (new shared EmptyPackMessage, reusable by tasks 8/9); downloads' play glyph in snow; favorite artist chips get a frost keyline; ShowCard heavy 15 title / slush artist; CoverArt gets a frost keyline and a slush placeholder glyph (show page header and ShowCard). - tintIfSet moved from TransportBar into ThemeEnvironment so every file can apply an explicit tint without resetting the root tint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Videos (spec item 16): section names come from the theme's copy (the
defaults are today's names, so the five themes read the same); under
.plainVoice they are heavy 22 and the "Just streamed" line drops its
redundant lead-in. Under .blisterPack the empty state is the empty pack.
VideoThumbnail loses its centered play glyph under .stickers (the poster
is the button) and its badges become a LabelSticker for offers (Live,
Free) and a PlainTag for facts (PPV, Audio, 4K); under .plainVoice the
poster takes a frost keyline, a slush placeholder glyph and ShowCard's
heavy title over a slush artist. Resume progress was already a frost
track with the accent fill, which under Yeti is foil: time moving.
Video detail (item 17): the Live sticker over AVKit (controls untouched);
Play/Pause, Watch on… and Buy on nugs.net are snow primaries; Go Live and
Resume are frost rings; Save is the iPhone's 36pt ring (labeled on the
Mac), never accent. Go Live reads the copy's liveEdgeDisabled while it has
nothing to do ("At the live edge" under Yeti, "Go Live" by default) and
"Go Live" otherwise in every theme. Benefit links are snow with a dotted
underline, set per link run with the AttributeScopes subscript; the runs
keep their .link, so the root interceptor still hands them to the system.
The current chapter is sale yellow with no lead glyph. The PPV panel is
card stock with a slush lock. Both error overlays are said in snow and
slush, with no triangle. The new pieces take plain values, so they render
offscreen; the five themes keep their exact previous views.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yeti (gated on caps, never the theme id):
- Sign-in under .blisterPack: the empty pack with the "Yeti" nameplate, a
snow primary "Sign in with nugs.net" that reads "Opening nugs.net…" with a
night-ink spinner while the browser sheet is up (kept at full strength,
second tap guarded instead of .disabled), and errors in snow, not red.
- Connection failed (shared ConnectionFailedView, both roots): the small
empty pack, the raw error in quiet monospace, "Try again" / "Listen
offline" / "Sign out". "Listen offline" keeps today's downloads-exist
gate; the line "Your downloads still play while you're offline." shows
only when downloads exist AND OfflineAccessPolicy allows offline play.
- Toasts gain a kind (UIState.Toast). The four failure strings (five call
sites) pass .failure. Under .plainVoice the shared ToastCapsule is glass
with a frost hairline and a snow check or a slush warning mark. Other
themes draw exactly as before.
App-wide (every theme):
- "Retry" -> "Try again" and "Sign Out" -> "Sign out" on the connection
screens. "Log Out" -> "Sign Out" in both account menus.
- All three clear-queue paths confirm with one shared dialog ("Clear the
queue?" / "Playback stops and the whole queue empties." / Clear Queue /
Cancel): the ⋯ menu, the dashboard's Clear, and Playback ▸ Clear Queue.
The menu raises a UIState flag and RootView's Group hosts the dialog,
because a Commands builder can't present one.
- The offline-expiry error gains its "why" line
(OfflineAccessPolicy.expiredDetail) inside the dashboard's error block.
The Signal rows are untouched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tim's call after living with theme #6 on his Duo: Yeti is the default experience (stage d, then e), and the Duo's own pill overlay gets a shelf so content stops above it, folding away with a Yeti-style motion when it ducks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three Yeti-only polish fixes (gated on .plainVoice; the other five themes
are unchanged):
- Artist page: the 34pt hero name already says who this is, so the iOS
navigation bar drops its visible title (toolbar(removing: .title) via a
new hidesNavigationBarTitle helper) while navigationTitle stays for the
back button and history menu. The scope segments lose their counts
("Shows / Videos / Albums"); the counts line under the name carries them.
- Show page: the meta stacks like the sheet's .show-ios instead of sitting
side by side, where a city wrapped into a ragged column: artist as a snow
link (routed through the existing x-applenugs-meta artist route), then
"date · venue" on one truncating line, then "runtime · N tracks", slush
13.5, bottom-aligned with the cover.
- YetiSegmentedControl: a shelf track (radius 10, 3pt inset, 34pt) with a
lighter thumb that springs between segments (none under Reduce Motion),
heavy 13.5 labels in snow/slush, each segment a button with .isSelected,
the group labeled like the picker it replaces. Used by the Library's
Favorites/Downloads switch and the artist page's scope; other themes keep
the system Picker(.segmented).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Playback > Clear Queue raised a UIState flag that only the main window's
RootView observed, so with only the Now Playing window open nothing
appeared and the dialog popped up unprompted once the main window came
back; a minimized or backgrounded main window took the sheet out of
sight, two main windows each presented one, and a window already showing
a sheet (the offline Downloads sheet) collided with it. The menu command
now asks itself with an NSAlert (ClearQueueAlert, macOS only): a sheet on
the key (else main) window when it is an ordinary visible window with no
sheet of its own, otherwise runModal; one alert at a time. The flag and
the RootView presentation are gone. The four strings move into a shared
ClearQueueCopy enum so the SwiftUI dialog and the alert can't drift; the
alert's Clear Queue button keeps hasDestructiveAction.
- YetiHomeHero: the container now carries the pill's label/hint pattern
("Now playing: title, artist", "Opens full-screen now playing"), so the
"Open now playing" action has something announced to land on; the play
sticker and meta links stay their own elements.
- AppModel.loginWithBrowser returns early while a login is in flight;
the Yeti sign-in button reports "Opening nugs.net" as its a11y value.
- Classic connection-failed screen: "Listen offline" (sentence case).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erified list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Where the pill is our own overlay (iPhone Duo, vertical tab bar), it no longer floats over scrolling content. It sits on a shelf, per the approved mockup docs/design/yeti-theme/yeti-pill-shelf.html. The shelf is an opaque night band with a frost lip, 76pt open and 16pt closed from the physical bottom edge, and the content stops at its lip. Scrolling down tips the pill into the shelf through its hang slot (240ms ease-in, down 70pt, 0.94x0.8 from the top). After 70ms the shelf folds over 300ms and the slot fades in. Scrolling up, reaching the top, a 1s idle, or tapping the slot (a button, "Show the player") opens the shelf (260ms ease-out). The pill then pops back with one overshoot: 440ms, -5pt at 1.02x1.04 at 70%. With Reduce Motion the shelf snaps and the pill only fades. VoiceOver never tucks, and turning it on brings the pill back. - PillShelfBand is drawn inside each tab, over its NavigationStack. That keeps it under the vertical tab bar, as in the mockup; an overlay on the TabView would paint over the rail. The pill and the slot stay above the TabView, so they ride over pushed screens. - Every scroll root's bottom margin, Downloads' footer and the toast follow the lip on the shelf's own curve. The clearance is the shelf height above the measured home-indicator band (42pt open, 0 closed on the outer display). - Feedback guard. Folding the shelf shrinks the scrollable range, and at the bottom of a page that read as a scroll up, which looped the pill. The pure policy now measures deltas inside the range both samples share. Content that can't scroll holds the pill's visibility. The top zone shows a ducked pill only when the user really brings it up there. PillDuckPolicy.Gate drops samples outside .tracking/.interacting/.decelerating, and every sample for 0.5s after a transition while the shelf moves. - Placement follows the mockup: 22pt side insets (was 46) and the pill 14pt above the physical edge. - The band, lip and slot colors come from the theme palette (on Yeti they match the mockup's hex). - New DEBUG hook -UITestPillTucked holds the closed shelf for screenshots. Regular iPhones keep the system accessory untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mockup drew the closed lip 16pt from the physical edge; on the Duo the home indicator then lands on the slot, hiding it and putting its tap target in the system's edge-gesture zone. The closed shelf is now the home-indicator band (capped at 40pt, so a keyboard can't lift it past the open shelf) plus the 16pt lip. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stage (d) of the Yeti-only spec. ThemeManager now always yields Theme.yeti and neither reads nor writes the old selectedTheme key (Tim is the only user, so there's no migration). The Theme picker is gone from the macOS and iOS account menus and the macOS View menu, and both apps drop -UITestTheme. The class goes when the old themes are deleted. On macOS, keyboard focus on our own controls now draws the design sheet's :focus-visible, a 2pt foil outline 3pt outside the control's shape, instead of the system ring. It covers the Yeti primary, secondary and icon button styles, YetiSegmentedControl segments, the transport bar and miniplayer play stickers, and the Home resume card. The outline is an overlay driven by \.isFocused, so no frame changes. The .plain sticker and segment buttons also disable the system ring (macOS only). System controls keep the system ring, and iOS doesn't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the old waveform AppIcon.appiconset (and the script that drew its iOS 1024) with AppleNugs/AppIcon.icon: a clear blister bubble filling the tile, the yellow playing sticker sealed inside, one foil-pink glint on the rim, on night navy. Four groups, front first: Foil, Bubble (rim is real Liquid Glass, replacing the flat art's painted glare), Sticker, Card; the night field is the document fill, so dark/clear/tinted come from the system. actool compiles it for both targets under the existing ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon and derives the pre-26 flat renditions (AppIcon.icns for macOS 14/15, AppIcon60x60@2x.png). It silently prefers a .icon over a same-named appiconset, so there is no hand-tuned 16/32 art: the system draws the small sizes from these layers. scripts/render-app-icon.py regenerates the .icon, the flat SVG masters in docs/design/yeti-icon/, and ictool previews of every rendition. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tim's call (2026-11-07): follow the system guidelines rather than run custom chrome. Where the tab bar is vertical (the folded iPhone Duo's outer display) the now-playing pill is now Apple's tabViewBottomAccessory, exactly as on every other iPhone, with .tabBarMinimizeBehavior(.onScrollDown) kept. The system's scroll edge effect handles content under the bars: the tab stacks set .scrollEdgeEffectStyle(.soft, for: .bottom), because with a vertical bar the .automatic default drew no bottom edge effect and content ran crisp under and below the pill (horizontal-bar iPhones already get soft and render unchanged). Accepted trade: on the folded Duo the pill no longer tucks away on scroll. Removed the whole custom path: PillDuck (the overlay, the shelf band, the scroll reporter, PillOverlayClearance, PillShelfMotion, PillDuckMetrics, the pillOverlayActive environment value, -UITestPillTucked), PillDuckPolicy + PillShelfGeometry and their 35 tests, the toolbarVerticalEdge fork, the tab stacks' safe-area measurement, the scroll-reporter modifiers on nine scroll roots, and the toast's shelf clearance (back to a plain 8pt above the stack). PillLayout's shed-by-measured-width stays: the Duo's ~290pt accessory still reports .expanded. Gate green: both schemes warning-free, 341 tests. Duo simulator (outer display): system pill beside Apple's vertical rail, shed to prev/play/ next + chevron, content softly faded under it; iPhone 17 Pro unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mockup as history Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yeti is the only design, and it uses the standard transport signature everywhere, so the bespoke ones are unreachable. Deleted: TapeLabelCard, JCardStrip, FaceplateTransport, TouchFaceplate, ClickWheelScreen, FaceplateParts (VUMeter/KnurledButton), MiniPlayerCassette (+ CassetteReels, ReelHub), MiniPlayerFaceplate, MiniPlayerClickWheel, TapeGeometry and its 11 tests (project.yml test-sources entry too). The four dispatches collapse to the standard path: - NowPlayingScreen: the theme.transport wrapper is gone and StandardNowPlayingScreen takes its name (call sites unchanged); the Tape Room tape counter and the Shoebox upper-case meta line go with it. - TransportBar: the faceplate bar and the tape-label / J-card blocks go; the bar always uses StandardNowPlaying. - DashboardMiniPlayer: always MiniPlayerStandard, which loses its J-card spine rule, tracked caps and outline play button. - MiniPlayerParts: MiniPlayerClock (used only by the cassette) folds its remaining-time formula into MiniPlayerScrubTrack; the scrub track's trackHeight/showsClocks/fillColor knobs and the triad's .outline/.bare styles and dead playGlyphColor switch go. Caps branches, the art-color pipeline and wash/grain are untouched (e2/e3); TransportSignature itself stays until Theme is stripped in e4. Yeti's paths see only removed siblings, so they render as before. CLAUDE.md drops the deleted files from the leaf-reader list, the DashboardPanel sheet hosts and the miniplayer bullet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yeti is the only design and it never consumed the cover color: a static accent, wash .none, texture 0. So the dominant-color extraction (a CIAreaAverage pass per track), the wash and the paper grain were all running for nothing or not at all. Deleted: ArtColorProvider, ArtColorModifier (.providesArtColor), the \.artColor environment key, ArtWashBackground, PaperGrain, the .artWash(...) modifier and themedSurface's grain overlay. - Every effectiveAccent(art: artColor) read is now palette.accent and every activeEmphasis(art: artColor) read palette.playState: what a static theme already returned. The two art-tinted shadows (artColor ?? accent) take the accent. rootTint becomes an art-free property; its .plainVoice branch stays for e3. - .themed(_:) loses its art: parameter; the macOS main window and Now Playing window call it directly, and IOSRootView drops its provider, task and environment write. PlayerService.nowPlayingImage stays. - With no readers left, Theme loses accentMode, washStyle and textureOpacity, AccentMode and WashStyle go, and Capabilities.artWash goes (bit 0 is left unused). - Wash and grain overlays under Yeti drew Color.clear or nothing, so they are removed outright rather than replaced. Yeti renders the same: the iPhone now-playing screen pixel-compares identical to e1 apart from the status-bar clock. 330 tests, unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yeti is the only design, so every `theme.caps.contains(...)` read (83 sites) and the per-view computed properties wrapping them (`plainVoice`, `stickers`, `card`, `pack`, `plain`) keep the branch Yeti takes and lose the other. `.condensedHeaders` / `.indexLetterTab` / `.vuMeter` were never set for Yeti, so their uppercasing, tracking and index tabs go too. Nothing reads `caps` any more; `Capabilities`, `ThemeID`, `ThemeManager` and the five theme literals stay for stage e4. - `tintIfSet(plainVoice ? x : nil)` becomes `.tint(x)`; `tintIfSet` is deleted. `card ? X : 0` paddings in `NowPlayingScreen` become `X`; its classic artwork, title block and standard bottom row are gone. - `.stickers`-before-`.equalizerRows` checks resolve to the sticker path: the queue slot draws `RowSticker`, the now-playing title and CrateHeader draw nothing. The animated `EqualizerBars` and the caller-less `ArtistRow` are deleted. - Home: the classic macOS `resumeCard` goes; `YetiResumeCard` (via `YetiHeroHost`) is the hero on both platforms. `ResumeProgressRule` loses its 320pt default (only the classic card used it). - The show and video pages' duplicate private button helpers collapse: primaries/secondaries are direct `.buttonStyle(.yetiPrimary/.yetiSecondary)` calls, and the platform-forked pair moves to `YetiButtonStyles.swift` as `yetiPageAction()` (was `yetiShowAction` == `videoSave`) and `yetiDownloadedAction()`. - Copy: views read Yeti's strings unconditionally; the system `ContentUnavailableView.search` fallback for a nil `searchNoResults` goes (the closure's optional return is e4's to remove). - Login, connection-failed, Library/Favorites/Videos/Downloads empty states, toast, playback/video error lines, PPV panel and video badges keep only their pack/sticker/plain-voice bodies. `MiniPlayerTransportTriad` loses its `Style` (always the sticker), `hidesNavigationBarTitle` its Bool, `VideoErrorMessage` its unused `systemImage`, `AudioFormat` its now-unread `badge`. Invariants kept: only leaf views read `currentTime`; Signal's six rows gated on `player.current`; `DashboardPanel` stays shared; the price sticker is gated on `player.current`; FoilSweep reads no time; art yields height (the pack card's monogram scales). iOS screenshots are pixel-identical to stage e2 apart from the clock and Home's random crate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Yeti is the only design, so the machinery for choosing between looks goes and ONE token struct remains as the design system. - Theme is palette / type / copy only, with one value, Theme.yeti, which is now the \.theme environment default (was .tapeRoom). Gone: id, caps, transport, darkOnly, Capabilities, TransportSignature, Theme.make, rootTint (ThemedRoot tints palette.textPrimary directly; its crossfade on theme.id goes) and the five dead theme literals. ThemeID.swift and ThemeManager.swift are deleted; the app roots drop their @State manager and rely on the environment default (the Mac Now Playing window calls .themed(.yeti)). - Palette folds in the off-palette hex with semantic names, values unchanged: cardStock / stickerInk (were the optional labelPaper / labelInk; the `?? raised` fallbacks go), stickerHighlight/Sale/Edge (StickerStock), segmentThumb (YetiSegmentedControl), videoErrorBackdrop (VideoErrorMessage), bubbleGlass/Shade/Glaze (BlisterGlass, whose snow/glazeBottom alias textPrimary/base). The environment-free StickerStock and BlisterGlass statics read Theme.yeti. losslessBadge and vuPeak had no readers and go. Theme.swift is the only file with hex. - IdleCopy becomes Copy: Yeti's strings as plain lets, dashHeaders as dashNow/dashQuality/dashUpNext, and searchNoResults a plain function returning String (the two `?? ""` go). - macFocusEffectDisabled() loses its unused parameter; still macOS-only, so iOS keeps its focus effects. - Stale comments naming themes, caps, signatures and theme switching are rewritten. CLAUDE.md is rewritten for one design: a Design system bullet replaces the five-theme one; the miniplayer, meta-link, leaf-reader and Duo bullets drop deleted surfaces; the scrub-guard note now counts three surfaces; START HERE records stages d and e and what is next; the 2026-08 sections are marked as history. Yeti renders the same: iOS screenshots (now playing, Home, Artists, Search, Library, Videos) pixel-compare identical to stage e3 apart from the status-bar clock and Home's time-of-day greeting. 330 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of stages d/e: the modifier sat on the stack's root view, so pushed artist/show/video screens on the folded Duo might not get it. It now sits on the NavigationStack. Also a stale comment and CLAUDE.md's BufferedRow note, and the Mac double-focus-ring check for the device pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Tim on his Duo: the 32pt lossless sticker in the panel's bottom row competed with the play sticker right above it. It's gone; "Lossless 16/44" is plain slush text under the venue instead (same presence policy, one constant line), and the bottom row is AirPlay … Queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starting a video paused audio only when PlayerService.isPlaying said so, and nothing stopped audio from starting while a video owned playback: a stream resolve or retry landing after the video took over played underneath it. Now the claim silences the engine unconditionally, startPlayback() refuses automatic starts while suspended for video, and a deliberate audio start (play, next/previous, the pill's play) asks the video to pause and hand back first. The last thing deliberately started wins. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On iPhone audio never paused when a video started. Claim now calls pauseForExternalAudio() on every claim, even one the video thinks it already holds, and "was audio playing" reads the engine's rate as well as the isPlaying flag, so a stale flag can neither leave audio running nor skip resuming it. A track ending with no successor preloaded advances with startAt(deliberate: false), so that automatic start stays silent under a video instead of pausing it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bubble's outer halo ring is drawn outside its frame (8pt on the regular bubble), and the design sheet's 30-34pt card tops left that ring touching the hang slot, overlapping it on the iPhone Home hero. The card's top padding is now derived from the slot's bottom edge + bare card stock (8pt, 6pt on the small pack) + the halo width, so changing either can't make them collide again; the slot moves up 2pt (13 -> 11). Cards grow 5-8pt at the top; on tight canvases the bubble yields that height. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Yeti becomes the app's only design on macOS and iOS, plus three fixes found on the Duo.
tabViewBottomAccessory.ThemeID/ThemeManager/Capabilitiesare deleted.Themeis now one token struct.Both schemes build warning-free under
SWIFT_STRICT_CONCURRENCY=complete, andAppleNugsTestspasses (330 tests). Both fixes are installed on the Duo.🤖 Generated with Claude Code