feat(calc): add copy-to-clipboard, skins in scientific, Dockerfile, settings

- Copy-to-clipboard button on all 5 skins (Modern, HP-35, Casio, TI-84, Minimal)
- Scientific calculator now supports skin switching like standard mode
- Production Dockerfile using sveltekit-base (port 5018)
- Real settings page: default mode/skin, decimal places, thousands separator,
  angle mode, history size, keyboard hints, shortcuts reference

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-29 08:48:21 +02:00
parent 49df3ead09
commit 0841f6b334
11 changed files with 709 additions and 97 deletions

View file

@ -0,0 +1,30 @@
# syntax=docker/dockerfile:1
FROM sveltekit-base:local AS builder
ARG PUBLIC_MANA_CORE_AUTH_URL=http://mana-auth:3001
ENV PUBLIC_MANA_CORE_AUTH_URL=$PUBLIC_MANA_CORE_AUTH_URL
COPY apps/calc/apps/web ./apps/calc/apps/web
COPY apps/calc/packages ./apps/calc/packages
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
pnpm install --no-frozen-lockfile --ignore-scripts
WORKDIR /app/apps/calc/apps/web
RUN pnpm exec svelte-kit sync
RUN NODE_OPTIONS="--max-old-space-size=4096" pnpm build
FROM node:20-alpine AS production
WORKDIR /app/apps/calc/apps/web
COPY --from=builder /app/node_modules/.pnpm /app/node_modules/.pnpm
COPY --from=builder /app/apps/calc/apps/web/node_modules ./node_modules
COPY --from=builder /app/apps/calc/apps/web/build ./build
COPY --from=builder /app/apps/calc/apps/web/package.json ./
EXPOSE 5018
ENV NODE_ENV=production PORT=5018 HOST=0.0.0.0
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:5018/health || exit 1
CMD ["node", "build"]

View file

@ -1,8 +1,17 @@
<script lang="ts">
import type { CalcSkinProps } from './types';
let { expression, display, error, onButton, onClear, onBackspace, onEquals }: CalcSkinProps =
$props();
let {
expression,
display,
error,
copied,
onButton,
onClear,
onBackspace,
onEquals,
onCopy,
}: CalcSkinProps = $props();
const buttons = [
['C', '(', ')', '%'],
@ -41,8 +50,15 @@
<!-- LCD Display (green-gray) -->
<div class="casio-display">
<div class="casio-expression">{expression || ' '}</div>
<div class="casio-result" class:casio-error={!!error}>
{error || display}
<div style="display: flex; align-items: flex-end; gap: 4px;">
<div class="casio-result" style="flex: 1;" class:casio-error={!!error}>
{error || display}
</div>
{#if display !== '0' && !error}
<button class="casio-copy" onclick={onCopy} title="Kopieren">
{copied ? '✓' : '⎘'}
</button>
{/if}
</div>
</div>
@ -166,6 +182,20 @@
font-size: 16px;
}
.casio-copy {
background: none;
border: none;
color: #3a4a2a;
opacity: 0.4;
font-size: 14px;
cursor: pointer;
padding: 2px 4px;
}
.casio-copy:hover {
opacity: 0.8;
}
.casio-keypad {
display: grid;
grid-template-columns: repeat(4, 1fr);

View file

@ -1,8 +1,17 @@
<script lang="ts">
import type { CalcSkinProps } from './types';
let { expression, display, error, onButton, onClear, onBackspace, onEquals }: CalcSkinProps =
$props();
let {
expression,
display,
error,
copied,
onButton,
onClear,
onBackspace,
onEquals,
onCopy,
}: CalcSkinProps = $props();
// HP-35 had a distinctive layout - we adapt it for standard calc use
const buttons = [
@ -36,8 +45,15 @@
<!-- LED Display (red on dark) -->
<div class="hp35-display">
<div class="hp35-expression">{expression || ' '}</div>
<div class="hp35-result" class:hp35-error={!!error}>
{error || display}
<div style="display: flex; align-items: flex-end; gap: 6px;">
<div class="hp35-result" style="flex: 1;" class:hp35-error={!!error}>
{error || display}
</div>
{#if display !== '0' && !error}
<button class="hp35-copy" onclick={onCopy} title="Kopieren">
{copied ? '✓' : '⎘'}
</button>
{/if}
</div>
</div>
@ -203,6 +219,20 @@
color: #aaccff;
}
.hp35-copy {
background: none;
border: none;
color: #ff3333;
opacity: 0.5;
font-size: 14px;
cursor: pointer;
padding: 2px 4px;
}
.hp35-copy:hover {
opacity: 1;
}
.hp35-backspace {
width: 100%;
margin-top: 8px;

View file

@ -1,8 +1,17 @@
<script lang="ts">
import type { CalcSkinProps } from './types';
let { expression, display, error, onButton, onClear, onBackspace, onEquals }: CalcSkinProps =
$props();
let {
expression,
display,
error,
copied,
onButton,
onClear,
onBackspace,
onEquals,
onCopy,
}: CalcSkinProps = $props();
const buttons = [
['C', '(', ')', '%'],
@ -23,8 +32,15 @@
<!-- Display: just big text -->
<div class="minimal-display">
<div class="minimal-expression">{expression || ' '}</div>
<div class="minimal-result" class:minimal-error={!!error}>
{error || display}
<div style="display: flex; align-items: flex-end; gap: 4px; justify-content: flex-end;">
<div class="minimal-result" style="flex: 1;" class:minimal-error={!!error}>
{error || display}
</div>
{#if display !== '0' && !error}
<button class="minimal-copy" onclick={onCopy}>
{copied ? '✓' : '⎘'}
</button>
{/if}
</div>
</div>
@ -142,6 +158,20 @@
color: hsl(var(--muted-foreground));
}
.minimal-copy {
background: none;
border: none;
color: hsl(var(--muted-foreground));
opacity: 0.3;
font-size: 16px;
cursor: pointer;
padding: 2px;
}
.minimal-copy:hover {
opacity: 0.7;
}
.minimal-backspace:hover {
background: hsl(var(--muted));
}

View file

@ -1,8 +1,17 @@
<script lang="ts">
import type { CalcSkinProps } from './types';
let { expression, display, error, onButton, onClear, onBackspace, onEquals }: CalcSkinProps =
$props();
let {
expression,
display,
error,
copied,
onButton,
onClear,
onBackspace,
onEquals,
onCopy,
}: CalcSkinProps = $props();
const buttons = [
['C', '(', ')', '%'],
@ -32,11 +41,22 @@
<div class="text-sm text-muted-foreground min-h-[1.5rem] font-mono truncate">
{expression || ' '}
</div>
<div
class="text-4xl font-bold text-foreground font-mono text-right tabular-nums truncate"
class:text-red-400={!!error}
>
{error || display}
<div class="flex items-end gap-2">
<div
class="flex-1 text-4xl font-bold text-foreground font-mono text-right tabular-nums truncate"
class:text-red-400={!!error}
>
{error || display}
</div>
{#if display !== '0' && !error}
<button
class="shrink-0 p-1.5 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted/50 transition-colors text-xs"
onclick={onCopy}
title="Kopieren"
>
{copied ? '✓' : '⎘'}
</button>
{/if}
</div>
</div>

View file

@ -1,8 +1,17 @@
<script lang="ts">
import type { CalcSkinProps } from './types';
let { expression, display, error, onButton, onClear, onBackspace, onEquals }: CalcSkinProps =
$props();
let {
expression,
display,
error,
copied,
onButton,
onClear,
onBackspace,
onEquals,
onCopy,
}: CalcSkinProps = $props();
const buttons = [
['C', '(', ')', '%'],
@ -35,8 +44,15 @@
<div class="ti84-screen">
<div class="ti84-screen-inner">
<div class="ti84-expression">{expression || ' '}</div>
<div class="ti84-result" class:ti84-error={!!error}>
{error || display}
<div style="display: flex; align-items: flex-end; gap: 6px;">
<div class="ti84-result" style="flex: 1;" class:ti84-error={!!error}>
{error || display}
</div>
{#if display !== '0' && !error}
<button class="ti84-copy" onclick={onCopy} title="Kopieren">
{copied ? '✓' : '⎘'}
</button>
{/if}
</div>
</div>
</div>
@ -157,6 +173,20 @@
font-size: 16px;
}
.ti84-copy {
background: none;
border: none;
color: #aaffaa;
opacity: 0.4;
font-size: 14px;
cursor: pointer;
padding: 2px 4px;
}
.ti84-copy:hover {
opacity: 1;
}
.ti84-nav {
display: flex;
gap: 6px;

View file

@ -5,8 +5,10 @@ export interface CalcSkinProps {
expression: string;
display: string;
error: string;
copied: boolean;
onButton: (btn: string) => void;
onClear: () => void;
onBackspace: () => void;
onEquals: () => void;
onCopy: () => void;
}

View file

@ -0,0 +1,65 @@
/**
* Calc-specific settings persisted to localStorage.
*/
import { browser } from '$app/environment';
import type { CalculatorMode, CalculatorSkin } from '@calc/shared';
const STORAGE_KEY = 'calc-settings';
interface CalcSettings {
defaultMode: CalculatorMode;
defaultSkin: CalculatorSkin;
decimalPlaces: number;
thousandsSeparator: boolean;
angleMode: 'deg' | 'rad';
historySize: number;
showKeyboardHints: boolean;
}
const DEFAULTS: CalcSettings = {
defaultMode: 'standard',
defaultSkin: 'modern',
decimalPlaces: 10,
thousandsSeparator: false,
angleMode: 'rad',
historySize: 50,
showKeyboardHints: true,
};
function load(): CalcSettings {
if (!browser) return { ...DEFAULTS };
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) return { ...DEFAULTS, ...JSON.parse(raw) };
} catch {}
return { ...DEFAULTS };
}
function save(settings: CalcSettings) {
if (!browser) return;
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
// Reactive settings store using Svelte 5 runes
let current = $state<CalcSettings>(load());
export const calcSettings = {
get value() {
return current;
},
update(partial: Partial<CalcSettings>) {
current = { ...current, ...partial };
save(current);
},
reset() {
current = { ...DEFAULTS };
save(current);
},
get defaults() {
return DEFAULTS;
},
};

View file

@ -2,8 +2,9 @@
import { getContext } from 'svelte';
import { evaluate, formatResult } from '$lib/engine/evaluate';
import { calculationsStore } from '$lib/stores/calculations.svelte';
import { SCIENTIFIC_CONSTANTS } from '@calc/shared/constants';
import type { Calculation } from '@calc/shared';
import { CALCULATOR_SKINS, SCIENTIFIC_CONSTANTS } from '@calc/shared/constants';
import type { CalculatorSkin, Calculation } from '@calc/shared';
import { ModernSkin, HP35Skin, CasioSkin, TI84Skin, MinimalSkin } from '$lib/components/skins';
const allCalculations = getContext<{ value: Calculation[] }>('calculations');
@ -11,7 +12,37 @@
let display = $state('0');
let hasResult = $state(false);
let error = $state('');
let copied = $state(false);
let angleMode = $state<'deg' | 'rad'>('rad');
let showExtraKeys = $state(true);
// Skin state
let activeSkin = $state<CalculatorSkin>('modern');
let showSkinPicker = $state(false);
if (typeof localStorage !== 'undefined') {
const saved = localStorage.getItem('calc-skin');
if (saved && CALCULATOR_SKINS.some((s) => s.id === saved)) {
activeSkin = saved as CalculatorSkin;
}
}
function setSkin(skin: CalculatorSkin) {
activeSkin = skin;
showSkinPicker = false;
if (typeof localStorage !== 'undefined') {
localStorage.setItem('calc-skin', skin);
}
}
async function copyToClipboard() {
if (display === '0' || error) return;
try {
await navigator.clipboard.writeText(display);
copied = true;
setTimeout(() => (copied = false), 1500);
} catch {}
}
function append(char: string) {
if (hasResult && /[0-9.]/.test(char)) {
@ -52,6 +83,7 @@
mode: 'scientific',
expression,
result: formatted,
skin: activeSkin,
});
display = formatted;
hasResult = true;
@ -61,37 +93,32 @@
}
}
function handleButton(btn: string) {
if (btn === 'C') clear();
else if (btn === '=') calculate();
else if (btn === 'DEL') backspace();
else append(btn);
}
let recentHistory = $derived(
allCalculations.value.filter((c) => c.mode === 'scientific').slice(0, 8)
);
const sciButtons = [
let skinProps = $derived({
expression,
display,
error,
copied,
onButton: append,
onClear: clear,
onBackspace: backspace,
onEquals: calculate,
onCopy: copyToClipboard,
});
const sciExtraButtons = [
['sin(', 'cos(', 'tan(', 'π'],
['asin(', 'acos(', 'atan(', 'e'],
['log(', 'ln(', 'sqrt(', '^'],
['(', ')', '!', '%'],
['7', '8', '9', '/'],
['4', '5', '6', '*'],
['1', '2', '3', '-'],
['0', '.', '=', '+'],
];
function getButtonClass(btn: string): string {
if (btn === '=') return 'bg-pink-500 text-white hover:bg-pink-600 font-bold';
function getSciButtonClass(btn: string): string {
if (['sin(', 'cos(', 'tan(', 'asin(', 'acos(', 'atan(', 'log(', 'ln(', 'sqrt('].includes(btn))
return 'bg-violet-500/20 text-violet-400 hover:bg-violet-500/30 text-xs';
if (['π', 'e', '^', '!', '%'].includes(btn))
return 'bg-muted text-foreground hover:bg-muted/80';
if (['+', '-', '*', '/', '(', ')'].includes(btn))
return 'bg-muted text-foreground hover:bg-muted/80';
return 'bg-card text-foreground hover:bg-card/80';
return 'bg-muted text-foreground hover:bg-muted/80';
}
</script>
@ -100,65 +127,101 @@
</svelte:head>
<div class="scientific-page">
<div class="calculator">
<div class="display rounded-xl bg-card border border-border p-4 mb-4">
<div class="flex justify-between items-center mb-1">
<span class="text-xs text-muted-foreground font-mono truncate flex-1"
>{expression || ' '}</span
>
<div class="calculator-column">
<!-- Skin picker + angle mode -->
<div class="flex items-center justify-between mb-3 gap-2">
<button
class="skin-toggle text-xs px-3 py-1.5 rounded-full border transition-all
{showSkinPicker
? 'bg-pink-500 text-white border-pink-500'
: 'bg-card border-border text-muted-foreground hover:bg-muted'}"
onclick={() => (showSkinPicker = !showSkinPicker)}
>
🎨 {CALCULATOR_SKINS.find((s) => s.id === activeSkin)?.label || 'Modern'}
</button>
<div class="flex gap-1">
<button
class="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground hover:bg-muted/80"
class="text-xs px-2 py-1 rounded bg-muted text-muted-foreground hover:bg-muted/80"
onclick={() => (angleMode = angleMode === 'rad' ? 'deg' : 'rad')}
>
{angleMode.toUpperCase()}
</button>
</div>
<div
class="text-3xl font-bold text-foreground font-mono text-right tabular-nums truncate"
class:text-red-400={!!error}
>
{error || display}
</div>
</div>
<!-- Constants quick insert -->
<div class="flex gap-1 mb-3 overflow-x-auto pb-1">
{#each SCIENTIFIC_CONSTANTS.slice(0, 6) as constant}
<button
class="shrink-0 text-xs px-2 py-1 rounded-lg bg-muted/50 text-muted-foreground hover:bg-muted transition-colors"
onclick={() => append(String(constant.value))}
title={constant.name}
class="text-xs px-2 py-1 rounded transition-all {showExtraKeys
? 'bg-violet-500 text-white'
: 'bg-muted text-muted-foreground hover:bg-muted/80'}"
onclick={() => (showExtraKeys = !showExtraKeys)}
>
{constant.symbol}
f(x)
</button>
{/each}
</div>
</div>
<div class="grid grid-cols-4 gap-1.5">
{#each sciButtons as row}
{#each row as btn}
<button
class="calc-btn h-12 rounded-lg border border-border transition-all active:scale-95 {getButtonClass(
btn
)}"
onclick={() => handleButton(btn)}
>
{btn === '/' ? '÷' : btn === '*' ? '×' : btn}
</button>
{/each}
{/each}
</div>
{#if showSkinPicker}
<div class="skin-picker mb-4 p-3 rounded-xl bg-card border border-border">
<div class="grid grid-cols-5 gap-2">
{#each CALCULATOR_SKINS as skin}
<button
class="skin-option p-2 rounded-lg text-center transition-all border
{activeSkin === skin.id ? 'border-pink-500 bg-pink-500/10' : 'border-transparent hover:bg-muted'}"
onclick={() => setSkin(skin.id)}
>
<div class="text-sm font-medium text-foreground">{skin.label}</div>
{#if skin.year}
<div class="text-xs text-muted-foreground">{skin.year}</div>
{/if}
</button>
{/each}
</div>
</div>
{/if}
<div class="flex gap-2 mt-2">
<button
class="flex-1 h-9 rounded-lg bg-red-500/20 text-red-400 hover:bg-red-500/30 text-sm"
onclick={clear}>C</button
>
<button
class="flex-1 h-9 rounded-lg bg-muted/50 text-muted-foreground hover:bg-muted text-sm"
onclick={backspace}>← DEL</button
>
</div>
<!-- Scientific extra keys (above the skin) -->
{#if showExtraKeys}
<div class="mb-3 space-y-1.5">
<!-- Constants -->
<div class="flex gap-1 overflow-x-auto pb-1">
{#each SCIENTIFIC_CONSTANTS.slice(0, 6) as constant}
<button
class="shrink-0 text-xs px-2 py-1 rounded-lg bg-muted/50 text-muted-foreground hover:bg-muted transition-colors"
onclick={() => append(String(constant.value))}
title={constant.name}
>
{constant.symbol}
</button>
{/each}
</div>
<!-- Function buttons -->
<div class="grid grid-cols-4 gap-1.5">
{#each sciExtraButtons as row}
{#each row as btn}
<button
class="h-9 rounded-lg border border-border transition-all active:scale-95 {getSciButtonClass(
btn
)}"
onclick={() => append(btn)}
>
{btn}
</button>
{/each}
{/each}
</div>
</div>
{/if}
<!-- Active Skin (handles standard buttons: 0-9, +-*/%, =, C, backspace) -->
{#if activeSkin === 'modern'}
<ModernSkin {...skinProps} />
{:else if activeSkin === 'hp35'}
<HP35Skin {...skinProps} />
{:else if activeSkin === 'casio-fx'}
<CasioSkin {...skinProps} />
{:else if activeSkin === 'ti84'}
<TI84Skin {...skinProps} />
{:else if activeSkin === 'minimal'}
<MinimalSkin {...skinProps} />
{/if}
</div>
<!-- History -->
@ -188,7 +251,7 @@
<style>
.scientific-page {
max-width: 700px;
max-width: 750px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 180px;
@ -196,15 +259,15 @@
align-items: start;
}
.calculator {
max-width: 380px;
.calculator-column {
max-width: 400px;
}
@media (max-width: 640px) {
.scientific-page {
grid-template-columns: 1fr;
}
.calculator {
.calculator-column {
max-width: 100%;
}
}

View file

@ -0,0 +1,300 @@
<script lang="ts">
import { calcSettings } from '$lib/stores/calc-settings.svelte';
import { CALCULATOR_SKINS, CALCULATOR_MODES } from '@calc/shared/constants';
import type { CalculatorMode, CalculatorSkin } from '@calc/shared';
let settings = $derived(calcSettings.value);
function updateDefaultMode(mode: CalculatorMode) {
calcSettings.update({ defaultMode: mode });
}
function updateDefaultSkin(skin: CalculatorSkin) {
calcSettings.update({ defaultSkin: skin });
// Also update the shared localStorage key
if (typeof localStorage !== 'undefined') {
localStorage.setItem('calc-skin', skin);
}
}
function updateDecimalPlaces(event: Event) {
const val = parseInt((event.target as HTMLInputElement).value);
if (val >= 1 && val <= 15) {
calcSettings.update({ decimalPlaces: val });
}
}
function updateHistorySize(event: Event) {
const val = parseInt((event.target as HTMLInputElement).value);
if (val >= 10 && val <= 500) {
calcSettings.update({ historySize: val });
}
}
function toggleThousandsSeparator() {
calcSettings.update({ thousandsSeparator: !settings.thousandsSeparator });
}
function toggleAngleMode() {
calcSettings.update({ angleMode: settings.angleMode === 'rad' ? 'deg' : 'rad' });
}
function toggleKeyboardHints() {
calcSettings.update({ showKeyboardHints: !settings.showKeyboardHints });
}
function resetAll() {
calcSettings.reset();
if (typeof localStorage !== 'undefined') {
localStorage.setItem('calc-skin', 'modern');
}
}
</script>
<svelte:head>
<title>Calc - Einstellungen</title>
</svelte:head>
<div class="settings-page">
<header class="mb-8">
<h1 class="text-2xl font-bold text-foreground">Einstellungen</h1>
<p class="text-muted-foreground text-sm mt-1">Passe Calc an deine Bedürfnisse an</p>
</header>
<!-- General -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Allgemein</h2>
<!-- Default Mode -->
<div class="setting-row">
<div>
<div class="setting-label">Standard-Modus</div>
<div class="setting-desc">Wird beim Öffnen der App gezeigt</div>
</div>
<select
value={settings.defaultMode}
onchange={(e) => updateDefaultMode((e.target as HTMLSelectElement).value as CalculatorMode)}
class="h-9 px-3 rounded-lg bg-background border border-border text-foreground text-sm"
>
{#each CALCULATOR_MODES as mode}
<option value={mode.id}>{mode.label.de}</option>
{/each}
</select>
</div>
<!-- Default Skin -->
<div class="setting-row">
<div>
<div class="setting-label">Standard-Skin</div>
<div class="setting-desc">Look des Rechners</div>
</div>
<select
value={settings.defaultSkin}
onchange={(e) => updateDefaultSkin((e.target as HTMLSelectElement).value as CalculatorSkin)}
class="h-9 px-3 rounded-lg bg-background border border-border text-foreground text-sm"
>
{#each CALCULATOR_SKINS as skin}
<option value={skin.id}>{skin.label}{skin.year ? ` (${skin.year})` : ''}</option>
{/each}
</select>
</div>
</section>
<!-- Calculation -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Berechnung</h2>
<!-- Decimal Places -->
<div class="setting-row">
<div>
<div class="setting-label">Dezimalstellen</div>
<div class="setting-desc">Genauigkeit der Ergebnisse (115)</div>
</div>
<div class="flex items-center gap-2">
<input
type="range"
min="1"
max="15"
value={settings.decimalPlaces}
oninput={updateDecimalPlaces}
class="w-24"
/>
<span class="text-sm font-mono text-foreground w-6 text-center"
>{settings.decimalPlaces}</span
>
</div>
</div>
<!-- Thousands Separator -->
<div class="setting-row">
<div>
<div class="setting-label">Tausendertrennzeichen</div>
<div class="setting-desc">1.000.000 statt 1000000</div>
</div>
<button
class="toggle-btn {settings.thousandsSeparator ? 'toggle-on' : 'toggle-off'}"
onclick={toggleThousandsSeparator}
role="switch"
aria-checked={settings.thousandsSeparator}
>
<span class="toggle-knob"></span>
</button>
</div>
<!-- Angle Mode -->
<div class="setting-row">
<div>
<div class="setting-label">Winkel-Modus</div>
<div class="setting-desc">Für sin, cos, tan im wissenschaftlichen Rechner</div>
</div>
<button
class="h-9 px-4 rounded-lg border transition-all text-sm font-medium
{settings.angleMode === 'rad'
? 'bg-violet-500 text-white border-violet-500'
: 'bg-amber-500 text-white border-amber-500'}"
onclick={toggleAngleMode}
>
{settings.angleMode === 'rad' ? 'Radiant' : 'Grad'}
</button>
</div>
</section>
<!-- Display -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Anzeige</h2>
<!-- History Size -->
<div class="setting-row">
<div>
<div class="setting-label">Verlauf-Größe</div>
<div class="setting-desc">Maximale Anzahl gespeicherter Berechnungen</div>
</div>
<div class="flex items-center gap-2">
<input
type="number"
min="10"
max="500"
step="10"
value={settings.historySize}
oninput={updateHistorySize}
class="w-20 h-9 px-2 rounded-lg bg-background border border-border text-foreground font-mono text-sm text-center"
/>
</div>
</div>
<!-- Keyboard Hints -->
<div class="setting-row">
<div>
<div class="setting-label">Tastatur-Hinweise</div>
<div class="setting-desc">Zeige Keyboard-Shortcuts in der UI</div>
</div>
<button
class="toggle-btn {settings.showKeyboardHints ? 'toggle-on' : 'toggle-off'}"
onclick={toggleKeyboardHints}
role="switch"
aria-checked={settings.showKeyboardHints}
>
<span class="toggle-knob"></span>
</button>
</div>
</section>
<!-- Keyboard shortcuts reference -->
<section class="settings-section">
<h2 class="text-lg font-bold text-foreground mb-4">Tastaturkürzel</h2>
<div class="grid grid-cols-2 gap-2">
{#each [['09, .', 'Ziffern eingeben'], ['+ - * /', 'Operatoren'], ['( )', 'Klammern'], ['Enter / =', 'Berechnen'], ['Backspace', 'Letzte Stelle löschen'], ['Esc / C', 'Alles löschen'], ['Cmd+K', 'Schnellzugriff'], ['Cmd+19', 'Navigation']] as [key, desc]}
<div class="flex items-center gap-3 p-2 rounded-lg bg-card border border-border">
<kbd class="shrink-0 px-2 py-0.5 rounded bg-muted text-xs font-mono text-muted-foreground"
>{key}</kbd
>
<span class="text-sm text-foreground">{desc}</span>
</div>
{/each}
</div>
</section>
<!-- Reset -->
<section class="settings-section">
<button
class="px-4 py-2 rounded-lg bg-red-500/10 text-red-400 hover:bg-red-500/20 transition-colors text-sm"
onclick={resetAll}
>
Alle Einstellungen zurücksetzen
</button>
</section>
</div>
<style>
.settings-page {
max-width: 600px;
margin: 0 auto;
}
.settings-section {
margin-bottom: 2rem;
padding-bottom: 2rem;
border-bottom: 1px solid hsl(var(--border));
}
.settings-section:last-child {
border-bottom: none;
}
.setting-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.75rem 0;
}
.setting-label {
font-size: 0.875rem;
font-weight: 500;
color: hsl(var(--foreground));
}
.setting-desc {
font-size: 0.75rem;
color: hsl(var(--muted-foreground));
margin-top: 2px;
}
.toggle-btn {
position: relative;
width: 44px;
height: 24px;
border-radius: 12px;
border: none;
cursor: pointer;
transition: background 0.2s;
flex-shrink: 0;
}
.toggle-on {
background: #ec4899;
}
.toggle-off {
background: hsl(var(--muted));
}
.toggle-knob {
position: absolute;
top: 2px;
width: 20px;
height: 20px;
border-radius: 10px;
background: white;
transition: left 0.2s;
}
.toggle-on .toggle-knob {
left: 22px;
}
.toggle-off .toggle-knob {
left: 2px;
}
</style>

View file

@ -12,6 +12,16 @@
let display = $state('0');
let hasResult = $state(false);
let error = $state('');
let copied = $state(false);
async function copyToClipboard() {
if (display === '0' || error) return;
try {
await navigator.clipboard.writeText(display);
copied = true;
setTimeout(() => (copied = false), 1500);
} catch {}
}
// Skin state — persisted to localStorage
let activeSkin = $state<CalculatorSkin>('modern');
@ -123,10 +133,12 @@
expression,
display,
error,
copied,
onButton: appendToExpression,
onClear: clear,
onBackspace: backspace,
onEquals: calculate,
onCopy: copyToClipboard,
});
</script>