From 2a43b599474d6bcbf29bb5f98be00814f542b47c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sun, 14 Jun 2026 21:57:52 +0200 Subject: [PATCH] tablet: split price/pfand, light mode, bigger bold text - show drink price and Pfand separately on tiles and in cart - add light/dark toggle on the bar-picker page (persisted) - theme via CSS variables, applied on load for tablet + admin - larger, bolder text throughout; much bigger total sum --- client/src/admin/main.tsx | 2 + client/src/api.ts | 16 ++++++ client/src/styles.css | 90 ++++++++++++++++++++++++--------- client/src/tablet/BarPicker.tsx | 13 +++++ client/src/tablet/Sale.tsx | 16 ++++-- client/src/tablet/main.tsx | 2 + 6 files changed, 111 insertions(+), 28 deletions(-) diff --git a/client/src/admin/main.tsx b/client/src/admin/main.tsx index 25a00c9..26f6d53 100644 --- a/client/src/admin/main.tsx +++ b/client/src/admin/main.tsx @@ -1,4 +1,6 @@ import { render } from 'preact'; import { Admin } from './Admin'; +import { applyTheme } from '../api'; +applyTheme(); render(, document.getElementById('app')!); diff --git a/client/src/api.ts b/client/src/api.ts index 97ae68e..21a12d2 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -27,6 +27,22 @@ export function formatCents(cents: number): string { return `${sign}${(abs / 100).toFixed(2)} €`; } +export type Theme = 'dark' | 'light'; +const THEME_KEY = 'wutz.theme'; + +export function getTheme(): Theme { + return localStorage.getItem(THEME_KEY) === 'light' ? 'light' : 'dark'; +} + +export function applyTheme(t: Theme = getTheme()): void { + document.documentElement.setAttribute('data-theme', t); +} + +export function setTheme(t: Theme): void { + localStorage.setItem(THEME_KEY, t); + applyTheme(t); +} + export function uuid(): string { if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) { return (crypto as any).randomUUID(); diff --git a/client/src/styles.css b/client/src/styles.css index 05707c6..6686b87 100644 --- a/client/src/styles.css +++ b/client/src/styles.css @@ -1,12 +1,37 @@ * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; } +:root { + --bg: #111; + --fg: #eee; + --surface: #1a1a1a; + --surface-2: #2a2a2a; + --surface-3: #3a3a3a; + --border: #444; + --border-soft: #333; + --pfand: #9ab; + --danger: #ff8a8a; +} + +:root[data-theme="light"] { + --bg: #f4f4f5; + --fg: #18181b; + --surface: #ffffff; + --surface-2: #e7e7ea; + --surface-3: #d8d8dc; + --border: #bcbcc2; + --border-soft: #dddde2; + --pfand: #4a6a8a; + --danger: #c0392b; +} + html, body, #app { margin: 0; padding: 0; height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; - background: #111; - color: #eee; + background: var(--bg); + color: var(--fg); + font-weight: 600; overscroll-behavior: none; -webkit-user-select: none; user-select: none; @@ -14,14 +39,15 @@ html, body, #app { button { font: inherit; + font-weight: 700; color: inherit; - background: #2a2a2a; - border: 1px solid #444; + background: var(--surface-2); + border: 1px solid var(--border); border-radius: 8px; padding: 12px 16px; cursor: pointer; } -button:active { background: #3a3a3a; } +button:active { background: var(--surface-3); } button:disabled { opacity: 0.4; } /* ---------- Tablet UI (portrait) ---------- */ @@ -47,7 +73,14 @@ button:disabled { opacity: 0.4; } .bar-picker button { width: 280px; height: 96px; - font-size: 28px; + font-size: 30px; +} +.theme-toggle { + margin-top: 24px; + background: transparent; + border: 1px solid var(--border); + font-size: 18px; + padding: 10px 18px; } .topbar { @@ -55,12 +88,12 @@ button:disabled { opacity: 0.4; } justify-content: space-between; align-items: center; padding: 10px 16px; - background: #1a1a1a; - border-bottom: 1px solid #333; + background: var(--surface); + border-bottom: 1px solid var(--border-soft); flex: 0 0 auto; } -.topbar .bar-name { font-size: 20px; font-weight: bold; } -.topbar .change { font-size: 14px; padding: 8px 12px; } +.topbar .bar-name { font-size: 26px; font-weight: 800; } +.topbar .change { font-size: 17px; padding: 10px 14px; } /* Drink grid fills available space with no scrolling — tiles auto-size to fit. 3 columns on portrait iPad (~768px); rows divide the remaining height equally. */ @@ -86,20 +119,22 @@ button:disabled { opacity: 0.4; } align-items: center; text-align: center; padding: 8px; - font-size: 18px; + font-size: 22px; } -.drink .name { font-weight: bold; line-height: 1.2; } -.drink .price { font-size: 14px; opacity: 0.7; margin-top: 6px; } +.drink .name { font-weight: 800; line-height: 1.15; } +.drink .price { font-size: 22px; font-weight: 700; margin-top: 6px; } +.drink .pfand { font-size: 15px; font-weight: 600; color: var(--pfand); margin-top: 2px; } .drink.pfand-return { background: #5a2a2a; border-color: #7a3a3a; + color: #fff; } .cart { flex: 0 0 38vh; - background: #1a1a1a; - border-top: 1px solid #333; + background: var(--surface); + border-top: 1px solid var(--border-soft); padding: 8px 16px; display: flex; flex-direction: column; @@ -118,17 +153,20 @@ button:disabled { opacity: 0.4; } display: flex; justify-content: space-between; padding: 3px 0; - font-size: 16px; + font-size: 20px; + font-weight: 600; } -.cart-line.return { color: #ff8a8a; } +.cart-line.pfand { color: var(--pfand); } +.cart-line.return { color: var(--danger); } .cart-line.muted { opacity: 0.5; } .cart-total { - font-size: 26px; - font-weight: bold; + font-size: 56px; + font-weight: 800; text-align: right; + line-height: 1.1; padding: 4px 0 8px; } -.cart-total.negative { color: #ff8a8a; } +.cart-total.negative { color: var(--danger); } .actions { display: flex; @@ -137,7 +175,9 @@ button:disabled { opacity: 0.4; } .actions button { flex: 1; height: 64px; - font-size: 18px; + font-size: 22px; + font-weight: 800; + color: #fff; } .actions .cancel { background: #5a2a2a; border-color: #7a3a3a; } .actions .confirm { background: #2a5a2a; border-color: #3a7a3a; } @@ -152,9 +192,9 @@ button:disabled { opacity: 0.4; } } .admin h1, .admin h2 { margin-top: 24px; } .admin table { width: 100%; border-collapse: collapse; margin-bottom: 16px; } -.admin th, .admin td { padding: 6px 8px; border-bottom: 1px solid #333; text-align: left; } +.admin th, .admin td { padding: 6px 8px; border-bottom: 1px solid var(--border-soft); text-align: left; } .admin input, .admin select { - background: #1a1a1a; color: #eee; border: 1px solid #444; border-radius: 4px; padding: 6px; + background: var(--surface); color: var(--fg); border: 1px solid var(--border); border-radius: 4px; padding: 6px; } .admin .row { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; } .admin .muted { opacity: 0.6; } @@ -167,8 +207,8 @@ button:disabled { opacity: 0.4; } gap: 8px; padding: 6px 8px; margin-bottom: 4px; - background: #1a1a1a; - border: 1px solid #333; + background: var(--surface); + border: 1px solid var(--border-soft); border-radius: 6px; cursor: grab; } diff --git a/client/src/tablet/BarPicker.tsx b/client/src/tablet/BarPicker.tsx index d10d584..5192d7c 100644 --- a/client/src/tablet/BarPicker.tsx +++ b/client/src/tablet/BarPicker.tsx @@ -1,4 +1,6 @@ +import { useState } from 'preact/hooks'; import type { Bar } from '@wutzcalc/shared'; +import { getTheme, setTheme } from '../api'; interface Props { bars: Bar[] | null; @@ -6,6 +8,14 @@ interface Props { } export function BarPicker({ bars, onPick }: Props) { + const [theme, setThemeState] = useState(getTheme()); + + function toggleTheme() { + const next = theme === 'dark' ? 'light' : 'dark'; + setTheme(next); + setThemeState(next); + } + return (

Bar auswählen

@@ -18,6 +28,9 @@ export function BarPicker({ bars, onPick }: Props) { )) )} +
); } diff --git a/client/src/tablet/Sale.tsx b/client/src/tablet/Sale.tsx index c13e97d..8c5d6dd 100644 --- a/client/src/tablet/Sale.tsx +++ b/client/src/tablet/Sale.tsx @@ -51,6 +51,8 @@ export function Sale({ config, onChangeBar }: Props) { setPfandReturns(0); } + const pfandCount = useMemo(() => lines.reduce((sum, l) => sum + l.qty, 0), [lines]); + const isEmpty = lines.length === 0 && pfandReturns === 0; async function confirm(crew: boolean) { @@ -84,7 +86,10 @@ export function Sale({ config, onChangeBar }: Props) { {drinks.map(d => ( ))}