feat(phone): unify popups and add dynamic island

This commit is contained in:
Leon.Schmidt
2026-08-17 22:19:43 +02:00
parent fc3f5b1da2
commit 29fb234d5c
34 changed files with 1397 additions and 211 deletions
@@ -0,0 +1,87 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const source = readFileSync(
new URL('./PhoneDynamicIsland.vue', import.meta.url),
'utf8',
)
const appSource = readFileSync(new URL('../App.vue', import.meta.url), 'utf8')
const recorderSource = readFileSync(
new URL('./PhoneMemoRecorder.vue', import.meta.url),
'utf8',
)
const memosSource = readFileSync(
new URL('../views/apps/MemosApp.vue', import.meta.url),
'utf8',
)
const clockSource = readFileSync(
new URL('../views/apps/ClockApp.vue', import.meta.url),
'utf8',
)
describe('Phone Dynamic Island contract', () => {
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('<PhoneDynamicIsland v-if="!setupRequired" />')
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('<Transition name="phone-dynamic-island">')
expect(source).toContain(
'<Transition name="phone-dynamic-island-content" mode="out-in">',
)
expect(source).toContain(".phone-dynamic-island[data-expanded='true']")
expect(source).toContain('~ .phone-notification-provider')
expect(source).toContain('@media (prefers-reduced-motion: reduce)')
})
})
File diff suppressed because it is too large Load Diff
@@ -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') {