ENH - complete Radio Sky UI migration

This commit is contained in:
DerEchteAlec
2026-08-13 20:13:06 +02:00
parent 0437fdfe4e
commit 9f1412d659
2 changed files with 146 additions and 23 deletions
@@ -0,0 +1,91 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const source = readFileSync(new URL('./RadioApp.vue', import.meta.url), 'utf8')
const navigationSource = source.slice(
source.indexOf('<SkyPillNavigation'),
source.indexOf('</SkyPillNavigation>') + '</SkyPillNavigation>'.length,
)
describe('RadioApp Sky UI contract', () => {
it('uses only the first-party Sky UI surface', () => {
expect(source).not.toContain("from 'konsta/vue'")
expect(source).not.toMatch(/<\/?k-[a-z]/)
expect(source).toContain('<SkyAppPage')
expect(source).toContain('<SkyNavbar')
expect(source).toContain('<SkyPillNavigation')
expect(source.match(/<SkyScrollArea\b/g)).toHaveLength(1)
expect(source).toContain('<SkyStatusCard')
expect(source).toContain('<SkyField')
expect(source).toContain('<SkyRange')
expect(source).toContain('<SkySettingsGroup')
expect(source).toContain('<SkySettingsRow')
expect(source).toContain('<SkyToast')
})
it('renders Radio and Settings in full-width bottom Glass navigation', () => {
expect(navigationSource).toContain('layout="full"')
expect(navigationSource).toContain('<SkySegmented')
expect(navigationSource).toContain('navigation')
expect(navigationSource).toContain('rounded')
expect(navigationSource).toContain('strong')
expect(navigationSource).toContain(
':active-index="tab === \'radio\' ? 0 : 1"',
)
expect(navigationSource).toContain(':data-active-tab="tab"')
expect(navigationSource).toContain(':item-count="2"')
expect(navigationSource.match(/<SkySegmentedButton\b/g)).toHaveLength(2)
expect(navigationSource).toContain("tab = 'radio'")
expect(navigationSource).toContain("tab = 'settings'")
expect(navigationSource).toContain(
':aria-label="phone.t(\'Apps.radio.tabs.radio\')"',
)
expect(navigationSource).toContain(
':aria-label="phone.t(\'Apps.radio.tabs.settings\')"',
)
expect(navigationSource.match(/:size="20"/g)).toHaveLength(2)
expect(navigationSource).not.toContain('compact')
expect(source).toContain(
'<SkyScrollArea class="radio-content" with-tabbar>',
)
expect(source.indexOf('</SkyScrollArea>')).toBeLessThan(
source.indexOf('<SkyPillNavigation'),
)
expect(source).not.toMatch(/\.radio-content\s*\{[^}]*padding-bottom\s*:/s)
expect(source).not.toContain('<template #subnavbar>')
expect(source).not.toContain('radio-tab-switcher')
expect(source).toMatch(
/\.radio-navigation__item > span:last-child\s*\{[^}]*text-overflow:\s*ellipsis[^}]*white-space:\s*nowrap/s,
)
})
it('keeps the Radio actions, hydration and trusted updates wired', () => {
expect(source).toContain('await radio.load()')
expect(source).toContain('await radio.connect(primary, secondary)')
expect(source).toContain('await radio.disconnect()')
expect(source).toContain('void radio.setVolume(volumeInput.value)')
expect(source).toContain("radio.saveSetting('autoRejoin', $event)")
expect(source).toContain("radio.saveSetting('notifications', $event)")
expect(source).toContain('await radio.saveDisplayName(displayName)')
expect(source).toContain('await radio.saveBadge(badge)')
expect(source).toContain('isTrustedRootMessageSource(event.source, window)')
expect(source).toContain("event.data?.type === 'radio:updated'")
expect(source).toContain('radio.updateMembers(event.data.data.members)')
expect(source).toContain("window.addEventListener('message', onMessage)")
expect(source).toContain("window.removeEventListener('message', onMessage)")
expect(source).toContain("value.replace(',', '.')")
expect(source).toContain('connectHistory(entry)')
})
it('preserves theme, labels, loading, empty, error and feedback states', () => {
expect(source).toContain(':dark="phone.isDarkMode"')
expect(source).toContain(':label="phone.t(\'Apps.radio.name\')"')
expect(source).toContain('radio.isLoading && !radio.data.provider')
expect(source).toContain('<span aria-hidden="true">')
expect(source).toContain('<SkyEmptyState')
expect(source).toContain('role="alert"')
expect(source).toContain(':opened="Boolean(feedback)"')
expect(source).toContain('vertical-position="center"')
})
})
+55 -23
View File
@@ -20,6 +20,7 @@ import {
SkyList,
SkyListItem,
SkyNavbar,
SkyPillNavigation,
SkyRange,
SkyScrollArea,
SkySection,
@@ -228,26 +229,10 @@ onBeforeUnmount(() => {
>
<SkyNavbar :title="phone.t('Apps.radio.name')" />
<div class="radio-tab-switcher">
<SkySegmented class="radio-tabs" :aria-label="phone.t('Apps.radio.name')">
<SkySegmentedButton :active="tab === 'radio'" @click="tab = 'radio'">
<RadioTower :size="16" aria-hidden="true" />
{{ phone.t('Apps.radio.tabs.radio') }}
</SkySegmentedButton>
<SkySegmentedButton
:active="tab === 'settings'"
@click="tab = 'settings'"
>
<Settings :size="16" aria-hidden="true" />
{{ phone.t('Apps.radio.tabs.settings') }}
</SkySegmentedButton>
</SkySegmented>
</div>
<SkyScrollArea class="radio-content">
<SkyScrollArea class="radio-content" with-tabbar>
<div v-if="radio.isLoading && !radio.data.provider" class="radio-loading">
<SkySpinner :label="phone.t('Common.loading')" />
<span>{{ phone.t('Common.loading') }}</span>
<span aria-hidden="true">{{ phone.t('Common.loading') }}</span>
</div>
<template v-else-if="tab === 'radio'">
@@ -453,6 +438,45 @@ onBeforeUnmount(() => {
</template>
</SkyScrollArea>
<SkyPillNavigation
class="radio-navigation"
layout="full"
:label="phone.t('Apps.radio.name')"
>
<SkySegmented
navigation
rounded
strong
:active-index="tab === 'radio' ? 0 : 1"
:aria-label="phone.t('Apps.radio.name')"
:data-active-tab="tab"
:item-count="2"
>
<SkySegmentedButton
:active="tab === 'radio'"
:aria-label="phone.t('Apps.radio.tabs.radio')"
type="button"
@click="tab = 'radio'"
>
<span class="radio-navigation__item">
<RadioTower :size="20" aria-hidden="true" />
<span>{{ phone.t('Apps.radio.tabs.radio') }}</span>
</span>
</SkySegmentedButton>
<SkySegmentedButton
:active="tab === 'settings'"
:aria-label="phone.t('Apps.radio.tabs.settings')"
type="button"
@click="tab = 'settings'"
>
<span class="radio-navigation__item">
<Settings :size="20" aria-hidden="true" />
<span>{{ phone.t('Apps.radio.tabs.settings') }}</span>
</span>
</SkySegmentedButton>
</SkySegmented>
</SkyPillNavigation>
<SkyToast
:opened="Boolean(feedback)"
position="center"
@@ -465,13 +489,21 @@ onBeforeUnmount(() => {
</template>
<style scoped>
.radio-tab-switcher {
padding: 4px var(--sky-page-gutter) 8px;
flex: none;
.radio-navigation__item {
min-width: 0;
max-width: 100%;
display: flex;
align-items: center;
flex-direction: column;
gap: 2px;
line-height: 1;
}
.radio-tabs :deep(.sky-segmented-button) {
gap: 6px;
.radio-navigation__item > span:last-child {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.radio-content {