ENH - rebuild calculator with scientific history

This commit is contained in:
smx.pusha
2026-08-13 14:23:22 +02:00
parent bc4b439eb6
commit 223cb4a600
7 changed files with 912 additions and 109 deletions
-75
View File
@@ -3699,64 +3699,6 @@ button {
background: #1c1c1e;
color: #a0a0a7;
}
.calculator-app {
padding: 85px 19px 29px;
display: flex;
flex-direction: column;
}
.calculator-display {
height: 145px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
padding: 0 8px 12px;
overflow: hidden;
font-size: 65px;
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);
gap: 11px;
}
.calculator-pad button {
aspect-ratio: 1;
border: 0;
border-radius: 50%;
background: #333336;
color: white;
font-size: 29px;
}
.calculator-pad button.utility {
background: #a5a5aa;
color: #000;
}
.calculator-pad button.operator {
background: #ff9f0a;
}
.calculator-pad button.zero {
grid-column: span 2;
aspect-ratio: auto;
border-radius: 50px;
text-align: left;
padding-left: 27px;
}
.clock-content {
min-height: 0;
flex: 1;
@@ -4107,23 +4049,6 @@ button {
font-size: 13px;
}
/* Reference-accurate app surfaces. These remain scoped to the phone resource. */
.calculator-app {
padding: 54px 16px 27px;
}
.calculator-display {
height: 226px;
padding: 0 5px 4px;
font-size: 78px;
letter-spacing: -5px;
}
.calculator-pad {
gap: 11px 14px;
}
.calculator-pad button {
font-size: 28px;
font-weight: 400;
}
.reference-tabbar {
position: absolute;
z-index: 6;
+149 -2
View File
@@ -1,25 +1,67 @@
import { defineStore } from 'pinia'
import {
applyCalculatorUnary,
calculatorPercent,
chooseCalculatorOperator,
clearCalculator,
formatCalculatorNumber,
inputDecimal,
inputDigit,
resolveCalculator,
toggleCalculatorSign,
type CalculatorOperator,
type CalculatorState,
type CalculatorUnaryOperation,
} from '@/utils/calculator'
export type CalculatorHistoryEntry = {
id: string
expression: string
result: string
createdAt: number
}
type CalculatorParentFrame = Pick<
CalculatorState,
'accumulator' | 'calculation' | 'pendingOperator' | 'waitingForOperand'
>
const HISTORY_KEY = 'sky_phone_calculator_history_v1'
function loadHistory(): CalculatorHistoryEntry[] {
if (typeof localStorage === 'undefined') return []
const stored = localStorage.getItem(HISTORY_KEY)
if (!stored) return []
try {
const parsed = JSON.parse(stored) as CalculatorHistoryEntry[]
return Array.isArray(parsed) ? parsed.slice(0, 50) : []
} catch {
console.warn('[Calculator] Ignored invalid persisted history.')
return []
}
}
export const useCalculatorStore = defineStore('calculator', {
state: (): CalculatorState => clearCalculator(),
state: () => ({
...clearCalculator(),
angleUnit: 'radians' as 'degrees' | 'radians',
history: loadHistory(),
memory: 0,
parentFrames: [] as CalculatorParentFrame[],
second: false,
}),
actions: {
backspace(): void {
if (this.error || this.waitingForOperand) return
this.display = this.display.length > 1 ? this.display.slice(0, -1) : '0'
},
chooseOperator(operator: CalculatorOperator): void {
Object.assign(this, chooseCalculatorOperator(this.$state, operator))
},
clear(): void {
Object.assign(this, clearCalculator())
this.parentFrames = []
},
decimal(): void {
Object.assign(this, inputDecimal(this.$state))
@@ -28,7 +70,18 @@ export const useCalculatorStore = defineStore('calculator', {
Object.assign(this, inputDigit(this.$state, value))
},
equals(): void {
Object.assign(this, resolveCalculator(this.$state))
const expression = this.calculation
const next = resolveCalculator(this.$state)
Object.assign(this, next)
if (!expression || next.error || next.calculation === expression) return
this.history.unshift({
id: `${Date.now()}-${Math.random().toString(36).slice(2, 7)}`,
expression: next.calculation.replace(/\s*=\s*$/, ''),
result: next.display,
createdAt: Date.now(),
})
this.history = this.history.slice(0, 50)
this.persistHistory()
},
percent(): void {
Object.assign(this, calculatorPercent(this.$state))
@@ -36,5 +89,99 @@ export const useCalculatorStore = defineStore('calculator', {
toggleSign(): void {
Object.assign(this, toggleCalculatorSign(this.$state))
},
unary(operation: CalculatorUnaryOperation): void {
const result = applyCalculatorUnary(
Number(this.display),
operation,
this.angleUnit,
)
if (result === null) {
Object.assign(this, {
...clearCalculator(),
display: 'Error',
error: true,
})
return
}
this.display = formatCalculatorNumber(result)
this.waitingForOperand = true
},
constant(value: number): void {
this.display = formatCalculatorNumber(value)
this.error = false
this.waitingForOperand = true
},
random(): void {
this.display = formatCalculatorNumber(Math.random())
this.error = false
this.waitingForOperand = true
},
memoryClear(): void {
this.memory = 0
},
memoryAdd(): void {
this.memory += Number(this.display) || 0
},
memorySubtract(): void {
this.memory -= Number(this.display) || 0
},
memoryRecall(): void {
this.constant(this.memory)
},
openParenthesis(): void {
this.parentFrames.push({
accumulator: this.accumulator,
calculation: this.calculation,
pendingOperator: this.pendingOperator,
waitingForOperand: this.waitingForOperand,
})
Object.assign(this, clearCalculator())
},
closeParenthesis(): void {
const frame = this.parentFrames.pop()
if (!frame) return
const inner = this.pendingOperator
? resolveCalculator(this.$state)
: { ...this.$state }
const innerExpression = (inner.calculation || inner.display).replace(
/\s*=\s*$/,
'',
)
Object.assign(this, {
...frame,
display: inner.display,
calculation: frame.calculation
? `${frame.calculation} (${innerExpression})`
: `(${innerExpression})`,
error: inner.error,
waitingForOperand: false,
})
},
toggleAngleUnit(): void {
this.angleUnit = this.angleUnit === 'radians' ? 'degrees' : 'radians'
},
toggleSecond(): void {
this.second = !this.second
},
removeHistory(id: string): void {
this.history = this.history.filter((entry) => entry.id !== id)
this.persistHistory()
},
clearHistory(): void {
this.history = []
this.persistHistory()
},
useHistory(entry: CalculatorHistoryEntry): void {
Object.assign(this, {
...clearCalculator(),
display: entry.result,
calculation: entry.expression,
waitingForOperand: true,
})
},
persistHistory(): void {
if (typeof localStorage === 'undefined') return
localStorage.setItem(HISTORY_KEY, JSON.stringify(this.history))
},
},
})
+14 -1
View File
@@ -2403,7 +2403,20 @@ const defaultLocales: LocaleTree = {
default: 'The housing request failed.',
},
},
calculator: { name: 'Calculator' },
calculator: {
name: 'Calculator',
history: 'Calculation history',
changeMode: 'Change calculator mode',
scientificFunctions: 'Scientific functions',
edit: 'Edit',
done: 'Done',
closeHistory: 'Close history',
today: 'Today',
yesterday: 'Yesterday',
earlier: 'Earlier',
noHistory: 'No calculations yet',
clearHistory: 'Clear History',
},
snake: {
name: 'Snake',
backToMenu: 'Back to game menu',
+9
View File
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import {
applyCalculatorUnary,
calculate,
chooseCalculatorOperator,
clearCalculator,
@@ -14,6 +15,14 @@ describe('calculator', () => {
expect(calculate(8, 2, 'multiply')).toBe(16)
expect(calculate(8, 2, 'divide')).toBe(4)
})
it('calculates scientific unary and binary operations', () => {
expect(calculate(2, 8, 'power')).toBe(256)
expect(calculate(3, 27, 'root')).toBe(3)
expect(applyCalculatorUnary(5, 'factorial')).toBe(120)
expect(applyCalculatorUnary(90, 'sin', 'degrees')).toBeCloseTo(1)
expect(applyCalculatorUnary(9, 'sqrt')).toBe(3)
expect(applyCalculatorUnary(-1, 'sqrt')).toBeNull()
})
it('chains operations and handles division by zero', () => {
let state = inputDigit(clearCalculator(), '8')
state = chooseCalculatorOperator(state, 'add')
+84 -7
View File
@@ -1,4 +1,31 @@
export type CalculatorOperator = 'add' | 'subtract' | 'multiply' | 'divide'
export type CalculatorOperator =
| 'add'
| 'subtract'
| 'multiply'
| 'divide'
| 'power'
| 'root'
export type CalculatorUnaryOperation =
| 'reciprocal'
| 'square'
| 'cube'
| 'exp'
| 'tenPower'
| 'sqrt'
| 'cbrt'
| 'ln'
| 'log10'
| 'factorial'
| 'sin'
| 'cos'
| 'tan'
| 'asin'
| 'acos'
| 'atan'
| 'sinh'
| 'cosh'
| 'tanh'
export type CalculatorState = {
accumulator: number | null
@@ -23,9 +50,11 @@ const OPERATOR_SYMBOLS: Record<CalculatorOperator, string> = {
subtract: '',
multiply: '×',
divide: '÷',
power: '^',
root: 'ʸ√',
}
function formatNumber(value: number): string {
export function formatCalculatorNumber(value: number): string {
if (!Number.isFinite(value)) return 'Error'
const rounded = Number(value.toPrecision(12))
const rendered = String(rounded)
@@ -40,8 +69,53 @@ export function calculate(
if (operator === 'add') return left + right
if (operator === 'subtract') return left - right
if (operator === 'multiply') return left * right
if (right === 0) return null
return left / right
if (operator === 'divide') return right === 0 ? null : left / right
if (operator === 'power') return left ** right
if (left === 0) return null
return right < 0 && left % 2 === 0
? null
: Math.sign(right) * Math.abs(right) ** (1 / left)
}
export function applyCalculatorUnary(
value: number,
operation: CalculatorUnaryOperation,
angleUnit: 'degrees' | 'radians' = 'radians',
): number | null {
const angle = angleUnit === 'degrees' ? (value * Math.PI) / 180 : value
let result: number
if (operation === 'reciprocal') return value === 0 ? null : 1 / value
if (operation === 'square') result = value ** 2
else if (operation === 'cube') result = value ** 3
else if (operation === 'exp') result = Math.exp(value)
else if (operation === 'tenPower') result = 10 ** value
else if (operation === 'sqrt')
result = value < 0 ? Number.NaN : Math.sqrt(value)
else if (operation === 'cbrt') result = Math.cbrt(value)
else if (operation === 'ln')
result = value <= 0 ? Number.NaN : Math.log(value)
else if (operation === 'log10')
result = value <= 0 ? Number.NaN : Math.log10(value)
else if (operation === 'factorial') {
if (value < 0 || !Number.isInteger(value) || value > 170) return null
result = 1
for (let factor = 2; factor <= value; factor += 1) result *= factor
} else if (operation === 'sin') result = Math.sin(angle)
else if (operation === 'cos') result = Math.cos(angle)
else if (operation === 'tan') result = Math.tan(angle)
else if (operation === 'asin') {
result = Math.asin(value)
if (angleUnit === 'degrees') result = (result * 180) / Math.PI
} else if (operation === 'acos') {
result = Math.acos(value)
if (angleUnit === 'degrees') result = (result * 180) / Math.PI
} else if (operation === 'atan') {
result = Math.atan(value)
if (angleUnit === 'degrees') result = (result * 180) / Math.PI
} else if (operation === 'sinh') result = Math.sinh(value)
else if (operation === 'cosh') result = Math.cosh(value)
else result = Math.tanh(value)
return Number.isFinite(result) ? result : null
}
export function clearCalculator(): CalculatorState {
@@ -94,7 +168,10 @@ export function toggleCalculatorSign(state: CalculatorState): CalculatorState {
export function calculatorPercent(state: CalculatorState): CalculatorState {
if (state.error) return state
return { ...state, display: formatNumber(Number(state.display) / 100) }
return {
...state,
display: formatCalculatorNumber(Number(state.display) / 100),
}
}
export function chooseCalculatorOperator(
@@ -132,7 +209,7 @@ export function chooseCalculatorOperator(
return {
accumulator,
calculation,
display: formatNumber(accumulator),
display: formatCalculatorNumber(accumulator),
error: false,
pendingOperator: operator,
waitingForOperand: true,
@@ -152,7 +229,7 @@ export function resolveCalculator(state: CalculatorState): CalculatorState {
return {
accumulator: null,
calculation: `${state.calculation} ${state.display} =`,
display: formatNumber(result),
display: formatCalculatorNumber(result),
error: false,
pendingOperator: null,
waitingForOperand: true,
+651 -23
View File
@@ -1,25 +1,153 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import { Calculator, Delete, History, Trash2, X } from 'lucide-vue-next'
import { useCalculatorStore } from '@/stores/calculator'
import {
useCalculatorStore,
type CalculatorHistoryEntry,
} from '@/stores/calculator'
import { usePhoneStore } from '@/stores/phone'
import { SkySheet } from '@/ui'
import type {
CalculatorOperator,
CalculatorUnaryOperation,
} from '@/utils/calculator'
type CalculatorKey = {
label: string
action: () => void
kind?: 'operator' | 'utility'
operator?: CalculatorOperator
disabled?: () => boolean
}
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' },
const historyOpened = ref(false)
const editingHistory = ref(false)
const scientificOpened = ref(true)
const resultDisplay = ref<HTMLElement | null>(null)
const expressionDisplay = ref<HTMLElement | null>(null)
const expression = computed(() =>
calculator.calculation
.replace(/\s*=\s*$/, '')
.split(' ')
.join(''),
)
function formatDisplayValue(value: string): string {
if (value === 'Error') return value
const exponentIndex = value.search(/e/i)
const mantissa = exponentIndex === -1 ? value : value.slice(0, exponentIndex)
const exponent = exponentIndex === -1 ? '' : value.slice(exponentIndex)
const negative = mantissa.startsWith('-')
const unsigned = negative ? mantissa.slice(1) : mantissa
const [integer, fraction] = unsigned.split('.')
const groupedInteger = integer.replace(/\B(?=(\d{3})+(?!\d))/g, '.')
return `${negative ? '' : ''}${groupedInteger}${fraction ? `,${fraction}` : ''}${exponent}`
}
const scientificKeys = computed<CalculatorKey[]>(() => [
{ label: '(', action: calculator.openParenthesis },
{
label: ')',
action: calculator.closeParenthesis,
disabled: () => calculator.parentFrames.length === 0,
},
{
label: 'mc',
action: calculator.memoryClear,
disabled: () => calculator.memory === 0,
},
{ label: 'm+', action: calculator.memoryAdd },
{ label: 'm', action: calculator.memorySubtract },
{
label: 'mr',
action: calculator.memoryRecall,
disabled: () => calculator.memory === 0,
},
{ label: '2ⁿᵈ', action: calculator.toggleSecond },
{
label: calculator.second ? '√x' : 'x²',
action: () => calculator.unary(calculator.second ? 'sqrt' : 'square'),
},
{
label: calculator.second ? '∛x' : 'x³',
action: () => calculator.unary(calculator.second ? 'cbrt' : 'cube'),
},
{
label: 'xʸ',
action: () => calculator.chooseOperator('power'),
operator: 'power',
},
{
label: 'eˣ',
action: () => calculator.unary('exp'),
},
{
label: '10ˣ',
action: () => calculator.unary('tenPower'),
},
{
label: '¹/x',
action: () => calculator.unary('reciprocal'),
},
{ label: '²√x', action: () => calculator.unary('sqrt') },
{ label: '³√x', action: () => calculator.unary('cbrt') },
{
label: 'ʸ√x',
action: () => calculator.chooseOperator('root'),
operator: 'root',
},
{ label: 'ln', action: () => calculator.unary('ln') },
{ label: 'log₁₀', action: () => calculator.unary('log10') },
{ label: 'x!', action: () => calculator.unary('factorial') },
{
label: calculator.second ? 'sin⁻¹' : 'sin',
action: () =>
calculator.unary(
calculator.second ? 'asin' : ('sin' as CalculatorUnaryOperation),
),
},
{
label: calculator.second ? 'cos⁻¹' : 'cos',
action: () =>
calculator.unary(
calculator.second ? 'acos' : ('cos' as CalculatorUnaryOperation),
),
},
{
label: calculator.second ? 'tan⁻¹' : 'tan',
action: () =>
calculator.unary(
calculator.second ? 'atan' : ('tan' as CalculatorUnaryOperation),
),
},
{ label: 'e', action: () => calculator.constant(Math.E) },
{ label: 'EE', action: () => calculator.chooseOperator('power') },
{ label: 'Rand', action: calculator.random },
{ label: 'sinh', action: () => calculator.unary('sinh') },
{ label: 'cosh', action: () => calculator.unary('cosh') },
{ label: 'tanh', action: () => calculator.unary('tanh') },
{ label: 'π', action: () => calculator.constant(Math.PI) },
{
label: calculator.angleUnit === 'radians' ? 'Deg' : 'Rad',
action: calculator.toggleAngleUnit,
},
])
const basicKeys: CalculatorKey[] = [
{ label: 'delete', action: calculator.backspace, kind: 'utility' },
{ label: 'C', action: calculator.clear, kind: 'utility' },
{ label: '%', action: calculator.percent, kind: 'utility' },
{
label: '÷',
action: () => calculator.chooseOperator('divide'),
kind: 'operator',
operator: 'divide',
},
...['7', '8', '9'].map((value) => ({
label: value,
@@ -29,6 +157,7 @@ const keys: Array<{ label: string; action: () => void; kind?: string }> = [
label: '×',
action: () => calculator.chooseOperator('multiply'),
kind: 'operator',
operator: 'multiply',
},
...['4', '5', '6'].map((value) => ({
label: value,
@@ -38,6 +167,7 @@ const keys: Array<{ label: string; action: () => void; kind?: string }> = [
label: '',
action: () => calculator.chooseOperator('subtract'),
kind: 'operator',
operator: 'subtract',
},
...['1', '2', '3'].map((value) => ({
label: value,
@@ -47,34 +177,532 @@ const keys: Array<{ label: string; action: () => void; kind?: string }> = [
label: '+',
action: () => calculator.chooseOperator('add'),
kind: 'operator',
operator: 'add',
},
{ label: '0', action: () => calculator.digit('0'), kind: 'zero' },
{ label: '.', action: calculator.decimal },
{ label: '+/', action: calculator.toggleSign },
{ label: '0', action: () => calculator.digit('0') },
{ label: ',', action: calculator.decimal },
{ label: '=', action: calculator.equals, kind: 'operator' },
]
const historyGroups = computed(() => {
const today = new Date()
today.setHours(0, 0, 0, 0)
const yesterday = today.getTime() - 86_400_000
const groups: Array<{ label: string; entries: CalculatorHistoryEntry[] }> = []
const todayEntries = calculator.history.filter(
(entry) => entry.createdAt >= today.getTime(),
)
const yesterdayEntries = calculator.history.filter(
(entry) =>
entry.createdAt >= yesterday && entry.createdAt < today.getTime(),
)
const olderEntries = calculator.history.filter(
(entry) => entry.createdAt < yesterday,
)
if (todayEntries.length)
groups.push({
label: phone.t('Apps.calculator.today'),
entries: todayEntries,
})
if (yesterdayEntries.length)
groups.push({
label: phone.t('Apps.calculator.yesterday'),
entries: yesterdayEntries,
})
if (olderEntries.length)
groups.push({
label: phone.t('Apps.calculator.earlier'),
entries: olderEntries,
})
return groups
})
function scrollDisplay(event: WheelEvent): void {
const display = event.currentTarget as HTMLElement
if (display.scrollWidth <= display.clientWidth) return
event.preventDefault()
display.scrollLeft +=
Math.abs(event.deltaX) > Math.abs(event.deltaY)
? event.deltaX
: event.deltaY
}
function useHistoryEntry(entry: CalculatorHistoryEntry): void {
if (editingHistory.value) {
calculator.removeHistory(entry.id)
return
}
calculator.useHistory(entry)
historyOpened.value = false
}
watch([() => calculator.display, expression], async () => {
await nextTick()
for (const display of [resultDisplay.value, expressionDisplay.value]) {
if (display) display.scrollLeft = display.scrollWidth
}
})
</script>
<template>
<main
class="native-app calculator-app"
:class="{ 'calculator-app--scientific': scientificOpened }"
:aria-label="phone.t('Apps.calculator.name')"
>
<div class="calculator-display" aria-live="polite">
<div class="calculator-result">{{ calculator.display }}</div>
<div v-if="calculation" class="calculator-calculation">
{{ calculation }}
</div>
</div>
<div class="calculator-pad">
<header class="calculator-toolbar">
<button
v-for="key in keys"
type="button"
:aria-label="phone.t('Apps.calculator.history')"
@click="historyOpened = true"
>
<History :size="24" :stroke-width="1.8" />
</button>
<button
type="button"
:aria-label="phone.t('Apps.calculator.changeMode')"
@click="scientificOpened = !scientificOpened"
>
<Calculator :size="23" :stroke-width="1.8" />
</button>
</header>
<section class="calculator-display" aria-live="polite">
<div
v-if="expression"
ref="expressionDisplay"
class="calculator-expression"
@wheel="scrollDisplay"
>
{{ expression }}
</div>
<div ref="resultDisplay" class="calculator-result" @wheel="scrollDisplay">
{{ formatDisplayValue(calculator.display) }}
</div>
</section>
<div v-if="scientificOpened" class="calculator-angle">
{{ calculator.angleUnit === 'radians' ? 'Rad' : 'Deg' }}
</div>
<section
v-if="scientificOpened"
class="calculator-scientific-pad"
:aria-label="phone.t('Apps.calculator.scientificFunctions')"
>
<button
v-for="key in scientificKeys"
:key="key.label"
type="button"
:class="key.kind"
class="calculator-key calculator-key--scientific"
:class="{
'calculator-key--selected':
(key.operator === calculator.pendingOperator &&
calculator.waitingForOperand) ||
(key.label === '2ⁿᵈ' && calculator.second),
}"
:disabled="key.disabled?.()"
@click="key.action"
>
{{ key.label }}
</button>
</div>
</section>
<section class="calculator-basic-pad">
<button
v-for="key in basicKeys"
:key="key.label"
type="button"
class="calculator-key calculator-key--basic"
:class="[
key.kind && `calculator-key--${key.kind}`,
{
'calculator-key--selected':
key.operator === calculator.pendingOperator &&
calculator.waitingForOperand,
},
]"
@click="key.action"
>
<Delete v-if="key.label === 'delete'" :size="23" />
<span v-else>{{ key.label }}</span>
</button>
</section>
<SkySheet
class="calculator-history-sheet"
:opened="historyOpened"
:aria-label="phone.t('Apps.calculator.history')"
@backdropclick="historyOpened = false"
@escape="historyOpened = false"
>
<section class="calculator-history">
<div class="calculator-history__handle"></div>
<header>
<button
type="button"
class="calculator-history__edit"
@click="editingHistory = !editingHistory"
>
{{
phone.t(
editingHistory
? 'Apps.calculator.done'
: 'Apps.calculator.edit',
)
}}
</button>
<button
type="button"
class="calculator-history__close"
:aria-label="phone.t('Apps.calculator.closeHistory')"
@click="historyOpened = false"
>
<X :size="23" />
</button>
</header>
<div v-if="historyGroups.length" class="calculator-history__scroll">
<section v-for="group in historyGroups" :key="group.label">
<h2>{{ group.label }}</h2>
<button
v-for="entry in group.entries"
:key="entry.id"
type="button"
class="calculator-history__entry"
@click="useHistoryEntry(entry)"
>
<Trash2 v-if="editingHistory" :size="18" />
<span>
<small>{{ entry.expression.split(' ').join('') }}</small>
<strong>{{ formatDisplayValue(entry.result) }}</strong>
</span>
</button>
</section>
</div>
<div v-else class="calculator-history__empty">
<History :size="34" />
<strong>{{ phone.t('Apps.calculator.noHistory') }}</strong>
</div>
<button
v-if="editingHistory && calculator.history.length"
type="button"
class="calculator-history__clear"
@click="calculator.clearHistory"
>
{{ phone.t('Apps.calculator.clearHistory') }}
</button>
</section>
</SkySheet>
</main>
</template>
<style scoped>
.calculator-app {
--calculator-orange: #ff9500;
padding: calc(var(--sky-safe-area-top) + 5px) 15px
calc(var(--sky-safe-area-bottom) + 4px);
display: flex;
flex-direction: column;
gap: 8px;
background: #000;
color: #fff;
}
.calculator-toolbar {
min-height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
}
.calculator-toolbar button,
.calculator-history__close {
width: 44px;
height: 44px;
border: 1px solid rgb(255 255 255 / 10%);
border-radius: 50%;
display: grid;
place-items: center;
background: linear-gradient(145deg, #202022, #101011);
box-shadow: inset 0 1px rgb(255 255 255 / 9%);
color: #f5f5f7;
}
.calculator-display {
min-height: 132px;
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end;
overflow: hidden;
}
.calculator-expression,
.calculator-result {
width: 100%;
overflow-x: auto;
overflow-y: hidden;
text-align: right;
white-space: nowrap;
scrollbar-width: none;
}
.calculator-expression::-webkit-scrollbar,
.calculator-result::-webkit-scrollbar {
display: none;
}
.calculator-expression {
color: #8e8e93;
font-size: 19px;
}
.calculator-result {
font-size: 54px;
font-weight: 280;
letter-spacing: -2px;
line-height: 1.05;
}
.calculator-angle {
height: 20px;
display: flex;
align-items: center;
color: #dedee2;
font-size: 13px;
}
.calculator-scientific-pad,
.calculator-basic-pad {
display: grid;
gap: 6px;
}
.calculator-scientific-pad {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.calculator-basic-pad {
flex: 1;
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.calculator-key {
min-width: 0;
border: 1px solid rgb(255 255 255 / 10%);
display: grid;
place-items: center;
background: linear-gradient(145deg, #29292b, #1a1a1c);
box-shadow: inset 0 1px rgb(255 255 255 / 7%);
color: #f5f5f7;
font: inherit;
cursor: pointer;
transition:
transform 130ms ease,
filter 150ms ease,
border-color 150ms ease;
}
.calculator-key--scientific {
min-height: 38px;
border-radius: 19px;
font-size: 15px;
}
.calculator-key--basic {
min-height: 45px;
border-radius: 23px;
font-size: 25px;
font-weight: 390;
}
.calculator-key--utility {
background: linear-gradient(145deg, #79797c, #58585b);
}
.calculator-key--operator {
border-color: #ffb23b;
background: linear-gradient(180deg, #ffa20c, var(--calculator-orange));
box-shadow: inset 0 1px rgb(255 255 255 / 28%);
}
.calculator-key--selected {
background: #f5f5f7;
color: var(--calculator-orange);
}
.calculator-key:disabled {
opacity: 0.35;
}
@media (hover: hover) {
.calculator-key:hover:not(:disabled),
.calculator-toolbar button:hover,
.calculator-history button:hover {
transform: translateY(-1px);
filter: brightness(1.1);
}
}
.calculator-key:active:not(:disabled),
.calculator-toolbar button:active,
.calculator-history button:active {
transform: scale(0.96);
filter: brightness(0.88);
}
.calculator-key:focus-visible,
.calculator-toolbar button:focus-visible,
.calculator-history button:focus-visible {
outline: 2px solid #ffd08a;
outline-offset: 2px;
}
.calculator-app:not(.calculator-app--scientific) .calculator-display {
min-height: 250px;
}
.calculator-app:not(.calculator-app--scientific) .calculator-basic-pad {
gap: 11px 14px;
}
.calculator-app:not(.calculator-app--scientific) .calculator-key--basic {
min-height: 62px;
border-radius: 31px;
font-size: 29px;
}
:deep(.calculator-history-sheet .sky-overlay-backdrop) {
background: rgb(0 0 0 / 60%);
}
:deep(.calculator-history-sheet .sky-sheet__panel) {
height: 72%;
max-height: 72%;
overflow: hidden;
border-color: rgb(255 255 255 / 15%);
border-radius: 31px 31px 0 0;
background: rgb(25 25 27 / 97%);
color: #f5f5f7;
box-shadow: 0 -10px 40px rgb(0 0 0 / 48%);
}
.calculator-history {
height: 100%;
padding: 8px 18px calc(var(--sky-safe-area-bottom) + 8px);
display: flex;
flex-direction: column;
}
.calculator-history__handle {
width: 40px;
height: 5px;
margin: 0 auto 8px;
border-radius: 999px;
background: #77777b;
}
.calculator-history > header {
display: flex;
align-items: center;
justify-content: space-between;
}
.calculator-history__edit {
min-width: 106px;
height: 42px;
border: 0;
border-radius: 21px;
background: #262628;
color: #fff;
font: inherit;
font-size: 15px;
}
.calculator-history__scroll {
min-height: 0;
margin-top: 18px;
flex: 1;
overflow-y: auto;
scrollbar-width: none;
}
.calculator-history__scroll::-webkit-scrollbar {
display: none;
}
.calculator-history__scroll section + section {
margin-top: 18px;
}
.calculator-history h2 {
margin: 0;
padding: 0 2px 10px;
border-bottom: 1px solid rgb(255 255 255 / 16%);
color: #a5a5aa;
font-size: 18px;
}
.calculator-history__entry {
width: 100%;
min-height: 66px;
border: 0;
border-bottom: 1px solid rgb(255 255 255 / 14%);
padding: 10px 2px;
display: flex;
align-items: center;
gap: 12px;
background: transparent;
color: #fff;
text-align: left;
}
.calculator-history__entry span,
.calculator-history__entry small,
.calculator-history__entry strong {
display: block;
}
.calculator-history__entry small {
margin-bottom: 4px;
color: #a5a5aa;
font-size: 13px;
}
.calculator-history__entry strong {
font-size: 18px;
font-weight: 450;
}
.calculator-history__entry > svg {
color: #ff453a;
}
.calculator-history__empty {
flex: 1;
display: grid;
place-content: center;
justify-items: center;
gap: 10px;
color: #8e8e93;
}
.calculator-history__clear {
min-height: 44px;
border: 0;
background: transparent;
color: #ff453a;
font: inherit;
}
@media (prefers-reduced-motion: reduce) {
.calculator-key,
.calculator-toolbar button,
.calculator-history button {
transition: none;
}
}
</style>
+5 -1
View File
@@ -1141,7 +1141,11 @@ Locales["en"] = {
device_locked = "Unlock the phone to manage your homes.", request_failed = "The housing request failed.", default = "The housing request failed.",
},
},
calculator = { name = "Calculator" },
calculator = {
name = "Calculator", history = "Calculation history", changeMode = "Change calculator mode",
scientificFunctions = "Scientific functions", edit = "Edit", done = "Done", closeHistory = "Close history",
today = "Today", yesterday = "Yesterday", earlier = "Earlier", noHistory = "No calculations yet", clearHistory = "Clear History",
},
snake = {
name = "Snake", backToMenu = "Back to game menu", board = "Snake game board",
controls = "Direction controls",