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:
Type
2026-08-18 10:08:59 +02:00
14 changed files with 380 additions and 226 deletions
+72 -2
View File
@@ -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"
+2
View File
@@ -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',
+23
View File
@@ -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 {
+19 -1
View File
@@ -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/,
)
})
})
+5
View File
@@ -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>
+1 -6
View File
@@ -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')
+114 -12
View File
@@ -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;
+75 -20
View File
@@ -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;
+51 -181
View File
@@ -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;
+2 -1
View File
@@ -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",
+2 -1
View File
@@ -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",