diff --git a/frontend/src/views/apps/MusicApp.navigation.contract.test.ts b/frontend/src/views/apps/MusicApp.navigation.contract.test.ts
new file mode 100644
index 0000000..98016b5
--- /dev/null
+++ b/frontend/src/views/apps/MusicApp.navigation.contract.test.ts
@@ -0,0 +1,73 @@
+import { readFileSync } from 'node:fs'
+
+import { describe, expect, it } from 'vitest'
+
+const source = readFileSync(new URL('./MusicApp.vue', import.meta.url), 'utf8')
+const navigationSource = source.slice(
+ source.indexOf('') + ''.length,
+)
+
+describe('MusicApp Sky pill navigation contract', () => {
+ it('uses the full-width sliding Glass navigation outside playlists', () => {
+ expect(source).not.toContain('kTabbar')
+ expect(source).not.toContain('kTabbarLink')
+ expect(source).not.toContain('kToolbarPane')
+ expect(source).not.toContain(' {
+ expect(source).toContain("{ id: 'library', icon: Library }")
+ expect(source).toContain("{ id: 'playlists', icon: ListMusic }")
+ expect(source).toContain("{ id: 'search', icon: Search }")
+ expect(navigationSource).toContain(':active="activeTab === item.id"')
+ expect(navigationSource).toContain(
+ ':aria-label="phone.t(`Apps.music.tabs.${item.id}`)"',
+ )
+ expect(navigationSource).toContain('@click="selectTab(item.id)"')
+ expect(navigationSource).toContain(':size="20"')
+ expect(source).toMatch(
+ /function selectTab\(tab: MusicTab\): void \{\s+activeTab\.value = tab\s+activePlaylist\.value = null\s+scrollToTop\(\)\s+\}/,
+ )
+ })
+
+ it('reserves the absolute navigation and mini-player without changing playlist space', () => {
+ expect(source).toMatch(
+ /\.music-scroll\s*\{[^}]*padding:\s*8px 0\s*calc\(\s*var\(--sky-safe-area-bottom\) \+ var\(--sky-tabbar-height\) \+\s*var\(--sky-space-3\)\s*\)/s,
+ )
+ expect(source).toMatch(
+ /\.music-app--playing \.music-scroll\s*\{[^}]*padding-bottom:\s*calc\(\s*var\(--music-mini-player-bottom\) \+ var\(--music-mini-player-height\) \+\s*var\(--sky-space-3\)\s*\)/s,
+ )
+ expect(source).toContain(
+ 'var(--sky-safe-area-bottom) + var(--sky-tabbar-height) + var(--sky-space-2)',
+ )
+ expect(source).toContain('--music-mini-player-height: 58px')
+ expect(source).toMatch(
+ /\.music-app--playlist \.music-scroll\s*\{[^}]*padding-bottom:\s*42px/s,
+ )
+ expect(source).toMatch(
+ /\.music-app--playlist\.music-app--playing \.music-scroll\s*\{[^}]*padding-bottom:\s*100px/s,
+ )
+ })
+
+ it('bridges the Music accent and light-dark theme into Sky UI', () => {
+ expect(source).toContain('class="music-app sky-ui-provider"')
+ expect(source).toContain("'sky-ui-provider--dark': phone.isDarkMode")
+ expect(source).toContain('--sky-app-accent: var(--music-accent)')
+ expect(source).toContain('class="music-navigation__item"')
+ expect(source).toMatch(
+ /\.music-navigation__item > span:last-child\s*\{[^}]*text-overflow:\s*ellipsis[^}]*white-space:\s*nowrap/s,
+ )
+ expect(source).not.toContain('.music-tabbar')
+ })
+})
diff --git a/frontend/src/views/apps/MusicApp.vue b/frontend/src/views/apps/MusicApp.vue
index 5de0de9..0ca455a 100644
--- a/frontend/src/views/apps/MusicApp.vue
+++ b/frontend/src/views/apps/MusicApp.vue
@@ -6,7 +6,6 @@ import {
kDialog,
kDialogButton,
kGlass,
- kIcon,
kLink,
kList,
kListButton,
@@ -19,10 +18,7 @@ import {
kRange,
kSearchbar,
kSheet,
- kTabbar,
- kTabbarLink,
kToast,
- kToolbarPane,
} from 'konsta/vue'
import {
Check,
@@ -55,6 +51,7 @@ import type { MusicPlaylist, MusicTrack } from '@/types/music'
import { easyShareMusicTarget } from '@/utils/easyshare'
import { consumeEscape, handleEnterAction } from '@/utils/keyboard'
import { musicEscapeLayer } from '@/utils/musicEscape'
+import { SkyPillNavigation, SkySegmented, SkySegmentedButton } from '@/ui'
type MusicTab = 'library' | 'playlists' | 'search'
type MusicSheet =
@@ -75,6 +72,14 @@ const easyShare = useEasyShareStore()
const phone = usePhoneStore()
const route = useRoute()
const activeTab = ref('library')
+const tabs = [
+ { id: 'library', icon: Library },
+ { id: 'playlists', icon: ListMusic },
+ { id: 'search', icon: Search },
+] as const
+const activeTabIndex = computed(() =>
+ tabs.findIndex((item) => item.id === activeTab.value),
+)
const activePlaylist = ref(null)
const addMenuOpened = ref(false)
const actionMenuOpened = ref(false)
@@ -566,10 +571,11 @@ onBeforeUnmount(() => {
{
-
-
-
+
-
- {{ phone.t('Apps.music.tabs.library') }}
-
-
-
- {{ phone.t('Apps.music.tabs.playlists') }}
-
-
-
- {{ phone.t('Apps.music.tabs.search') }}
-
-
-
+
+
+ {{ phone.t(`Apps.music.tabs.${item.id}`) }}
+
+
+
+