From 29fb234d5cbe47611d16c74599c6eeb91729108f Mon Sep 17 00:00:00 2001 From: "Leon.Schmidt" Date: Mon, 17 Aug 2026 22:19:43 +0200 Subject: [PATCH 1/2] feat(phone): unify popups and add dynamic island --- frontend/src/App.vue | 20 +- .../PhoneDynamicIsland.contract.test.ts | 87 ++ .../src/components/PhoneDynamicIsland.vue | 1192 +++++++++++++++++ frontend/src/components/PhoneMemoRecorder.vue | 2 + .../views/apps/BankingApp.contract.test.ts | 2 +- frontend/src/views/apps/BankingApp.vue | 11 +- .../views/apps/BillingApp.contract.test.ts | 2 +- frontend/src/views/apps/BillingApp.vue | 12 +- frontend/src/views/apps/CityMarktApp.vue | 34 +- frontend/src/views/apps/ClockApp.vue | 6 +- frontend/src/views/apps/CompaniesApp.vue | 11 +- frontend/src/views/apps/CrewLinkApp.vue | 6 +- frontend/src/views/apps/DarkChatApp.vue | 15 +- frontend/src/views/apps/FeatherApp.vue | 4 +- frontend/src/views/apps/FlareApp.vue | 6 +- frontend/src/views/apps/FlipTokApp.vue | 10 +- frontend/src/views/apps/GalleryApp.vue | 10 +- frontend/src/views/apps/GarageApp.vue | 11 +- frontend/src/views/apps/HouseApp.vue | 11 +- frontend/src/views/apps/LocalPagesApp.vue | 34 +- frontend/src/views/apps/MailApp.vue | 10 +- frontend/src/views/apps/MapApp.vue | 6 +- frontend/src/views/apps/MemosApp.vue | 13 +- .../views/apps/MessagesApp.contract.test.ts | 7 +- frontend/src/views/apps/MessagesApp.vue | 10 +- frontend/src/views/apps/MusicApp.vue | 6 +- frontend/src/views/apps/PicstagramApp.vue | 9 +- .../src/views/apps/RadioApp.contract.test.ts | 5 +- frontend/src/views/apps/RadioApp.vue | 11 +- frontend/src/views/apps/SettingsApp.vue | 11 +- frontend/src/views/apps/SkyRideApp.vue | 6 +- .../src/views/apps/SkyUiApps.contract.test.ts | 7 + frontend/src/views/apps/WeatherApp.vue | 11 +- frontend/src/views/apps/WeazelNewsApp.vue | 10 +- 34 files changed, 1397 insertions(+), 211 deletions(-) create mode 100644 frontend/src/components/PhoneDynamicIsland.contract.test.ts create mode 100644 frontend/src/components/PhoneDynamicIsland.vue diff --git a/frontend/src/App.vue b/frontend/src/App.vue index f973a80..00039f7 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -13,6 +13,7 @@ import { useRoute, useRouter } from 'vue-router' import { SkyProvider } from '@/ui' import PhoneHomeIndicator from '@/components/PhoneHomeIndicator.vue' import PhoneControlCenter from '@/components/PhoneControlCenter.vue' +import PhoneDynamicIsland from '@/components/PhoneDynamicIsland.vue' import PhoneMediaCapture from '@/components/PhoneMediaCapture.vue' import PhoneMemoRecorder from '@/components/PhoneMemoRecorder.vue' import PhoneLockScreen from '@/components/PhoneLockScreen.vue' @@ -316,9 +317,6 @@ const DARK_STATUS_BAR_APP_IDS = new Set([ const isDevelopmentRoute = computed( () => isDevelopment && route.name === 'development-sky-ui', ) -const showActiveCallReturn = computed( - () => Boolean(calls.activeCall) && route.params.appId !== 'phone', -) const appTransitionName = computed(() => route.query.transition === 'app-switch' ? 'app-switch' : 'app-window', ) @@ -1084,7 +1082,6 @@ function onMessage(event: MessageEvent): void { isUnlocking.value = false loadUnlockedPhoneData() } - window.setTimeout(() => void router.push('/apps/phone'), 0) } else if (event.data?.type === 'sim:picker' && event.data.data) { simPicker.value = event.data.data as unknown as SimPickerPayload } else if (event.data?.type === 'sim:picker-close') { @@ -1330,12 +1327,6 @@ function toggleHardwareAlertMute(): void { showHardwareVolumeHud() } -function returnToActiveCall(): void { - if (!calls.activeCall) return - controlCenterOpened.value = false - void router.push('/apps/phone') -} - function unlockCamera(): void { if (phone.security.enabled) { pendingUnlockRoute.value = '/apps/camera' @@ -1414,9 +1405,7 @@ onMounted(() => { window.setTimeout(() => { notifications.show({ appId: 'messages', - persistent: !developmentParameters.has( - 'mutedNotificationPreview', - ), + persistent: !developmentParameters.has('mutedNotificationPreview'), route: '/apps/messages', text: 'You still got that spare alternator?', title: 'Tommy V', @@ -1570,7 +1559,7 @@ onBeforeUnmount(() => { @open="openNotificationPreview" />
@@ -1682,14 +1671,13 @@ onBeforeUnmount(() => { > + { + it('renders once at phone shell level instead of forcing calls into Phone', () => { + expect(appSource).toContain( + "import PhoneDynamicIsland from '@/components/PhoneDynamicIsland.vue'", + ) + expect(appSource).toContain('') + expect(appSource).toContain( + 'phone.isOpen || notifications.current || calls.activeCall', + ) + expect(appSource).not.toContain( + "window.setTimeout(() => void router.push('/apps/phone'), 0)", + ) + }) + + it('prioritizes calls and exposes answer, decline, and hangup controls', () => { + expect(source.indexOf("return 'incoming-call'")).toBeLessThan( + source.indexOf("return 'recording'"), + ) + expect(source).toContain('@click.stop="calls.answer()"') + expect(source).toContain('@click.stop="endCall"') + expect(source).toContain('void calls.decline()') + expect(source).toContain('void calls.hangup()') + expect(source).toContain('call.answeredAt ?? call.startedAt') + }) + + it('connects music, recorder, timer, and stopwatch controls to their stores', () => { + expect(source).toContain('@click.stop="music.previous()"') + expect(source).toContain('@click.stop="music.toggle()"') + expect(source).toContain('@click.stop="music.next()"') + expect(source).toContain("postRecorderCommand('memo:recordStop')") + expect(source).toContain('clock.pauseTimer(Date.now())') + expect(source).toContain('clock.pauseStopwatch(Date.now())') + expect(source).toContain('clock.addLap(Date.now())') + }) + + it('keeps recorder state available across app changes', () => { + expect(recorderSource).toContain( + "message.type === 'memo:recordStateRequest'", + ) + expect(memosSource).toContain( + "postRecorderCommand('memo:recordStateRequest')", + ) + expect(memosSource).not.toContain( + "if (recordingActive.value) postRecorderCommand('memo:recordCancel')", + ) + }) + + it('opens both clock live activities on the correct clock tab', () => { + expect(source).toContain("'/apps/clock?section=timer'") + expect(source).toContain("'/apps/clock?section=stopwatch'") + expect(clockSource).toContain( + "section === 'timer' || section === 'stopwatch'", + ) + }) + + it('animates state changes and moves popup notifications below expanded UI', () => { + expect(source).toContain('') + expect(source).toContain( + '', + ) + expect(source).toContain(".phone-dynamic-island[data-expanded='true']") + expect(source).toContain('~ .phone-notification-provider') + expect(source).toContain('@media (prefers-reduced-motion: reduce)') + }) +}) diff --git a/frontend/src/components/PhoneDynamicIsland.vue b/frontend/src/components/PhoneDynamicIsland.vue new file mode 100644 index 0000000..2293ab0 --- /dev/null +++ b/frontend/src/components/PhoneDynamicIsland.vue @@ -0,0 +1,1192 @@ + + + + + + + diff --git a/frontend/src/components/PhoneMemoRecorder.vue b/frontend/src/components/PhoneMemoRecorder.vue index 7784cc0..67bb605 100644 --- a/frontend/src/components/PhoneMemoRecorder.vue +++ b/frontend/src/components/PhoneMemoRecorder.vue @@ -557,6 +557,8 @@ function onMessage(event: MessageEvent): void { void stopRecording(data) } else if (message.type === 'memo:recordCancel') { cancelRecording() + } else if (message.type === 'memo:recordStateRequest') { + postRecorderState(currentState) } else if (message.type === 'memos:uploadReady') { void uploadReady(data as MemoUploadReady) } else if (message.type === 'memos:uploadResult') { diff --git a/frontend/src/views/apps/BankingApp.contract.test.ts b/frontend/src/views/apps/BankingApp.contract.test.ts index 657fb26..a60a3d2 100644 --- a/frontend/src/views/apps/BankingApp.contract.test.ts +++ b/frontend/src/views/apps/BankingApp.contract.test.ts @@ -42,7 +42,7 @@ describe('Banking app Sky UI migration', () => { 'SkyButton', 'SkySpinner', 'SkyEmptyState', - 'SkyToast', + 'SkyNotification', ]) { expect(source).toContain(`<${component}`) } diff --git a/frontend/src/views/apps/BankingApp.vue b/frontend/src/views/apps/BankingApp.vue index 88ecd45..f4c4a09 100644 --- a/frontend/src/views/apps/BankingApp.vue +++ b/frontend/src/views/apps/BankingApp.vue @@ -44,7 +44,7 @@ import { SkySpinner, SkyTabBar, SkyTabButton, - SkyToast, + SkyNotification, } from '@/ui' type BankingTab = 'home' | 'activity' @@ -696,13 +696,10 @@ onBeforeUnmount(() => { - - {{ errorMessage('reload_cooldown') }} - + /> diff --git a/frontend/src/views/apps/BillingApp.contract.test.ts b/frontend/src/views/apps/BillingApp.contract.test.ts index ecb792d..74d5ca1 100644 --- a/frontend/src/views/apps/BillingApp.contract.test.ts +++ b/frontend/src/views/apps/BillingApp.contract.test.ts @@ -29,7 +29,7 @@ describe('Billing app Sky UI migration', () => { 'SkyTabBar', 'SkyTabButton', 'SkySheet', - 'SkyToast', + 'SkyNotification', ]) { expect(source).toContain(`<${component}`) } diff --git a/frontend/src/views/apps/BillingApp.vue b/frontend/src/views/apps/BillingApp.vue index d382f5a..26e67e0 100644 --- a/frontend/src/views/apps/BillingApp.vue +++ b/frontend/src/views/apps/BillingApp.vue @@ -47,7 +47,7 @@ import { SkySpinner, SkyTabBar, SkyTabButton, - SkyToast, + SkyNotification, } from '@/ui' import { isTrustedRootMessageSource } from '@/utils/windowMessages' @@ -771,9 +771,11 @@ onBeforeUnmount(() => { - - {{ toastText }} - + @@ -1523,7 +1525,7 @@ onBeforeUnmount(() => { var(--billing-panel); } } -.billing-toast { +.billing-notification { z-index: 50; } diff --git a/frontend/src/views/apps/CityMarktApp.vue b/frontend/src/views/apps/CityMarktApp.vue index 887a8ea..46af0a7 100644 --- a/frontend/src/views/apps/CityMarktApp.vue +++ b/frontend/src/views/apps/CityMarktApp.vue @@ -38,6 +38,7 @@ import { SkyButton, SkyGlass, SkyIcon, + SkyNotification, SkyNavbar, SkyAppPage, SkySearchbar, @@ -2113,11 +2114,7 @@ onMounted(async () => { app-id="citymarkt" :app-name="phone.t('Apps.citymarkt.name')" /> -
- {{ feedback }} -
+ @@ -3014,30 +3011,6 @@ onMounted(async () => { background: var(--panel); color: #ff796f; } -.citymarkt__toast { - position: absolute; - z-index: 20; - right: 18px; - bottom: 92px; - left: 18px; - padding: 10px 12px; - border-radius: 11px; - background: #f4f4ee; - color: #171816; - box-shadow: 0 8px 30px #0007; - font-size: 10px; - font-weight: 800; - text-align: center; -} -.toast-enter-active, -.toast-leave-active { - transition: 0.2s; -} -.toast-enter-from, -.toast-leave-to { - transform: translateY(8px); - opacity: 0; -} .citymarkt { --color-primary: var(--yellow); position: relative; @@ -3740,8 +3713,7 @@ onMounted(async () => { font-size: 10.5px; } .citymarkt__meta, -.citymarkt__inline-auth, -.citymarkt__toast { +.citymarkt__inline-auth { font-size: 12px; } .citymarkt__composer textarea, diff --git a/frontend/src/views/apps/ClockApp.vue b/frontend/src/views/apps/ClockApp.vue index d832f04..54e3334 100644 --- a/frontend/src/views/apps/ClockApp.vue +++ b/frontend/src/views/apps/ClockApp.vue @@ -47,7 +47,9 @@ const phone = usePhoneStore() const clock = useClockStore() const route = useRoute() const tab = ref<'world' | 'alarm' | 'stopwatch' | 'timer'>( - route.query.section === 'timer' ? 'timer' : 'world', + route.query.section === 'timer' || route.query.section === 'stopwatch' + ? route.query.section + : 'world', ) const alarmEditor = ref< { mode: 'create' } | { id: string; mode: 'edit' } | null @@ -205,7 +207,7 @@ watch([() => clock.stopwatchStartedAt, tab], ([startedAt, activeTab]) => { watch( () => route.query.section, (section) => { - if (section === 'timer') tab.value = 'timer' + if (section === 'timer' || section === 'stopwatch') tab.value = section }, ) diff --git a/frontend/src/views/apps/CompaniesApp.vue b/frontend/src/views/apps/CompaniesApp.vue index 45d2273..533ebd3 100644 --- a/frontend/src/views/apps/CompaniesApp.vue +++ b/frontend/src/views/apps/CompaniesApp.vue @@ -37,7 +37,7 @@ import { SkyScrollRail, SkySection, SkySheet, - SkyToast, + SkyNotification, SkyToggle, SkyToolbarPane, } from '@/ui' @@ -2599,14 +2599,11 @@ onBeforeUnmount(() => { - - {{ toastText }} - + /> diff --git a/frontend/src/views/apps/CrewLinkApp.vue b/frontend/src/views/apps/CrewLinkApp.vue index 1c8e43f..3cb9f96 100644 --- a/frontend/src/views/apps/CrewLinkApp.vue +++ b/frontend/src/views/apps/CrewLinkApp.vue @@ -17,7 +17,7 @@ import { SkySheet, SkyTabBar, SkyTabButton, - SkyToast, + SkyNotification, SkyToggle, SkyToolbarPane, } from '@/ui' @@ -2352,9 +2352,7 @@ onBeforeUnmount(() => { > - {{ - toastText - }} + diff --git a/frontend/src/views/apps/DarkChatApp.vue b/frontend/src/views/apps/DarkChatApp.vue index 2f81bc7..88e1e59 100644 --- a/frontend/src/views/apps/DarkChatApp.vue +++ b/frontend/src/views/apps/DarkChatApp.vue @@ -72,7 +72,7 @@ import { SkySettingsRow, SkySheet, SkySpinner, - SkyToast, + SkyNotification, } from '@/ui' const VOICE_MAX_DURATION_MS = 60_000 @@ -1896,7 +1896,7 @@ onBeforeUnmount(() => { margin: 12px 0 8px; } -.dc-toast { +.dc-notification { z-index: 150; } @@ -2852,13 +2852,10 @@ onBeforeUnmount(() => { - - {{ toast }} - + :text="toast" + class="dc-notification" + /> diff --git a/frontend/src/views/apps/FeatherApp.vue b/frontend/src/views/apps/FeatherApp.vue index 5bd5fd2..29700f0 100644 --- a/frontend/src/views/apps/FeatherApp.vue +++ b/frontend/src/views/apps/FeatherApp.vue @@ -56,7 +56,7 @@ import { SkySegmented, SkySegmentedButton, SkySheet, - SkyToast, + SkyNotification, SkyToggle, SkyMessagebar, } from '@/ui' @@ -2334,7 +2334,7 @@ onMounted(async () => {
- {{ feedback }} + diff --git a/frontend/src/views/apps/FlareApp.vue b/frontend/src/views/apps/FlareApp.vue index 954d82d..0fc5998 100644 --- a/frontend/src/views/apps/FlareApp.vue +++ b/frontend/src/views/apps/FlareApp.vue @@ -26,7 +26,7 @@ import { SkyTabBar, SkyTabButton, SkyToolbarPane, - SkyToast, + SkyNotification, SkyToggle, } from '@/ui' import { @@ -2237,9 +2237,7 @@ onBeforeUnmount(() => { - - {{ actionToast }} - + diff --git a/frontend/src/views/apps/FlipTokApp.vue b/frontend/src/views/apps/FlipTokApp.vue index 90539ea..7432bec 100644 --- a/frontend/src/views/apps/FlipTokApp.vue +++ b/frontend/src/views/apps/FlipTokApp.vue @@ -73,7 +73,7 @@ import { SkySegmentedButton, SkySheet, SkySpinner, - SkyToast, + SkyNotification, SkyToggle, } from '@/ui' import { nuiCall } from '@/utils/nui' @@ -3361,13 +3361,11 @@ onBeforeUnmount(() => { - - {{ feedback }} - + /> diff --git a/frontend/src/views/apps/GalleryApp.vue b/frontend/src/views/apps/GalleryApp.vue index f272df7..4cf8e24 100644 --- a/frontend/src/views/apps/GalleryApp.vue +++ b/frontend/src/views/apps/GalleryApp.vue @@ -11,7 +11,7 @@ import { SkySpinner, SkySegmented, SkySegmentedButton, - SkyToast, + SkyNotification, } from '@/ui' import { ChevronLeft, @@ -1625,13 +1625,11 @@ onBeforeUnmount(() => { - - {{ toastText }} - + />