ENH - refine phone apps and lock screen

This commit is contained in:
Eichenholz
2026-08-04 18:08:35 +02:00
parent c6bac5b2c6
commit 75be4c2a69
10 changed files with 214 additions and 113 deletions
+1 -4
View File
@@ -63,10 +63,7 @@ function getViewportScale(): number {
const heightScale = window.innerHeight / REFERENCE_VIEWPORT_HEIGHT
if (isDevelopment) return heightScale
return Math.min(
window.innerWidth / REFERENCE_VIEWPORT_WIDTH,
heightScale,
)
return Math.min(window.innerWidth / REFERENCE_VIEWPORT_WIDTH, heightScale)
}
function onMessage(event: MessageEvent<AppMessage>): void {
+63 -55
View File
@@ -115,6 +115,7 @@ button {
color-scheme: light;
}
.phone-notification {
top: 68px !important;
right: 8px !important;
left: 8px !important;
width: auto !important;
@@ -241,10 +242,18 @@ button {
left: 26px;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-end;
min-height: 18px;
filter: drop-shadow(0 1px 2px #0009);
}
.lock-screen__lock {
position: absolute;
z-index: 1;
top: 64px;
left: 50%;
transform: translateX(-50%);
filter: drop-shadow(0 1px 2px #0009);
}
.lock-screen__indicators {
position: absolute;
right: 0;
@@ -258,21 +267,26 @@ button {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 80px;
padding-top: 94px;
text-shadow: 0 2px 14px #0005;
}
.lock-screen__date {
font-size: 20px;
font-weight: 500;
letter-spacing: -0.35px;
font-size: 16px;
font-weight: 550;
letter-spacing: -0.2px;
}
.lock-screen__time {
margin-top: -8px;
font-size: 92px;
font-weight: 250;
line-height: 1.1;
letter-spacing: -6px;
margin-top: -5px;
color: #ffffffaa;
font-size: 110px;
font-weight: 300;
line-height: 1.04;
letter-spacing: -7px;
font-variant-numeric: tabular-nums;
text-shadow:
0 0 12px #ffffff38,
0 2px 16px #0006;
filter: blur(0.24px);
}
.lock-screen__footer {
position: absolute;
@@ -281,35 +295,6 @@ button {
bottom: 25px;
left: 0;
}
.lock-screen__shortcuts {
display: flex;
justify-content: space-between;
padding: 0 43px;
}
.lock-screen__shortcut {
width: 50px;
height: 50px;
border: 0;
border-radius: 50%;
display: grid;
place-items: center;
color: #fff;
background: #2424248f;
backdrop-filter: blur(18px) saturate(135%);
-webkit-backdrop-filter: blur(18px) saturate(135%);
box-shadow: inset 0 0 0 1px #ffffff17;
transition:
transform 0.18s cubic-bezier(0.2, 0.8, 0.2, 1),
color 0.18s,
background 0.18s;
}
.lock-screen__shortcut:active {
transform: scale(0.88);
}
.lock-screen__shortcut--active {
color: #171717;
background: #fff;
}
.lock-screen__swipe {
width: 100%;
margin-top: 18px;
@@ -873,6 +858,7 @@ button {
.calculator-display {
height: 145px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
padding: 0 8px 12px;
@@ -881,6 +867,20 @@ button {
font-weight: 250;
white-space: nowrap;
}
.calculator-result {
max-width: 100%;
overflow: hidden;
}
.calculator-calculation {
max-width: 100%;
overflow: hidden;
color: #8e8e93;
font-size: 20px;
font-weight: 400;
letter-spacing: 0;
line-height: 1.2;
text-overflow: ellipsis;
}
.calculator-pad {
display: grid;
grid-template-columns: repeat(4, 1fr);
@@ -1078,15 +1078,6 @@ button {
padding: 0 16px;
text-align: center;
}
.clock-section-heading {
margin: 0;
padding: 4px 0 10px;
text-align: left;
font-size: 34px;
font-weight: 700;
line-height: 41px;
letter-spacing: -0.8px;
}
.clock-digits {
margin: 70px 0 54px;
font-variant-numeric: tabular-nums;
@@ -1095,9 +1086,11 @@ button {
}
.clock-sound-menu {
min-height: 0;
flex: 1;
max-height: calc(100% - 52px);
flex: 0 1 auto;
align-self: stretch;
margin-top: 8px;
margin-bottom: 0;
margin-bottom: 8px;
overflow-y: auto;
}
.clock-sound-selection {
@@ -1126,12 +1119,13 @@ button {
}
.timer-ring {
position: relative;
width: 170px;
height: 170px;
width: 210px;
height: 210px;
margin: 12px auto 16px;
display: grid;
place-items: center;
font-size: 34px;
font-size: 42px;
font-weight: 200;
}
.timer-ring svg {
position: absolute;
@@ -1142,7 +1136,7 @@ button {
}
.timer-ring circle {
fill: none;
stroke-width: 7;
stroke-width: 4;
}
.timer-ring-track {
stroke: #ff9f0a33;
@@ -1162,9 +1156,12 @@ button {
margin-right: 0;
margin-left: 0;
}
.clock-timer-sound {
.clock-timer-note {
margin-top: 16px;
}
.clock-timer-sound {
margin-top: 12px;
}
.clock-timer {
padding-bottom: 28px;
}
@@ -1610,6 +1607,17 @@ button {
flex-direction: column;
overflow: hidden;
background: #020202;
color: #fff;
}
.phone-app--light .clock-app {
background: #f2f2f7;
color: #000;
}
.phone-app--light .time-wheel-picker__selection {
background: #e5e5ea;
}
.phone-app--light .time-wheel-picker__value--selected {
color: #000;
}
.reference-photos {
+58 -25
View File
@@ -1,4 +1,5 @@
<script setup lang="ts">
import { kLink, kNavbar } from 'konsta/vue'
import {
BatteryMedium,
Camera,
@@ -20,6 +21,12 @@ const now = ref(new Date())
const dragOffset = ref(0)
const dragging = ref(false)
const flashlightActive = ref(false)
const lockNavbarColors = { bgIos: 'bg-transparent' }
const neutralGlassClass =
'!bg-white/10 !shadow-none ring-1 ring-inset ring-white/15 backdrop-saturate-150'
const activeFlashlightGlassClass =
'!bg-white !shadow-none ring-1 ring-inset ring-white/60 backdrop-saturate-150'
const whiteNavbarLinkColors = { navbarTextIos: 'text-white' }
let pointerStart = 0
let pointerStartedAt = 0
let clockTicker: number | undefined
@@ -45,6 +52,12 @@ const time = computed(() =>
const dragStyle = computed(() => ({
'--lock-drag': `${dragOffset.value}px`,
}))
const flashlightGlassClass = computed(() =>
flashlightActive.value ? activeFlashlightGlassClass : neutralGlassClass,
)
const flashlightLinkColors = computed(() => ({
navbarTextIos: flashlightActive.value ? 'text-purple-500' : 'text-white',
}))
function onPointerDown(event: PointerEvent): void {
if ((event.target as HTMLElement).closest('button')) return
@@ -78,6 +91,11 @@ function unlockWithKeyboard(event: KeyboardEvent): void {
if (event.key === 'Enter' || event.key === ' ') emit('unlock')
}
function unlockFromWallpaper(event: MouseEvent): void {
if ((event.target as HTMLElement).closest('button, [role="link"]')) return
emit('unlock')
}
onMounted(() => {
clockTicker = window.setInterval(() => {
now.value = new Date()
@@ -104,17 +122,23 @@ onBeforeUnmount(() => {
@pointermove="onPointerMove"
@pointerup="finishPointer"
@pointercancel="finishPointer"
@click="unlockFromWallpaper"
>
<div class="lock-screen__shade" aria-hidden="true"></div>
<header class="lock-screen__status">
<LockKeyhole :size="13" :stroke-width="2.5" aria-hidden="true" />
<div class="lock-screen__indicators" aria-hidden="true">
<Signal :size="12" :stroke-width="2.5" />
<Wifi :size="13" :stroke-width="2.5" />
<BatteryMedium :size="17" :stroke-width="2.4" />
</div>
</header>
<LockKeyhole
class="lock-screen__lock"
:size="14"
:stroke-width="1.8"
aria-hidden="true"
/>
<div class="lock-screen__content">
<time class="lock-screen__date">{{ date }}</time>
@@ -122,31 +146,40 @@ onBeforeUnmount(() => {
</div>
<div class="lock-screen__footer">
<div class="lock-screen__shortcuts">
<button
class="lock-screen__shortcut"
:class="{ 'lock-screen__shortcut--active': flashlightActive }"
type="button"
:aria-label="phone.t('LockScreen.flashlight')"
@click="flashlightActive = !flashlightActive"
>
<Flashlight :size="22" fill="currentColor" />
</button>
<button
class="lock-screen__shortcut"
type="button"
:aria-label="phone.t('LockScreen.camera')"
@click="emit('unlock', 'camera')"
>
<Camera :size="24" fill="currentColor" />
</button>
</div>
<button
class="lock-screen__swipe"
type="button"
@click="emit('unlock')"
<k-navbar
transparent
:colors="lockNavbarColors"
inner-class="!px-12"
:left-class="flashlightGlassClass"
:right-class="neutralGlassClass"
>
<template #left>
<k-link
component="button"
icon-only
:colors="flashlightLinkColors"
:link-props="{ type: 'button' }"
:aria-label="phone.t('LockScreen.flashlight')"
@click="flashlightActive = !flashlightActive"
>
<Flashlight :stroke-width="1.4" aria-hidden="true" />
</k-link>
</template>
<template #right>
<k-link
component="button"
icon-only
:colors="whiteNavbarLinkColors"
:link-props="{ type: 'button' }"
:aria-label="phone.t('LockScreen.camera')"
@click="emit('unlock', 'camera')"
>
<Camera :stroke-width="1.4" aria-hidden="true" />
</k-link>
</template>
</k-navbar>
<button class="lock-screen__swipe" type="button" @click="emit('unlock')">
<span class="lock-screen__swipe-chevron" aria-hidden="true"></span>
{{ phone.t('LockScreen.swipeUp') }}
</button>
+17 -1
View File
@@ -17,11 +17,27 @@ describe('calculator', () => {
it('chains operations and handles division by zero', () => {
let state = inputDigit(clearCalculator(), '8')
state = chooseCalculatorOperator(state, 'add')
expect(state.calculation).toBe('8 +')
state = inputDigit(state, '2')
state = chooseCalculatorOperator(state, 'multiply')
expect(state.display).toBe('10')
expect(state.calculation).toBe('8 + 2 ×')
state = inputDigit(state, '3')
expect(resolveCalculator(state).display).toBe('30')
state = resolveCalculator(state)
expect(state.display).toBe('30')
expect(state.calculation).toBe('8 + 2 × 3 =')
expect(calculate(4, 0, 'divide')).toBeNull()
})
it('replaces a pending operator and clears completed history on new input', () => {
let state = inputDigit(clearCalculator(), '9')
state = chooseCalculatorOperator(state, 'add')
state = chooseCalculatorOperator(state, 'subtract')
expect(state.calculation).toBe('9 ')
state = inputDigit(state, '3')
state = resolveCalculator(state)
expect(state.calculation).toBe('9 3 =')
expect(inputDigit(state, '4').calculation).toBe('')
})
})
+36 -2
View File
@@ -2,6 +2,7 @@ export type CalculatorOperator = 'add' | 'subtract' | 'multiply' | 'divide'
export type CalculatorState = {
accumulator: number | null
calculation: string
display: string
error: boolean
pendingOperator: CalculatorOperator | null
@@ -10,12 +11,20 @@ export type CalculatorState = {
export const INITIAL_CALCULATOR_STATE: CalculatorState = {
accumulator: null,
calculation: '',
display: '0',
error: false,
pendingOperator: null,
waitingForOperand: false,
}
const OPERATOR_SYMBOLS: Record<CalculatorOperator, string> = {
add: '+',
subtract: '',
multiply: '×',
divide: '÷',
}
function formatNumber(value: number): string {
if (!Number.isFinite(value)) return 'Error'
const rounded = Number(value.toPrecision(12))
@@ -45,7 +54,13 @@ export function inputDigit(
): CalculatorState {
if (!/^\d$/.test(digit)) return state
if (state.error || state.waitingForOperand) {
return { ...state, display: digit, error: false, waitingForOperand: false }
return {
...state,
calculation: state.pendingOperator ? state.calculation : '',
display: digit,
error: false,
waitingForOperand: false,
}
}
if (state.display === '0') return { ...state, display: digit }
if (state.display.replace('-', '').replace('.', '').length >= 10) return state
@@ -54,7 +69,13 @@ export function inputDigit(
export function inputDecimal(state: CalculatorState): CalculatorState {
if (state.error || state.waitingForOperand) {
return { ...state, display: '0.', error: false, waitingForOperand: false }
return {
...state,
calculation: state.pendingOperator ? state.calculation : '',
display: '0.',
error: false,
waitingForOperand: false,
}
}
return state.display.includes('.')
? state
@@ -83,6 +104,15 @@ export function chooseCalculatorOperator(
if (state.error) return clearCalculator()
const input = Number(state.display)
let accumulator = state.accumulator
let calculation = state.calculation
if (state.pendingOperator && state.waitingForOperand) {
return {
...state,
calculation: `${calculation.slice(0, -1)}${OPERATOR_SYMBOLS[operator]}`,
pendingOperator: operator,
}
}
if (
accumulator !== null &&
@@ -93,12 +123,15 @@ export function chooseCalculatorOperator(
if (result === null)
return { ...clearCalculator(), display: 'Error', error: true }
accumulator = result
calculation = `${calculation} ${state.display} ${OPERATOR_SYMBOLS[operator]}`
} else if (accumulator === null) {
accumulator = input
calculation = `${state.display} ${OPERATOR_SYMBOLS[operator]}`
}
return {
accumulator,
calculation,
display: formatNumber(accumulator),
error: false,
pendingOperator: operator,
@@ -118,6 +151,7 @@ export function resolveCalculator(state: CalculatorState): CalculatorState {
return { ...clearCalculator(), display: 'Error', error: true }
return {
accumulator: null,
calculation: `${state.calculation} ${state.display} =`,
display: formatNumber(result),
error: false,
pendingOperator: null,
+2 -2
View File
@@ -21,8 +21,8 @@ describe('timestamp clocks', () => {
expect(timerPickerMilliseconds('01:01:01')).toBe(3_661_000)
})
it('formats stopwatch durations with actual milliseconds', () => {
expect(formatStopwatch(61_234)).toBe('01:01.234')
it('formats stopwatch durations with hundredths of a second', () => {
expect(formatStopwatch(61_234)).toBe('01:01.23')
})
it('calculates bounded timer progress from remaining duration', () => {
+2 -2
View File
@@ -21,8 +21,8 @@ export function formatStopwatch(milliseconds: number): string {
const totalMilliseconds = Math.floor(milliseconds)
const minutes = Math.floor(totalMilliseconds / 60_000)
const seconds = Math.floor((totalMilliseconds % 60_000) / 1000)
const millisecondsPart = totalMilliseconds % 1000
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(millisecondsPart).padStart(3, '0')}`
const hundredths = Math.floor((totalMilliseconds % 1000) / 10)
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}.${String(hundredths).padStart(2, '0')}`
}
export function formatTimer(milliseconds: number): string {
+12 -1
View File
@@ -1,9 +1,17 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useCalculatorStore } from '@/stores/calculator'
import { usePhoneStore } from '@/stores/phone'
const calculator = useCalculatorStore()
const phone = usePhoneStore()
const calculation = computed(() => {
if (!calculator.calculation) return ''
return calculator.waitingForOperand
? calculator.calculation
: `${calculator.calculation} ${calculator.display}`
})
const keys: Array<{ label: string; action: () => void; kind?: string }> = [
{ label: 'AC', action: calculator.clear, kind: 'utility' },
{ label: '+/', action: calculator.toggleSign, kind: 'utility' },
@@ -52,7 +60,10 @@ const keys: Array<{ label: string; action: () => void; kind?: string }> = [
:aria-label="phone.t('Apps.calculator.name')"
>
<div class="calculator-display" aria-live="polite">
{{ calculator.display }}
<div class="calculator-result">{{ calculator.display }}</div>
<div v-if="calculation" class="calculator-calculation">
{{ calculation }}
</div>
</div>
<div class="calculator-pad">
<button
+21 -19
View File
@@ -103,15 +103,17 @@ const tabs = [
{ id: 'timer', icon: TimerReset },
] as const
const tabBarColors = {
strongHighlightBgIos: 'bg-[#2c2c2e]',
strongHighlightBgIos: 'bg-[#e5e5ea] dark:bg-[#2c2c2e]',
}
const secondaryActionColors = {
tonalBgIos: 'bg-[#2c2c2e] active:bg-[#3a3a3c]',
tonalTextIos: 'text-white',
tonalBgIos:
'bg-[#e5e5ea] active:bg-[#d1d1d6] dark:bg-[#2c2c2e] dark:active:bg-[#3a3a3c]',
tonalTextIos: 'text-black dark:text-white',
}
const positiveActionColors = {
fillBgIos: 'bg-[#103a20] active:bg-[#174d2a]',
fillTextIos: 'text-[#30d158]',
fillBgIos:
'bg-[#d9f7df] active:bg-[#c7efcf] dark:bg-[#103a20] dark:active:bg-[#174d2a]',
fillTextIos: 'text-[#248a3d] dark:text-[#30d158]',
}
const toggleColors = {
checkedBgIos: 'bg-[#30d158]',
@@ -232,10 +234,10 @@ onBeforeUnmount(() => {
<template v-else>
<k-navbar
v-if="tab === 'world' || tab === 'alarm'"
v-if="tab !== 'stopwatch'"
:title="phone.t(`Apps.clock.tabs.${tab}`)"
:large="tab === 'world' || tab === 'alarm'"
:transparent="tab === 'world' || tab === 'alarm'"
large
transparent
class="clock-navbar"
>
<template #left>
@@ -365,10 +367,6 @@ onBeforeUnmount(() => {
</k-block>
<k-block v-else nested class="clock-tool clock-timer">
<h1 class="clock-section-heading">
{{ phone.t('Apps.clock.tabs.timer') }}
</h1>
<div
v-if="clock.timerStartedAt !== null"
class="timer-ring"
@@ -378,13 +376,13 @@ onBeforeUnmount(() => {
:aria-valuenow="Math.round(timerProgress * 100)"
:aria-valuetext="formatTimer(timerValue)"
>
<svg viewBox="0 0 170 170" aria-hidden="true">
<circle class="timer-ring-track" cx="85" cy="85" r="72" />
<svg viewBox="0 0 210 210" aria-hidden="true">
<circle class="timer-ring-track" cx="105" cy="105" r="92" />
<circle
class="timer-ring-progress"
cx="85"
cy="85"
r="72"
cx="105"
cy="105"
r="92"
pathLength="100"
:style="{ strokeDashoffset: 100 - timerProgress * 100 }"
/>
@@ -407,7 +405,7 @@ onBeforeUnmount(() => {
<k-list
strong
inset
class="clock-konsta-list clock-timer-settings clock-timer-sound"
class="clock-konsta-list clock-timer-settings clock-timer-note"
>
<k-list-input
:label="phone.t('Apps.clock.timer.note')"
@@ -418,7 +416,11 @@ onBeforeUnmount(() => {
/>
</k-list>
<k-list strong inset class="clock-konsta-list clock-timer-settings">
<k-list
strong
inset
class="clock-konsta-list clock-timer-settings clock-timer-sound"
>
<k-list-item
link
:chevron="false"
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sky Phone</title>
<script type="module" crossorigin src="./assets/sky-index-u01-18EF.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-DLFsufyp.css">
<script type="module" crossorigin src="./assets/sky-index-BW7SWjn6.js"></script>
<link rel="stylesheet" crossorigin href="./assets/sky-index-CFphfAe0.css">
</head>
<body>
<div id="app"></div>