Skip to content

Commit a5888ef

Browse files
committed
liquid glass improvements
Signed-off-by: Sertac Ozercan <sozercan@gmail.com>
1 parent 249a1aa commit a5888ef

File tree

8 files changed

+120
-49
lines changed

8 files changed

+120
-49
lines changed

AGENTS.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,37 @@ HStack { /* controls */ }
175175
GlassEffectContainer(spacing: 0) {
176176
// Glass elements here
177177
}
178+
179+
// Materialize transition for appearing panels
180+
QueueView()
181+
.glassEffectTransition(.materialize)
182+
183+
// Glass effect ID for morphing support
184+
.glassEffectID("queue", in: playerNamespace)
185+
186+
// Glass search field
187+
TextField("Search...", text: $query)
188+
.glassEffect(.regular, in: .capsule)
189+
```
190+
191+
**Liquid Glass Anti-Patterns** (avoid these):
192+
-`.buttonStyle(.glass)` on buttons inside a glass container (causes rectangles)
193+
-`glassEffectUnion` on buttons already in a glass capsule (glass-on-glass)
194+
- ❌ Glass effects on content areas (lists, tables, media)
195+
- ❌ Custom opacity that bypasses accessibility
196+
197+
**Implemented Glass Effects**:
198+
- `PlayerBar` — glass capsule with namespace for morphing
199+
- `Sidebar` — wrapped in `GlassEffectContainer`
200+
- `QueueView` / `LyricsView``.glassEffectTransition(.materialize)`
201+
- `SearchView` — glass search field and suggestions dropdown
178202
```
179203
180204
**PlayerBar Pattern**:
181205
- Each view that can be navigated to must include the `PlayerBar` via `safeAreaInset`
182206
- The `PlayerBar` floats at the bottom of the content area (not sidebar)
183207
- Uses `.glassEffect(.regular.interactive(), in: .capsule)` for the liquid glass look
208+
- Uses `@Namespace` for glass effect morphing support
184209
185210
```swift
186211
// Add to every navigable view

Views/macOS/LyricsView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ struct LyricsView: View {
3333
}
3434
.frame(minWidth: 280, maxWidth: 280)
3535
.background(.background.opacity(0.95))
36+
.glassEffectTransition(.materialize)
3637
.onChange(of: self.playerService.currentTrack?.videoId) { _, newVideoId in
3738
if let videoId = newVideoId, videoId != lastLoadedVideoId {
3839
// Reset explanation when track changes

Views/macOS/MiniPlayerViews.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,18 @@ struct PersistentPlayerView: NSViewRepresentable {
6565
SingletonPlayerWebView.shared.loadVideo(videoId: self.videoId)
6666
}
6767
}
68+
69+
// MARK: - MiniPlayerToast
70+
71+
/// A small toast-style view that appears when mini player is shown.
72+
/// Uses Liquid Glass materialize transition for smooth appearance.
73+
@available(macOS 26.0, *)
74+
struct MiniPlayerToast: View {
75+
let videoId: String
76+
77+
var body: some View {
78+
PersistentPlayerView(videoId: self.videoId, isExpanded: true)
79+
.clipShape(RoundedRectangle(cornerRadius: 6))
80+
.glassEffectTransition(.materialize)
81+
}
82+
}

Views/macOS/PlayerBar.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ struct PlayerBar: View {
99
@Environment(PlayerService.self) private var playerService
1010
@Environment(WebKitManager.self) private var webKitManager
1111

12+
/// Namespace for glass effect morphing and unioning.
13+
@Namespace private var playerNamespace
14+
1215
@State private var isHovering = false
1316

1417
/// Local seek value for smooth slider dragging without network calls on every change.
@@ -39,6 +42,7 @@ struct PlayerBar: View {
3942
.padding(.vertical, 8)
4043
.frame(height: 52)
4144
.glassEffect(.regular.interactive(), in: .capsule)
45+
.glassEffectID("playerBar", in: self.playerNamespace)
4246
}
4347
.padding(.horizontal, 16)
4448
.padding(.bottom, 12)
@@ -255,6 +259,7 @@ struct PlayerBar: View {
255259
.contentTransition(.symbolEffect(.replace))
256260
}
257261
.buttonStyle(.pressable)
262+
.glassEffectID("playPause", in: self.playerNamespace)
258263
.accessibilityLabel(self.playerService.isPlaying ? "Pause" : "Play")
259264

260265
// Next
@@ -417,6 +422,7 @@ struct PlayerBar: View {
417422
.foregroundStyle(self.playerService.showLyrics ? .red : .primary.opacity(0.85))
418423
}
419424
.buttonStyle(.pressable)
425+
.glassEffectID("lyrics", in: self.playerNamespace)
420426
.accessibilityIdentifier(AccessibilityID.PlayerBar.lyricsButton)
421427
.accessibilityLabel("Lyrics")
422428
.accessibilityValue(self.playerService.showLyrics ? "Showing" : "Hidden")
@@ -434,6 +440,7 @@ struct PlayerBar: View {
434440
.foregroundStyle(self.playerService.showQueue ? .red : .primary.opacity(0.85))
435441
}
436442
.buttonStyle(.pressable)
443+
.glassEffectID("queue", in: self.playerNamespace)
437444
.accessibilityIdentifier(AccessibilityID.PlayerBar.queueButton)
438445
.accessibilityLabel("Queue")
439446
.accessibilityValue(self.playerService.showQueue ? "Showing" : "Hidden")

Views/macOS/QueueView.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct QueueView: View {
2020
}
2121
.frame(minWidth: 280, maxWidth: 280)
2222
.background(.background.opacity(0.95))
23+
.glassEffectTransition(.materialize)
2324
.accessibilityIdentifier(AccessibilityID.Queue.container)
2425
}
2526

Views/macOS/SearchView.swift

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,7 @@ struct SearchView: View {
137137
}
138138
}
139139
.padding(10)
140-
.background(.quaternary.opacity(0.5))
141-
.clipShape(.rect(cornerRadius: 8))
140+
.glassEffect(.regular, in: .capsule)
142141
}
143142

144143
private var suggestionsDropdown: some View {
@@ -151,8 +150,8 @@ struct SearchView: View {
151150
}
152151
}
153152
}
154-
.background(.ultraThinMaterial)
155-
.clipShape(.rect(cornerRadius: 8))
153+
.glassEffect(.regular, in: .rect(cornerRadius: 8))
154+
.glassEffectTransition(.materialize)
156155
.shadow(color: .black.opacity(0.15), radius: 8, x: 0, y: 4)
157156
}
158157

Views/macOS/Sidebar.swift

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,63 +5,68 @@ import SwiftUI
55
struct Sidebar: View {
66
@Binding var selection: NavigationItem?
77

8+
/// Namespace for glass effect morphing.
9+
@Namespace private var sidebarNamespace
10+
811
var body: some View {
9-
List(selection: self.$selection) {
10-
// Main navigation
11-
Section {
12-
NavigationLink(value: NavigationItem.search) {
13-
Label("Search", systemImage: "magnifyingglass")
14-
}
15-
.accessibilityIdentifier(AccessibilityID.Sidebar.searchItem)
12+
GlassEffectContainer(spacing: 0) {
13+
List(selection: self.$selection) {
14+
// Main navigation
15+
Section {
16+
NavigationLink(value: NavigationItem.search) {
17+
Label("Search", systemImage: "magnifyingglass")
18+
}
19+
.accessibilityIdentifier(AccessibilityID.Sidebar.searchItem)
1620

17-
NavigationLink(value: NavigationItem.home) {
18-
Label("Home", systemImage: "house")
21+
NavigationLink(value: NavigationItem.home) {
22+
Label("Home", systemImage: "house")
23+
}
24+
.accessibilityIdentifier(AccessibilityID.Sidebar.homeItem)
1925
}
20-
.accessibilityIdentifier(AccessibilityID.Sidebar.homeItem)
21-
}
2226

23-
// Discover section
24-
Section("Discover") {
25-
NavigationLink(value: NavigationItem.explore) {
26-
Label("Explore", systemImage: "globe")
27-
}
28-
.accessibilityIdentifier(AccessibilityID.Sidebar.exploreItem)
27+
// Discover section
28+
Section("Discover") {
29+
NavigationLink(value: NavigationItem.explore) {
30+
Label("Explore", systemImage: "globe")
31+
}
32+
.accessibilityIdentifier(AccessibilityID.Sidebar.exploreItem)
2933

30-
NavigationLink(value: NavigationItem.charts) {
31-
Label("Charts", systemImage: "chart.line.uptrend.xyaxis")
32-
}
33-
.accessibilityIdentifier(AccessibilityID.Sidebar.chartsItem)
34+
NavigationLink(value: NavigationItem.charts) {
35+
Label("Charts", systemImage: "chart.line.uptrend.xyaxis")
36+
}
37+
.accessibilityIdentifier(AccessibilityID.Sidebar.chartsItem)
3438

35-
NavigationLink(value: NavigationItem.moodsAndGenres) {
36-
Label("Moods & Genres", systemImage: "theatermask.and.paintbrush")
37-
}
38-
.accessibilityIdentifier(AccessibilityID.Sidebar.moodsAndGenresItem)
39+
NavigationLink(value: NavigationItem.moodsAndGenres) {
40+
Label("Moods & Genres", systemImage: "theatermask.and.paintbrush")
41+
}
42+
.accessibilityIdentifier(AccessibilityID.Sidebar.moodsAndGenresItem)
3943

40-
NavigationLink(value: NavigationItem.newReleases) {
41-
Label("New Releases", systemImage: "sparkles")
44+
NavigationLink(value: NavigationItem.newReleases) {
45+
Label("New Releases", systemImage: "sparkles")
46+
}
47+
.accessibilityIdentifier(AccessibilityID.Sidebar.newReleasesItem)
4248
}
43-
.accessibilityIdentifier(AccessibilityID.Sidebar.newReleasesItem)
44-
}
4549

46-
// Library section
47-
Section("Library") {
48-
NavigationLink(value: NavigationItem.likedMusic) {
49-
Label("Liked Music", systemImage: "heart.fill")
50-
}
51-
.accessibilityIdentifier(AccessibilityID.Sidebar.likedMusicItem)
50+
// Library section
51+
Section("Library") {
52+
NavigationLink(value: NavigationItem.likedMusic) {
53+
Label("Liked Music", systemImage: "heart.fill")
54+
}
55+
.accessibilityIdentifier(AccessibilityID.Sidebar.likedMusicItem)
5256

53-
NavigationLink(value: NavigationItem.library) {
54-
Label("Playlists", systemImage: "music.note.list")
57+
NavigationLink(value: NavigationItem.library) {
58+
Label("Playlists", systemImage: "music.note.list")
59+
}
60+
.accessibilityIdentifier(AccessibilityID.Sidebar.libraryItem)
5561
}
56-
.accessibilityIdentifier(AccessibilityID.Sidebar.libraryItem)
5762
}
58-
}
59-
.listStyle(.sidebar)
60-
.navigationSplitViewColumnWidth(min: 200, ideal: 220, max: 300)
61-
.accessibilityIdentifier(AccessibilityID.Sidebar.container)
62-
.onChange(of: self.selection) { _, newValue in
63-
if newValue != nil {
64-
HapticService.navigation()
63+
.listStyle(.sidebar)
64+
.navigationSplitViewColumnWidth(min: 200, ideal: 220, max: 300)
65+
.accessibilityIdentifier(AccessibilityID.Sidebar.container)
66+
.onChange(of: self.selection) { _, newValue in
67+
if newValue != nil {
68+
HapticService.navigation()
69+
}
6570
}
6671
}
6772
}

docs/architecture.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,24 @@ DiagnosticsLogger.auth.error("Cookie extraction failed")
416416

417417
The app uses Apple's **Liquid Glass** design language introduced in macOS 26.
418418

419+
### Glass Effect Patterns
420+
421+
| Component | Glass Pattern |
422+
|-----------|---------------|
423+
| `PlayerBar` | `.glassEffect(.regular.interactive(), in: .capsule)` |
424+
| `Sidebar` | Wrapped in `GlassEffectContainer` |
425+
| `QueueView` / `LyricsView` | `.glassEffectTransition(.materialize)` |
426+
| Search field | `.glassEffect(.regular, in: .capsule)` |
427+
| Search suggestions | `.glassEffect(.regular, in: .rect(cornerRadius: 8))` |
428+
429+
### Glass Effect Best Practices
430+
431+
1. **Use `GlassEffectContainer`** to wrap multiple glass elements
432+
2. **Use `.glassEffectTransition(.materialize)`** for panels that appear/disappear
433+
3. **Use `@Namespace` + `.glassEffectID()`** for morphing between states
434+
4. **Avoid glass-on-glass** — don't apply `.buttonStyle(.glass)` to buttons already inside a glass container
435+
5. **Reserve glass for navigation/floating controls** — not for content areas
436+
419437
## Foundation Models (Apple Intelligence)
420438

421439
Kaset integrates Apple's on-device Foundation Models framework for AI-powered features. See [ADR-0005: Foundation Models Architecture](adr/0005-foundation-models-architecture.md) for detailed design decisions.

0 commit comments

Comments
 (0)