ENH - polish core phone app interfaces

Adds an active-call Dynamic Island and refines the Phone, Health, Notes, and Companies experiences with shared Sky UI behavior, improved keyboard interaction, and localized note deletion confirmation. Updates the relevant contract and component tests.
This commit is contained in:
Type
2026-08-16 23:18:00 +02:00
parent e4fc873bf0
commit e9cd220971
17 changed files with 486 additions and 210 deletions
@@ -21,6 +21,8 @@ describe('NotesRichTextEditor formatting tabbar', () => {
expect(source).toContain('@click="formatMode = true"')
expect(source).toContain('@click="formatMode = false"')
expect(source).toContain(':disabled="editor.state.selection.empty"')
expect(source).toContain('class="notes-rich-editor sky-ui-provider"')
expect(source).toContain("'sky-ui-provider--dark': dark")
expect(source).not.toContain('notes-rich-editor__toolbar-row')
expect(source).not.toContain('scrollToolbar')
})
@@ -263,8 +263,11 @@ onBeforeUnmount(() => editor.value?.destroy())
<template>
<section
class="notes-rich-editor"
:class="{ 'notes-rich-editor--dark': dark }"
class="notes-rich-editor sky-ui-provider"
:class="{
'notes-rich-editor--dark': dark,
'sky-ui-provider--dark': dark,
}"
>
<EditorContent
v-if="editor"
@@ -0,0 +1,24 @@
import { readFileSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
const component = readFileSync(
new URL('./PhoneDynamicIsland.vue', import.meta.url),
'utf8',
)
const app = readFileSync(new URL('../App.vue', import.meta.url), 'utf8')
describe('PhoneDynamicIsland contract', () => {
it('renders only a ringing incoming call with Sky actions', () => {
expect(component).toContain("call?.direction === 'incoming'")
expect(component).toContain("call.state === 'ringing'")
expect(component).toContain('<SkyButton')
expect(component).toContain("await calls.answer()")
expect(component).toContain("await calls.decline()")
})
it('keeps incoming calls in the current app until accepted', () => {
expect(app).toContain("call.state !== 'ringing'")
expect(app).toContain("call.direction !== 'incoming'")
expect(app).toContain('<PhoneDynamicIsland @accepted="openAcceptedCall" />')
})
})
@@ -0,0 +1,81 @@
<script setup lang="ts">
import { Phone, PhoneOff } from 'lucide-vue-next'
import { computed, ref } from 'vue'
import { useCallsStore } from '@/stores/calls'
import { usePhoneStore } from '@/stores/phone'
import { SkyButton } from '@/ui'
const emit = defineEmits<{ accepted: [] }>()
const calls = useCallsStore()
const phone = usePhoneStore()
const pendingAction = ref<'answer' | 'decline' | null>(null)
const incomingCall = computed(() => {
const call = calls.activeCall
return call?.direction === 'incoming' && call.state === 'ringing'
? call
: null
})
const callerLabel = computed(() => {
const number = incomingCall.value?.otherNumber ?? ''
return (
calls.contacts.find((contact) => contact.phone_number === number)?.name ||
number
)
})
async function answer(): Promise<void> {
if (pendingAction.value || !incomingCall.value) return
pendingAction.value = 'answer'
const response = await calls.answer()
pendingAction.value = null
if (response.success) emit('accepted')
}
async function decline(): Promise<void> {
if (pendingAction.value || !incomingCall.value) return
pendingAction.value = 'decline'
await calls.decline()
pendingAction.value = null
}
</script>
<template>
<Transition name="phone-dynamic-island">
<aside
v-if="incomingCall"
class="phone-dynamic-island sky-ui-provider sky-ui-provider--dark"
:aria-label="phone.t('Apps.phone.incoming')"
role="region"
>
<div class="phone-dynamic-island__caller">
<span>{{ phone.t('Apps.phone.incoming') }}</span>
<strong>{{ callerLabel }}</strong>
</div>
<div class="phone-dynamic-island__actions">
<SkyButton
:aria-label="phone.t('Apps.phone.decline')"
:disabled="pendingAction !== null"
icon-only
rounded
variant="danger"
@click="decline"
>
<PhoneOff aria-hidden="true" />
</SkyButton>
<SkyButton
:aria-label="phone.t('Apps.phone.answer')"
:disabled="pendingAction !== null"
icon-only
rounded
class="phone-dynamic-island__answer"
@click="answer"
>
<Phone aria-hidden="true" />
</SkyButton>
</div>
</aside>
</Transition>
</template>