mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 17:01:18 +00:00
ENH - merge weather app into dev
Integrates the weather-app interface refinements while preserving the newer dev Dynamic Island and cross-platform contract tests. Adds the matching German Notes deletion confirmation strings required by the merged locale contract.
This commit is contained in:
@@ -788,6 +788,76 @@ button {
|
||||
cursor: pointer;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.phone-dynamic-island {
|
||||
position: absolute;
|
||||
z-index: 98;
|
||||
top: 12px;
|
||||
left: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 300px;
|
||||
min-height: 72px;
|
||||
padding: 10px 12px 10px 16px;
|
||||
border: 1px solid rgb(255 255 255 / 10%);
|
||||
border-radius: 28px;
|
||||
color: #fff;
|
||||
background: #050505;
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 45%);
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
.phone-dynamic-island__caller {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
.phone-dynamic-island__caller span,
|
||||
.phone-dynamic-island__caller strong {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.phone-dynamic-island__caller span {
|
||||
color: rgb(255 255 255 / 58%);
|
||||
font-size: 11px;
|
||||
line-height: 14px;
|
||||
}
|
||||
.phone-dynamic-island__caller strong {
|
||||
margin-top: 2px;
|
||||
font-size: 15px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.phone-dynamic-island__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.phone-dynamic-island__actions .sky-button--icon-only {
|
||||
width: 44px;
|
||||
min-width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.phone-dynamic-island__actions svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
.phone-dynamic-island__answer {
|
||||
--sky-app-accent: #34c759;
|
||||
}
|
||||
.phone-dynamic-island-enter-active,
|
||||
.phone-dynamic-island-leave-active {
|
||||
transition: opacity 180ms ease, transform 220ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
.phone-dynamic-island-enter-from,
|
||||
.phone-dynamic-island-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) scale(0.86);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.phone-dynamic-island-enter-active,
|
||||
.phone-dynamic-island-leave-active {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
.phone-home-indicator {
|
||||
position: absolute;
|
||||
z-index: 90;
|
||||
@@ -4460,9 +4530,9 @@ button {
|
||||
display: grid;
|
||||
grid-template-columns: 21px 1fr;
|
||||
gap: 4px 7px;
|
||||
min-height: 82px;
|
||||
min-height: 94px;
|
||||
margin: 0;
|
||||
padding: 12px;
|
||||
padding: 14px;
|
||||
border-radius: calc(var(--sky-radius-card) - 6px);
|
||||
}
|
||||
.weather-detail-card svg {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -3927,6 +3927,8 @@ const defaultLocales: LocaleTree = {
|
||||
pin: 'Pin note',
|
||||
unpin: 'Unpin note',
|
||||
deleteNote: 'Delete note',
|
||||
deleteTitle: 'Delete Note?',
|
||||
deleteBody: 'This note will be permanently deleted.',
|
||||
tools: {
|
||||
bold: 'Bold',
|
||||
bulletList: 'Bullet list',
|
||||
|
||||
@@ -861,6 +861,29 @@ a.sky-chip::before {
|
||||
color 300ms ease;
|
||||
}
|
||||
|
||||
.sky-list-item--with-actions {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.sky-list-item--with-actions > .sky-list-item__row {
|
||||
width: auto;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.sky-list-item__actions {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: var(--sky-space-1, 4px);
|
||||
padding: 0 var(--sky-space-2, 8px);
|
||||
color: var(--sky-muted, rgba(0, 0, 0, 0.55));
|
||||
}
|
||||
|
||||
.sky-list-item__actions > .sky-button--clear {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button.sky-list-item__row,
|
||||
a.sky-list-item__row,
|
||||
label.sky-list-item__row {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSSRApp } from 'vue'
|
||||
import { createSSRApp, h } from 'vue'
|
||||
import { renderToString } from 'vue/server-renderer'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
@@ -15,4 +15,22 @@ describe('SkyListItem', () => {
|
||||
)
|
||||
expect(html).not.toContain('<button')
|
||||
})
|
||||
|
||||
it('renders row actions beside rather than inside the primary control', async () => {
|
||||
const html = await renderToString(
|
||||
createSSRApp({
|
||||
render: () =>
|
||||
h(
|
||||
SkyListItem,
|
||||
{ link: true, linkComponent: 'button', title: 'Note' },
|
||||
{ actions: () => h('button', { type: 'button' }, 'Delete') },
|
||||
),
|
||||
}),
|
||||
)
|
||||
|
||||
expect(html).toContain('sky-list-item--with-actions')
|
||||
expect(html).toMatch(
|
||||
/<button[^>]*class="sky-list-item__row"[\s\S]*?<\/button><div class="sky-list-item__actions">[\s\S]*?<button/,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -73,6 +73,7 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const slots = useSlots()
|
||||
const hasActions = computed(() => Boolean(slots.actions))
|
||||
const listContext = inject(skyListContextKey, undefined)
|
||||
const effectiveDividers = computed(
|
||||
() => props.dividers ?? listContext?.value.dividers ?? false,
|
||||
@@ -160,6 +161,7 @@ function handleClick(event: MouseEvent): void {
|
||||
'sky-list-item--active': active,
|
||||
'sky-list-item--contacts': contacts,
|
||||
'sky-list-item--dividers': effectiveDividers,
|
||||
'sky-list-item--with-actions': hasActions,
|
||||
'sky-list-item--no-dividers': !effectiveDividers,
|
||||
'sky-list-item--label': label,
|
||||
'sky-list-item--link': isLink,
|
||||
@@ -237,6 +239,9 @@ function handleClick(event: MouseEvent): void {
|
||||
</span>
|
||||
<slot name="content" />
|
||||
</component>
|
||||
<div v-if="hasActions" class="sky-list-item__actions">
|
||||
<slot name="actions" />
|
||||
</div>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
@@ -364,12 +364,7 @@ function relativeTime(value: string): string {
|
||||
}
|
||||
|
||||
function companyInitials(company: CompanySummary): string {
|
||||
return company.name
|
||||
.split(/\s+/)
|
||||
.slice(0, 2)
|
||||
.map((word) => word[0] ?? '')
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
return company.name.trim().charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
function companySubtitle(company: CompanySummary): string {
|
||||
|
||||
@@ -28,6 +28,13 @@ describe('HealthApp Sky UI contract', () => {
|
||||
expect(source).toContain('health.saveMedicalId')
|
||||
})
|
||||
|
||||
it('uses the Sky iOS action sheet instead of a native blood type select', () => {
|
||||
expect(source).toContain('<SkyActionSheet')
|
||||
expect(source).toContain('<SkyActionButton')
|
||||
expect(source).toContain('@click="selectBloodType(option.value)"')
|
||||
expect(source).not.toContain(':options="bloodTypeOptions"')
|
||||
})
|
||||
|
||||
it('does not present values the phone cannot observe', () => {
|
||||
expect(source).not.toContain('healthPercent')
|
||||
expect(source).not.toContain('recoveryPercent')
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import {
|
||||
Activity,
|
||||
ChartNoAxesColumnIncreasing,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ContactRound,
|
||||
Flame,
|
||||
Footprints,
|
||||
@@ -19,6 +21,10 @@ import { usePhoneStore } from '@/stores/phone'
|
||||
import type { HealthMedicalIdInput } from '@/types/health'
|
||||
import {
|
||||
SkyAppPage,
|
||||
SkyActionButton,
|
||||
SkyActionGroup,
|
||||
SkyActionSheet,
|
||||
SkyActionsLabel,
|
||||
SkyButton,
|
||||
SkyEmptyState,
|
||||
SkyField,
|
||||
@@ -39,6 +45,7 @@ const phone = usePhoneStore()
|
||||
const health = useHealthStore()
|
||||
const activeTab = ref<HealthTab>('today')
|
||||
const editingMedicalId = ref(false)
|
||||
const bloodTypePickerOpened = ref(false)
|
||||
const actionError = ref('')
|
||||
const medicalDraft = reactive<HealthMedicalIdInput>({
|
||||
allergies: '',
|
||||
@@ -136,10 +143,11 @@ function formatMinutes(seconds: number): string {
|
||||
})
|
||||
}
|
||||
|
||||
function weekday(date: string, narrow = true): string {
|
||||
return new Intl.DateTimeFormat(phone.lang, {
|
||||
weekday: narrow ? 'narrow' : 'short',
|
||||
}).format(new Date(`${date}T12:00:00`))
|
||||
function weekday(date: string): string {
|
||||
const value = new Intl.DateTimeFormat(phone.lang, { weekday: 'short' })
|
||||
.format(new Date(`${date}T12:00:00`))
|
||||
.replace(/\.$/, '')
|
||||
return Array.from(value).slice(0, 2).join('')
|
||||
}
|
||||
|
||||
function fullDate(date: string): string {
|
||||
@@ -168,6 +176,7 @@ function syncMedicalDraft(): void {
|
||||
|
||||
async function toggleMedicalEdit(): Promise<void> {
|
||||
actionError.value = ''
|
||||
bloodTypePickerOpened.value = false
|
||||
if (!editingMedicalId.value) {
|
||||
syncMedicalDraft()
|
||||
editingMedicalId.value = true
|
||||
@@ -185,6 +194,11 @@ async function toggleMedicalEdit(): Promise<void> {
|
||||
: translatedError
|
||||
}
|
||||
|
||||
function selectBloodType(value: string): void {
|
||||
medicalDraft.bloodType = value
|
||||
bloodTypePickerOpened.value = false
|
||||
}
|
||||
|
||||
async function dial(number: string): Promise<void> {
|
||||
if (!number) return
|
||||
actionError.value = ''
|
||||
@@ -429,12 +443,18 @@ onBeforeUnmount(() => {
|
||||
<section class="health-section">
|
||||
<h2>{{ phone.t('Apps.health.medicalId.emergencyInformation') }}</h2>
|
||||
<ul class="health-form">
|
||||
<SkyField
|
||||
v-model="medicalDraft.bloodType"
|
||||
:label="phone.t('Apps.health.medicalId.bloodType')"
|
||||
:options="bloodTypeOptions"
|
||||
type="select"
|
||||
/>
|
||||
<li class="health-blood-type-field">
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="dialog"
|
||||
:aria-expanded="bloodTypePickerOpened"
|
||||
@click="bloodTypePickerOpened = true"
|
||||
>
|
||||
<span>{{ phone.t('Apps.health.medicalId.bloodType') }}</span>
|
||||
<strong>{{ medicalDraft.bloodType || '—' }}</strong>
|
||||
<ChevronDown :size="17" aria-hidden="true" />
|
||||
</button>
|
||||
</li>
|
||||
<SkyField
|
||||
v-model="medicalDraft.allergies"
|
||||
:label="phone.t('Apps.health.medicalId.allergies')"
|
||||
@@ -557,6 +577,39 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
</SkyScrollArea>
|
||||
|
||||
<SkyActionSheet
|
||||
:aria-label="phone.t('Apps.health.medicalId.bloodType')"
|
||||
:opened="bloodTypePickerOpened"
|
||||
@backdropclick="bloodTypePickerOpened = false"
|
||||
@escape="bloodTypePickerOpened = false"
|
||||
>
|
||||
<SkyActionGroup>
|
||||
<SkyActionsLabel>
|
||||
{{ phone.t('Apps.health.medicalId.bloodType') }}
|
||||
</SkyActionsLabel>
|
||||
<SkyActionButton
|
||||
v-for="option in bloodTypeOptions"
|
||||
:key="option.value || 'none'"
|
||||
class="health-blood-type-option"
|
||||
:bold="medicalDraft.bloodType === option.value"
|
||||
:aria-pressed="medicalDraft.bloodType === option.value"
|
||||
@click="selectBloodType(option.value)"
|
||||
>
|
||||
<span>{{ option.label }}</span>
|
||||
<Check
|
||||
v-if="medicalDraft.bloodType === option.value"
|
||||
:size="18"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
<SkyActionGroup>
|
||||
<SkyActionButton bold @click="bloodTypePickerOpened = false">
|
||||
{{ phone.t('Common.cancel') }}
|
||||
</SkyActionButton>
|
||||
</SkyActionGroup>
|
||||
</SkyActionSheet>
|
||||
|
||||
<SkyPillNavigation
|
||||
class="health-navigation"
|
||||
layout="full"
|
||||
@@ -708,6 +761,7 @@ onBeforeUnmount(() => {
|
||||
border: calc(var(--sky-hairline-scale) * 1px) solid var(--health-panel-border);
|
||||
border-radius: var(--sky-radius-card);
|
||||
background: var(--health-panel);
|
||||
flex: none;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@@ -745,8 +799,9 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.health-metrics span {
|
||||
color: var(--sky-muted);
|
||||
font-size: 12px;
|
||||
color: var(--sky-text);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.health-section {
|
||||
@@ -1099,6 +1154,53 @@ onBeforeUnmount(() => {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.health-blood-type-field button {
|
||||
display: grid;
|
||||
width: 100%;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
padding: 8px 16px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--sky-text);
|
||||
cursor: pointer;
|
||||
grid-template-columns: 1fr auto auto;
|
||||
grid-template-rows: auto auto;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.health-blood-type-field button:focus-visible {
|
||||
outline: 2px solid var(--sky-app-accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.health-blood-type-field span {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--sky-muted);
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.health-blood-type-field strong {
|
||||
font-size: 17px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.health-blood-type-field svg {
|
||||
color: var(--sky-muted);
|
||||
}
|
||||
|
||||
.health-blood-type-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.health-blood-type-option svg {
|
||||
color: var(--sky-app-accent);
|
||||
}
|
||||
|
||||
.health-action-error {
|
||||
margin: 0;
|
||||
padding: 0 4px;
|
||||
|
||||
@@ -1,15 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyLink,
|
||||
SkyList,
|
||||
SkyListButton,
|
||||
SkyListItem,
|
||||
SkyNavbar,
|
||||
SkyNavbarBackLink,
|
||||
SkyAppPage,
|
||||
} from '@/ui'
|
||||
import {
|
||||
Ellipsis,
|
||||
Pin,
|
||||
@@ -26,8 +15,19 @@ import { useEasyShareStore } from '@/stores/easyshare'
|
||||
import { usePhoneStore } from '@/stores/phone'
|
||||
import {
|
||||
SkyActionSheet,
|
||||
SkyAppPage,
|
||||
SkyBlock,
|
||||
SkyBlockTitle,
|
||||
SkyButton,
|
||||
SkyDialog,
|
||||
SkyDialogButton,
|
||||
SkyFab,
|
||||
SkyLink,
|
||||
SkyList,
|
||||
SkyListButton,
|
||||
SkyListItem,
|
||||
SkyNavbar,
|
||||
SkyNavbarBackLink,
|
||||
SkyScrollArea,
|
||||
SkySearchbar,
|
||||
SkyToolbar,
|
||||
@@ -43,6 +43,7 @@ const editorId = ref<string | null>(null)
|
||||
const editorOpened = ref(false)
|
||||
const draftBody = ref('')
|
||||
const menuOpened = ref(false)
|
||||
const listDeleteCandidateId = ref<string | null>(null)
|
||||
const currentNote = computed(() =>
|
||||
editorId.value
|
||||
? notes.notes.find((note) => note.id === editorId.value)
|
||||
@@ -121,6 +122,21 @@ function editNote(note: Note): void {
|
||||
editorOpened.value = true
|
||||
}
|
||||
|
||||
function requestListDelete(note: Note): void {
|
||||
listDeleteCandidateId.value = note.id
|
||||
}
|
||||
|
||||
function cancelListDelete(): void {
|
||||
listDeleteCandidateId.value = null
|
||||
}
|
||||
|
||||
function confirmListDelete(): void {
|
||||
const noteId = listDeleteCandidateId.value
|
||||
if (!noteId) return
|
||||
notes.deleteNote(noteId)
|
||||
listDeleteCandidateId.value = null
|
||||
}
|
||||
|
||||
function persistDraft(): Note | undefined {
|
||||
const draft = {
|
||||
body: draftBody.value,
|
||||
@@ -185,7 +201,8 @@ function shareNote(): void {
|
||||
<template>
|
||||
<sky-app-page
|
||||
v-if="!editorOpened"
|
||||
class="notes-list-page"
|
||||
class="notes-list-page sky-ui-provider"
|
||||
:class="{ 'sky-ui-provider--dark': phone.isDarkMode }"
|
||||
:aria-label="phone.t('Apps.notes.name')"
|
||||
>
|
||||
<sky-navbar
|
||||
@@ -195,22 +212,34 @@ function shareNote(): void {
|
||||
/>
|
||||
|
||||
<SkyScrollArea as="main" class="notes-list-scroll">
|
||||
<sky-list v-if="visibleNotes.length" strong inset>
|
||||
<sky-list-item
|
||||
<SkyList v-if="visibleNotes.length" inset strong>
|
||||
<SkyListItem
|
||||
v-for="note in visibleNotes"
|
||||
:key="note.id"
|
||||
href="#"
|
||||
link
|
||||
link-component="button"
|
||||
:title="noteTitle(note)"
|
||||
:subtitle="noteSubtitle(note)"
|
||||
:chevron="false"
|
||||
strong-title="auto"
|
||||
@click.prevent="editNote(note)"
|
||||
@click="editNote(note)"
|
||||
>
|
||||
<template v-if="note.pinned" #after>
|
||||
<Pin :size="15" aria-hidden="true" />
|
||||
</template>
|
||||
</sky-list-item>
|
||||
</sky-list>
|
||||
<template #actions>
|
||||
<SkyButton
|
||||
:aria-label="phone.t('Apps.notes.deleteNote')"
|
||||
clear
|
||||
icon-only
|
||||
rounded
|
||||
@click.stop="requestListDelete(note)"
|
||||
>
|
||||
<Trash2 :size="18" aria-hidden="true" />
|
||||
</SkyButton>
|
||||
</template>
|
||||
</SkyListItem>
|
||||
</SkyList>
|
||||
|
||||
<template v-else>
|
||||
<sky-block-title large>{{
|
||||
@@ -232,8 +261,7 @@ function shareNote(): void {
|
||||
</SkyScrollArea>
|
||||
|
||||
<SkyToolbar
|
||||
class="notes-composer sky-ui-provider"
|
||||
:class="{ 'sky-ui-provider--dark': phone.isDarkMode }"
|
||||
class="notes-composer"
|
||||
component="footer"
|
||||
:aria-label="phone.t('Apps.notes.searchPlaceholder')"
|
||||
>
|
||||
@@ -253,6 +281,28 @@ function shareNote(): void {
|
||||
</template>
|
||||
</SkyFab>
|
||||
</SkyToolbar>
|
||||
|
||||
<SkyDialog
|
||||
:opened="listDeleteCandidateId !== null"
|
||||
:title="phone.t('Apps.notes.deleteTitle')"
|
||||
:content="phone.t('Apps.notes.deleteBody')"
|
||||
role="alertdialog"
|
||||
@backdropclick="cancelListDelete"
|
||||
@escape="cancelListDelete"
|
||||
>
|
||||
<template #buttons>
|
||||
<SkyDialogButton @click="cancelListDelete">
|
||||
{{ phone.t('Common.cancel') }}
|
||||
</SkyDialogButton>
|
||||
<SkyDialogButton
|
||||
strong
|
||||
class="notes-delete-confirm"
|
||||
@click="confirmListDelete"
|
||||
>
|
||||
{{ phone.t('Common.delete') }}
|
||||
</SkyDialogButton>
|
||||
</template>
|
||||
</SkyDialog>
|
||||
</sky-app-page>
|
||||
|
||||
<sky-app-page v-else class="notes-editor-page !pt-[44px] !pb-0">
|
||||
@@ -336,6 +386,11 @@ function shareNote(): void {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.notes-delete-confirm {
|
||||
color: var(--sky-danger);
|
||||
background: var(--sky-danger-soft);
|
||||
}
|
||||
|
||||
.notes-editor-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -82,7 +82,6 @@ const recentFilter = ref<RecentFilter>('all')
|
||||
const selectedNumber = ref('')
|
||||
const viewingOwnCard = ref(false)
|
||||
const phoneContent = ref<HTMLElement | null>(null)
|
||||
const contactDetailCompact = ref(false)
|
||||
const keypad = ref('')
|
||||
const editorOpened = ref(false)
|
||||
const editingContact = ref<PhoneContact | null>(null)
|
||||
@@ -245,10 +244,7 @@ const activeCallContact = computed(
|
||||
) ?? null,
|
||||
)
|
||||
const contactEditorInitials = computed(() =>
|
||||
[contactFirstName.value, contactLastName.value]
|
||||
.map((part) => part.trim().charAt(0).toUpperCase())
|
||||
.filter(Boolean)
|
||||
.join(''),
|
||||
contactFirstName.value.trim().charAt(0).toUpperCase(),
|
||||
)
|
||||
|
||||
function eventValue(event: Event): string {
|
||||
@@ -301,7 +297,6 @@ function removeContactPhoto(): void {
|
||||
function openRecentDetail(number: string): void {
|
||||
viewingOwnCard.value = false
|
||||
selectedNumber.value = number
|
||||
contactDetailCompact.value = false
|
||||
error.value = ''
|
||||
void nextTick(() => phoneContent.value?.scrollTo({ top: 0 }))
|
||||
}
|
||||
@@ -310,7 +305,6 @@ function openMyCard(): void {
|
||||
if (!phone.device?.sim) return
|
||||
viewingOwnCard.value = true
|
||||
selectedNumber.value = phone.device.sim.number
|
||||
contactDetailCompact.value = false
|
||||
error.value = ''
|
||||
void nextTick(() => phoneContent.value?.scrollTo({ top: 0 }))
|
||||
}
|
||||
@@ -318,27 +312,14 @@ function openMyCard(): void {
|
||||
function closeRecentDetail(): void {
|
||||
viewingOwnCard.value = false
|
||||
selectedNumber.value = ''
|
||||
contactDetailCompact.value = false
|
||||
}
|
||||
|
||||
function selectTab(nextTab: PhoneTab): void {
|
||||
viewingOwnCard.value = false
|
||||
selectedNumber.value = ''
|
||||
contactDetailCompact.value = false
|
||||
tab.value = nextTab
|
||||
}
|
||||
|
||||
function handlePhoneContentScroll(): void {
|
||||
if (!selectedNumber.value) {
|
||||
contactDetailCompact.value = false
|
||||
return
|
||||
}
|
||||
const scrollTop = phoneContent.value?.scrollTop ?? 0
|
||||
contactDetailCompact.value = contactDetailCompact.value
|
||||
? scrollTop > 110
|
||||
: scrollTop > 165
|
||||
}
|
||||
|
||||
function scrollToContactGroup(letter: string): void {
|
||||
const scroller = phoneContent.value
|
||||
if (letter === 'favorites') {
|
||||
@@ -424,7 +405,6 @@ async function saveContact(): Promise<void> {
|
||||
editorOpened.value = false
|
||||
viewingOwnCard.value = false
|
||||
selectedNumber.value = number
|
||||
contactDetailCompact.value = false
|
||||
void nextTick(() => phoneContent.value?.scrollTo({ top: 0 }))
|
||||
}
|
||||
|
||||
@@ -588,6 +568,48 @@ function addDigit(digit: string): void {
|
||||
if (keypad.value.length < 10) keypad.value += digit
|
||||
}
|
||||
|
||||
function isEditableTarget(target: EventTarget | null): boolean {
|
||||
if (!(target instanceof HTMLElement)) return false
|
||||
return (
|
||||
target.isContentEditable ||
|
||||
target instanceof HTMLInputElement ||
|
||||
target instanceof HTMLSelectElement ||
|
||||
target instanceof HTMLTextAreaElement
|
||||
)
|
||||
}
|
||||
|
||||
function handleKeypadKeyboard(event: KeyboardEvent): void {
|
||||
if (
|
||||
tab.value !== 'keypad' ||
|
||||
selectedNumber.value ||
|
||||
event.defaultPrevented ||
|
||||
event.isComposing ||
|
||||
event.altKey ||
|
||||
event.ctrlKey ||
|
||||
event.metaKey ||
|
||||
isEditableTarget(event.target)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
if (/^[0-9*#]$/.test(event.key)) {
|
||||
event.preventDefault()
|
||||
addDigit(event.key)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Backspace' || event.key === 'Delete') {
|
||||
event.preventDefault()
|
||||
keypad.value = keypad.value.slice(0, -1)
|
||||
return
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && keypad.value && !event.repeat) {
|
||||
event.preventDefault()
|
||||
void startCall(keypad.value)
|
||||
}
|
||||
}
|
||||
|
||||
function chooseKeypadSuggestion(contact: PhoneContact): void {
|
||||
keypad.value = contact.phone_number.replace(/\D/g, '').slice(0, 10)
|
||||
}
|
||||
@@ -611,12 +633,7 @@ function recentSubtitle(recent: RecentCall): string {
|
||||
function contactInitials(number: string): string {
|
||||
const contact = calls.contacts.find((entry) => entry.phone_number === number)
|
||||
if (!contact) return ''
|
||||
return contact.name
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((part) => part[0]?.toUpperCase())
|
||||
.join('')
|
||||
return contact.name.trim().charAt(0).toUpperCase()
|
||||
}
|
||||
|
||||
function formatRecentDate(value: string): string {
|
||||
@@ -648,6 +665,7 @@ function formatRecentDate(value: string): string {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', handleKeypadKeyboard)
|
||||
await calls.bootstrap()
|
||||
const photoSelection = mediaPicker.consumeMany<ContactPhotoContext>(
|
||||
'phone:contact-photo',
|
||||
@@ -690,6 +708,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeypadKeyboard)
|
||||
if (callClock !== null) window.clearInterval(callClock)
|
||||
})
|
||||
</script>
|
||||
@@ -903,7 +922,6 @@ onBeforeUnmount(() => {
|
||||
ref="phoneContent"
|
||||
class="phone-call-content"
|
||||
:class="{ 'phone-call-content--profile': selectedNumber }"
|
||||
@scroll.passive="handlePhoneContentScroll"
|
||||
>
|
||||
<sky-block v-if="!phone.device?.sim" class="text-center">
|
||||
<h2>{{ phone.t('Apps.phone.noSim') }}</h2>
|
||||
@@ -911,10 +929,7 @@ onBeforeUnmount(() => {
|
||||
</sky-block>
|
||||
|
||||
<template v-else-if="selectedNumber">
|
||||
<section
|
||||
class="phone-contact-detail"
|
||||
:class="{ 'phone-contact-detail--compact': contactDetailCompact }"
|
||||
>
|
||||
<section class="phone-contact-detail">
|
||||
<header class="phone-detail-header">
|
||||
<sky-button
|
||||
rounded
|
||||
@@ -925,35 +940,7 @@ onBeforeUnmount(() => {
|
||||
<ChevronLeft :size="29" :stroke-width="2.4" />
|
||||
</sky-button>
|
||||
|
||||
<div class="phone-detail-compact-center">
|
||||
<h2 class="phone-detail-compact-title">
|
||||
{{ selectedDisplayName }}
|
||||
</h2>
|
||||
<div class="phone-detail-compact-actions">
|
||||
<sky-button
|
||||
v-for="action in contactProfileActions"
|
||||
:key="action.id"
|
||||
rounded
|
||||
class="phone-profile-action phone-profile-action--compact"
|
||||
:disabled="
|
||||
viewingOwnCard ||
|
||||
['video', 'mail'].includes(action.id) ||
|
||||
(action.id === 'call' &&
|
||||
selectedContact?.canCall === false) ||
|
||||
(action.id === 'message' &&
|
||||
selectedContact?.canMessage === false)
|
||||
"
|
||||
:aria-label="phone.t(`Apps.phone.${action.id}`)"
|
||||
@click="triggerContactAction(action.id)"
|
||||
>
|
||||
<component
|
||||
:is="action.icon"
|
||||
:size="24"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</sky-button>
|
||||
</div>
|
||||
</div>
|
||||
<span class="phone-detail-header-spacer" />
|
||||
|
||||
<sky-button
|
||||
v-if="!viewingOwnCard"
|
||||
@@ -3024,30 +3011,6 @@ onBeforeUnmount(() => {
|
||||
margin: 0 -15px;
|
||||
padding: 7px 15px;
|
||||
overflow: visible;
|
||||
transition:
|
||||
height 360ms cubic-bezier(0.22, 1, 0.36, 1),
|
||||
background 260ms ease,
|
||||
border-color 260ms ease,
|
||||
border-radius 260ms ease,
|
||||
box-shadow 260ms ease,
|
||||
backdrop-filter 260ms ease;
|
||||
}
|
||||
|
||||
.phone-contact-detail--compact .phone-detail-header {
|
||||
height: 156px;
|
||||
border-bottom: 1px solid rgba(100, 168, 255, 0.2);
|
||||
border-radius: 0 0 22px 22px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(32, 41, 51, 0.97) 0%,
|
||||
rgba(31, 34, 51, 0.96) 56%,
|
||||
rgba(19, 47, 78, 0.94) 100%
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 -1px 0 rgba(255, 255, 255, 0.05),
|
||||
0 14px 30px rgba(9, 7, 25, 0.28);
|
||||
backdrop-filter: blur(30px) saturate(155%);
|
||||
-webkit-backdrop-filter: blur(30px) saturate(155%);
|
||||
}
|
||||
|
||||
.phone-detail-header-button,
|
||||
@@ -3078,63 +3041,8 @@ onBeforeUnmount(() => {
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
.phone-detail-compact-center {
|
||||
position: static;
|
||||
.phone-detail-header-spacer {
|
||||
min-width: 0;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.phone-detail-compact-title {
|
||||
display: -webkit-box;
|
||||
max-width: 190px;
|
||||
margin: 1px auto 0;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
font-size: 19px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.25px;
|
||||
line-height: 1.12;
|
||||
opacity: 0;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
text-shadow: 0 1px 3px rgba(20, 25, 35, 0.12);
|
||||
transform: translateY(-10px) scale(0.94);
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 340ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
}
|
||||
|
||||
.phone-contact-detail--compact .phone-detail-compact-title {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
transition-delay: 55ms;
|
||||
}
|
||||
|
||||
.phone-detail-compact-actions {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
display: grid;
|
||||
width: max-content;
|
||||
grid-template-columns: repeat(4, 62px);
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, -18px) scale(0.9);
|
||||
transform-origin: top center;
|
||||
transition:
|
||||
opacity 180ms ease,
|
||||
transform 360ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.phone-contact-detail--compact .phone-detail-compact-actions {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: translate(-50%, 0) scale(1);
|
||||
transition-delay: 80ms;
|
||||
}
|
||||
|
||||
.phone-contact-hero {
|
||||
@@ -3181,6 +3089,7 @@ onBeforeUnmount(() => {
|
||||
min-height: 62px;
|
||||
padding: 0;
|
||||
border: 1px solid rgba(10, 132, 255, 0.58);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: rgba(18, 82, 145, 0.72);
|
||||
box-shadow:
|
||||
@@ -3193,26 +3102,6 @@ onBeforeUnmount(() => {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.phone-profile-action--compact {
|
||||
width: 62px;
|
||||
min-width: 62px;
|
||||
height: 62px;
|
||||
min-height: 62px;
|
||||
border-color: rgba(255, 255, 255, 0.26);
|
||||
background: rgba(24, 35, 45, 0.82);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 5px 15px rgba(20, 24, 37, 0.16);
|
||||
transition:
|
||||
background-color 140ms ease,
|
||||
transform 140ms ease;
|
||||
}
|
||||
|
||||
.phone-profile-action--compact:active {
|
||||
background: rgba(40, 55, 67, 0.9);
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
.phone-profile-content {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -3642,28 +3531,12 @@ onBeforeUnmount(() => {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.phone-profile-action--compact {
|
||||
width: 54px;
|
||||
min-width: 54px;
|
||||
height: 54px;
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.phone-detail-compact-actions {
|
||||
grid-template-columns: repeat(4, 54px);
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.phone-detail-compact-title {
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.phone-contact-actions {
|
||||
grid-template-columns: repeat(4, 56px);
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.phone-profile-action:not(.phone-profile-action--compact) {
|
||||
.phone-profile-action {
|
||||
width: 56px;
|
||||
min-width: 56px;
|
||||
height: 56px;
|
||||
@@ -3882,9 +3755,6 @@ onBeforeUnmount(() => {
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.phone-detail-header,
|
||||
.phone-detail-compact-title,
|
||||
.phone-detail-compact-actions,
|
||||
.phone-profile-action--compact,
|
||||
.phone-calls-app button,
|
||||
.phone-contact-editor button {
|
||||
transition-duration: 1ms;
|
||||
|
||||
@@ -1717,7 +1717,8 @@ Locales["de"] = {
|
||||
untitled = "Unbenannt", noText = "Kein zusätzlicher Text", emptyBadge = "ON DEVICE", emptyTitle = "Keine Anmerkungen",
|
||||
emptyBody = "Erstelle eine Notiz, um wichtige Details nahe bei der Hand zu halten.", noResults = "Keine Ergebnisse",
|
||||
noResultsBody = "Versuch nach einem anderen Wort oder Satz zu suchen.", pin = "Hinweis zum Stift", unpin = "Anmerkung zum Öffnen",
|
||||
deleteNote = "Anmerkung löschen",
|
||||
deleteNote = "Anmerkung löschen", deleteTitle = "Notiz löschen?",
|
||||
deleteBody = "Diese Notiz wird dauerhaft gelöscht.",
|
||||
tools = {
|
||||
bold = "Mutig", bulletList = "Liste der Kugeln", decreaseText = "Kleinerer Text", increaseText = "Größerer Text",
|
||||
italic = "kursiv", numberedList = "Nummerierte Liste", quote = "Zitat", redo = "Wiederherstellen",
|
||||
|
||||
@@ -1717,7 +1717,8 @@ Locales["en"] = {
|
||||
untitled = "Untitled", noText = "No additional text", emptyBadge = "ON DEVICE", emptyTitle = "No Notes",
|
||||
emptyBody = "Create a note to keep important details close at hand.", noResults = "No Results",
|
||||
noResultsBody = "Try searching for a different word or phrase.", pin = "Pin note", unpin = "Unpin note",
|
||||
deleteNote = "Delete note",
|
||||
deleteNote = "Delete note", deleteTitle = "Delete Note?",
|
||||
deleteBody = "This note will be permanently deleted.",
|
||||
tools = {
|
||||
bold = "Bold", bulletList = "Bullet list", decreaseText = "Smaller text", increaseText = "Larger text",
|
||||
italic = "Italic", numberedList = "Numbered list", quote = "Quote", redo = "Redo",
|
||||
|
||||
Reference in New Issue
Block a user