feat: add responsive calc-02 HP48GX demo

This commit is contained in:
2026-05-15 18:45:44 +02:00
parent 95eb1d265f
commit 48a262eb87
7 changed files with 775 additions and 4 deletions
+367
View File
@@ -0,0 +1,367 @@
:root {
--bg0: #10151e;
--bg1: #1b2432;
--panel: #2c3442;
--panel2: #394354;
--edge: #0c1118;
--display: #cfe0ae;
--display2: #b9cd8a;
--displayText: #1f2a12;
--buttonText: #f4f7fb;
--shadow: rgba(0, 0, 0, 0.35);
--btnTop: #444c58;
--btnBottom: #2f3640;
--btnAccentTop: #3f526b;
--btnAccentBottom: #2b394c;
--btnAltTop: #525c69;
--btnAltBottom: #3a434f;
--btnDangerTop: #584042;
--btnDangerBottom: #402d2f;
--btnEnterTop: #465349;
--btnEnterBottom: #303a31;
--btnText: #eef2f7;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
min-height: 100%;
}
body {
min-height: 100vh;
font-family: Arial, sans-serif;
color: var(--buttonText);
background:
radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 32%),
linear-gradient(180deg, var(--bg1), var(--bg0));
}
.app-shell {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(12px, 2vw, 28px);
}
.calculator {
width: min(100vw - 24px, 1120px);
height: min(100vh - 24px, 900px);
display: grid;
gap: clamp(10px, 1.4vw, 18px);
padding: clamp(12px, 1.8vw, 18px);
border-radius: 28px;
background: linear-gradient(180deg, var(--panel2), var(--panel));
border: 1px solid var(--edge);
box-shadow: 0 26px 70px var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
grid-template-columns: 1.3fr 0.9fr;
grid-template-rows: minmax(0, 0.62fr) min-content minmax(180px, 1fr) minmax(180px, 1fr);
align-content: start;
grid-template-areas:
"display functions"
"buttons functions"
"keypad functions"
"keypad trigo";
}
.display-panel,
.display-buttons-panel,
.keypad-panel,
.functions-panel,
.trigo-panel,
.status-line {
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(6, 10, 16, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.display-panel {
grid-area: display;
padding: clamp(12px, 1.5vw, 16px);
background: linear-gradient(180deg, var(--display), var(--display2));
color: var(--displayText);
font-family: "Courier New", monospace;
overflow: hidden;
height: clamp(112px, 18vw, 160px);
max-height: 140px;
align-self: start;
margin-bottom: 0;
min-height: 0;
}
.display-grid {
height: 100%;
display: grid;
grid-template-columns: 1fr;
grid-template-rows: repeat(4, minmax(0, 1fr));
gap: 2px;
}
.stack-cell {
display: grid;
grid-template-columns: 2.2ch 1fr;
align-items: center;
gap: 12px;
font-size: clamp(18px, 3vw, 30px);
line-height: 1;
min-height: 0;
padding-block: 0;
}
.stack-label {
text-align: right;
opacity: 0.78;
}
.stack-value {
min-height: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.display-buttons-panel {
grid-area: buttons;
padding: 8px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(2, auto);
gap: 8px;
align-content: start;
align-items: stretch;
grid-auto-flow: row;
grid-auto-rows: auto;
background: linear-gradient(180deg, #242a33, #1a1f27);
border-color: rgba(255, 255, 255, 0.04);
margin-top: 0;
}
.display-button {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 3px 0 rgba(0, 0, 0, 0.34);
background-clip: padding-box;
background: linear-gradient(180deg, #3a414c, #252b34);
color: #e8edf3;
border-color: rgba(255, 255, 255, 0.05);
}
.display-button-offset {
grid-column-start: 2;
}
.display-buttons-panel > button {
width: 100%;
}
.mode-menu {
position: fixed;
z-index: 20;
display: grid;
gap: 6px;
padding: 10px;
border-radius: 14px;
background: rgba(18, 24, 33, 0.98);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.35);
}
.mode-menu-item {
min-width: 120px;
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom));
}
.mode-menu-item.is-active {
outline: 2px solid rgba(207, 224, 174, 0.7);
}
.display-button:nth-child(6),
.display-button:nth-child(7) {
background: linear-gradient(180deg, #343b46, #20262e);
}
.keypad-panel {
grid-area: keypad;
padding: 10px;
}
.functions-panel {
grid-area: functions;
padding: 10px;
align-self: start;
min-height: 0;
padding-top: 10px;
}
.trigo-panel {
grid-area: trigo;
padding: 10px;
align-self: start;
min-height: 0;
padding-top: 10px;
}
.status-line {
grid-area: status;
padding: 10px 14px;
display: flex;
align-items: center;
min-height: 42px;
font-size: 14px;
color: rgba(255, 255, 255, 0.85);
}
.keypad-grid,
.functions-grid,
.trigo-grid {
display: grid;
gap: 8px;
grid-auto-rows: minmax(0, 1fr);
}
.keypad-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(5, minmax(0, 1fr));
}
.functions-grid,
.trigo-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.functions-grid,
.trigo-grid {
grid-template-rows: repeat(2, minmax(0, 1fr));
}
button {
border: 1px solid rgba(14, 18, 25, 0.85);
border-radius: 12px;
padding: 10px 8px;
font: inherit;
font-weight: 700;
color: var(--btnText);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 3px 0 rgba(0, 0, 0, 0.28);
transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease;
}
button:hover {
filter: brightness(1.06);
}
.display-button:hover {
filter: brightness(1.08);
}
button:active {
transform: translateY(2px);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 1px 0 rgba(0, 0, 0, 0.25);
}
.display-button:active {
transform: translateY(2px);
}
.key-default {
background: linear-gradient(180deg, var(--btnTop), var(--btnBottom));
color: #eef2f7;
}
.key-accent {
background: linear-gradient(180deg, var(--btnAccentTop), var(--btnAccentBottom));
color: #eef2f7;
}
.key-alt {
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom));
color: #eef2f7;
}
.key-danger {
background: linear-gradient(180deg, var(--btnDangerTop), var(--btnDangerBottom));
color: #eef2f7;
}
.key-enter {
background: linear-gradient(180deg, var(--btnEnterTop), var(--btnEnterBottom));
color: #eef2f7;
}
.hidden-input {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
@media (orientation: portrait), (max-width: 860px) {
.calculator {
width: min(100vw - 16px, 760px);
height: auto;
min-height: calc(100vh - 16px);
grid-template-columns: 1fr;
grid-template-rows: minmax(160px, auto) auto minmax(220px, auto) auto auto;
grid-template-areas:
"display"
"buttons"
"keypad"
"functions"
"trigo";
}
.display-buttons-panel {
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(2, auto);
margin-top: 0;
}
.keypad-grid {
grid-template-rows: repeat(5, minmax(42px, 1fr));
}
.functions-grid,
.trigo-grid {
grid-auto-rows: minmax(0, 1fr);
grid-template-rows: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 520px) {
.app-shell {
padding: 8px;
}
.calculator {
width: 100%;
min-height: calc(100vh - 16px);
border-radius: 20px;
padding: 10px;
gap: 10px;
}
.display-panel {
padding: 10px;
}
.stack-cell {
font-size: clamp(16px, 5.2vw, 22px);
gap: 8px;
}
button {
border-radius: 10px;
padding: 8px 6px;
font-size: 13px;
}
.display-buttons-panel {
gap: 6px;
}
}
+52
View File
@@ -0,0 +1,52 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HP48GX RPN Calculator</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<main class="app-shell">
<section class="calculator calculator-portrait" aria-label="HP48GX style RPN calculator">
<div class="display-panel">
<div class="display-frame">
<div class="display-grid">
<div class="stack-cell"><span class="stack-label">T:</span><span id="stackT" class="stack-value"></span></div>
<div class="stack-cell"><span class="stack-label">Z:</span><span id="stackZ" class="stack-value"></span></div>
<div class="stack-cell"><span class="stack-label">Y:</span><span id="stackY" class="stack-value"></span></div>
<div class="stack-cell"><span class="stack-label">X:</span><span id="stackX" class="stack-value"></span></div>
</div>
</div>
</div>
<div class="display-buttons-panel">
<button id="modeButton" class="display-button">Mode</button>
<button id="pasteButton" class="display-button">Paste</button>
<button id="upButton" class="display-button">Up</button>
<button id="constButton" class="display-button">Const</button>
<button id="rightButton" class="display-button display-button-offset">Right</button>
<button id="downButton" class="display-button">Down</button>
<button id="constRightButton" class="display-button">Right</button>
</div>
<div class="keypad-panel">
<div class="keypad-grid" id="keypadGrid"></div>
</div>
<div class="functions-panel">
<div class="functions-grid" id="functionsGrid"></div>
</div>
<div class="trigo-panel">
<div class="trigo-grid" id="trigoGrid"></div>
</div>
<input id="hiddenInput" class="hidden-input" type="text" autocomplete="off" aria-hidden="true" tabindex="-1">
</section>
</main>
<script src="../../src/rpn-calculator.js"></script>
<script src="./index.js"></script>
</body>
</html>
+310
View File
@@ -0,0 +1,310 @@
const calc = new RpnCalculator({ angleMode: 'deg' });
const hiddenInput = document.getElementById('hiddenInput');
const modeButton = document.getElementById('modeButton');
const pasteButton = document.getElementById('pasteButton');
const upButton = document.getElementById('upButton');
const constButton = document.getElementById('constButton');
const rightButton = document.getElementById('rightButton');
const downButton = document.getElementById('downButton');
const constRightButton = document.getElementById('constRightButton');
const stackEls = {
T: document.getElementById('stackT'),
Z: document.getElementById('stackZ'),
Y: document.getElementById('stackY'),
X: document.getElementById('stackX'),
};
const keypadGrid = document.getElementById('keypadGrid');
const functionsGrid = document.getElementById('functionsGrid');
const trigoGrid = document.getElementById('trigoGrid');
const calculatorEl = document.querySelector('.calculator');
const keypadKeys = [
{ label: '±', action: 'neg', className: 'key-default' },
{ label: 'C', action: 'clear', className: 'key-danger' },
{ label: '⎋', action: 'escape', className: 'key-danger' },
{ label: '⌫', action: 'backspace', className: 'key-danger' },
{ label: '7', input: '7', className: 'key-default' },
{ label: '8', input: '8', className: 'key-default' },
{ label: '9', input: '9', className: 'key-default' },
{ label: '/', action: 'div', className: 'key-accent' },
{ label: '4', input: '4', className: 'key-default' },
{ label: '5', input: '5', className: 'key-default' },
{ label: '6', input: '6', className: 'key-default' },
{ label: '*', action: 'mul', className: 'key-accent' },
{ label: '1', input: '1', className: 'key-default' },
{ label: '2', input: '2', className: 'key-default' },
{ label: '3', input: '3', className: 'key-default' },
{ label: '-', action: 'sub', className: 'key-accent' },
{ label: '0', input: '0', className: 'key-default' },
{ label: '.', input: '.', className: 'key-default' },
{ label: 'Enter', action: 'enter', className: 'key-enter' },
{ label: '+', action: 'add', className: 'key-accent' },
];
const functionKeys = [
{ label: 'x²', action: 'sqr', className: 'key-default' },
{ label: 'yˣ', action: 'pow', className: 'key-default' },
{ label: '1/x', action: 'recip', className: 'key-default' },
{ label: '%', action: 'mod', className: 'key-default' },
{ label: '√x', action: 'sqrt', className: 'key-default' },
{ label: 'y√x', action: 'pow', className: 'key-default' },
{ label: '10ˣ', action: 'pow10', className: 'key-default' },
{ label: '', spacer: true },
{ label: 'log', action: 'log', className: 'key-default' },
{ label: 'ln', action: 'ln', className: 'key-default' },
{ label: '', spacer: true },
{ label: '', spacer: true },
];
const trigoKeys = [
{ label: 'sin', action: 'sin', className: 'key-default' },
{ label: 'cos', action: 'cos', className: 'key-default' },
{ label: 'tan', action: 'tan', className: 'key-default' },
{ label: '', spacer: true },
{ label: 'asin', action: 'asin', className: 'key-default' },
{ label: 'acos', action: 'acos', className: 'key-default' },
{ label: 'atan', action: 'atan', className: 'key-default' },
{ label: '', spacer: true },
];
function focusInput() {
hiddenInput.focus();
}
function setStatus(message) {
console.log(message);
}
function normalizeStack() {
while (calc.stack.length > 4) {
calc.stack.shift();
}
}
function getStackLine(indexFromTop) {
const index = calc.stack.length - 1 - indexFromTop;
return index >= 0 ? calc.stack[index] : '';
}
function render() {
normalizeStack();
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
calculatorEl?.classList.toggle('portrait', isPortrait);
calculatorEl?.classList.toggle('landscape', !isPortrait);
stackEls.T.textContent = calc.formatNumber(getStackLine(3)) || '';
stackEls.Z.textContent = calc.formatNumber(getStackLine(2)) || '';
stackEls.Y.textContent = calc.formatNumber(getStackLine(1)) || '';
stackEls.X.textContent = calc.formatNumber(getStackLine(0)) || (calc.isEditing ? calc.inputValue : '');
modeButton.textContent = calc.angleMode;
}
function pushEditingValueIfNeeded() {
if (!calc.isEditing) return;
if (calc.inputValue !== '') {
calc.push(calc.parseInputValue(calc.inputValue));
}
calc.inputValue = '';
calc.isEditing = false;
}
function inputToX(value) {
if (!calc.isEditing) {
calc.isEditing = true;
calc.inputValue = '';
}
if (value === 'Backspace') {
calc.inputValue = calc.inputValue.slice(0, -1);
} else {
calc.inputValue += value;
}
if (calc.inputValue === '') {
calc.isEditing = false;
}
}
function execute(name) {
try {
if (name === 'enter') {
pushEditingValueIfNeeded();
} else if (name === 'clear') {
calc.clear();
calc.inputValue = '';
calc.isEditing = false;
} else if (name === 'escape') {
calc.inputValue = '';
calc.isEditing = false;
} else if (name === 'backspace') {
if (calc.isEditing) {
inputToX('Backspace');
} else {
calc.remove(0);
}
} else if (name === 'neg') {
if (calc.isEditing) {
calc.inputValue = calc.inputValue.startsWith('-') ? calc.inputValue.slice(1) : `-${calc.inputValue}`;
} else {
calc.push(calc.pop() * -1);
}
} else if (name === 'pow10') {
pushEditingValueIfNeeded();
calc.push(10);
calc.command('pow');
} else {
pushEditingValueIfNeeded();
calc.command(name);
}
render();
} catch (error) {
console.error(error);
}
}
function createKeyButton({ label, input, action, spacer, className }) {
if (spacer) {
const div = document.createElement('div');
return div;
}
const button = document.createElement('button');
button.type = 'button';
button.textContent = label;
button.className = className;
button.addEventListener('click', () => {
focusInput();
if (input) {
inputToX(input);
render();
return;
}
execute(action);
});
return button;
}
function buildGrid(container, keys) {
container.innerHTML = '';
keys.forEach((key) => container.appendChild(createKeyButton(key)));
}
function handleKeyboard(event) {
if (event.target === hiddenInput) return;
const key = event.key;
if (/^[0-9.]$/.test(key)) {
event.preventDefault();
inputToX(key);
render();
return;
}
const map = {
Enter: 'enter',
Backspace: 'backspace',
Escape: 'escape',
Delete: 'clear',
'+': 'add',
'-': 'sub',
'*': 'mul',
'/': 'div',
'%': 'mod',
'^': 'pow',
};
if (map[key]) {
event.preventDefault();
execute(map[key]);
}
}
const modeOptions = ['deg', 'rad', 'grad'];
let modeMenuEl = null;
function closeModeMenu() {
if (modeMenuEl) {
modeMenuEl.remove();
modeMenuEl = null;
}
}
function openModeMenu() {
closeModeMenu();
const rect = modeButton.getBoundingClientRect();
modeMenuEl = document.createElement('div');
modeMenuEl.className = 'mode-menu';
modeMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
modeMenuEl.style.left = `${rect.left + window.scrollX}px`;
modeOptions.forEach((mode) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `mode-menu-item${mode === calc.angleMode ? ' is-active' : ''}`;
button.textContent = mode;
button.addEventListener('click', () => {
calc.angleMode = mode;
render();
closeModeMenu();
});
modeMenuEl.appendChild(button);
});
document.body.appendChild(modeMenuEl);
const menuRect = modeMenuEl.getBoundingClientRect();
const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8);
modeMenuEl.style.left = `${Math.max(8, Math.min(maxLeft, rect.left + window.scrollX))}px`;
}
modeButton.addEventListener('click', (event) => {
event.stopPropagation();
openModeMenu();
});
window.addEventListener('resize', closeModeMenu);
window.addEventListener('scroll', closeModeMenu, true);
window.addEventListener('click', (event) => {
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) {
closeModeMenu();
}
});
pasteButton.addEventListener('click', async () => {
try {
const text = await navigator.clipboard.readText();
if (!text) {
setStatus('Clipboard empty');
return;
}
if (calc.isEditing) {
calc.inputValue += text;
} else {
calc.isEditing = true;
calc.inputValue = text;
}
setStatus('Pasted');
render();
} catch (error) {
setStatus('Paste unavailable');
}
});
upButton.addEventListener('click', () => {});
constButton.addEventListener('click', () => {});
rightButton.addEventListener('click', () => {});
downButton.addEventListener('click', () => {});
constRightButton.addEventListener('click', () => {});
window.addEventListener('keydown', handleKeyboard);
window.addEventListener('load', focusInput);
document.addEventListener('click', (event) => {
if (!event.target.closest('.calculator')) {
focusInput();
}
});
buildGrid(keypadGrid, keypadKeys);
buildGrid(functionsGrid, functionKeys);
buildGrid(trigoGrid, trigoKeys);
render();
+10
View File
@@ -0,0 +1,10 @@
┌──────────── Display ────────────┐ ┌────── Functions ─────┐ ┌──────────── Keypad ─────────────┐
| T: | | x^2 | y^x | 1/x | % | | +/- | Clear | Esc | backspace |
| Z: | | √x | y√x | 10^x | | | 7 | 8 | 9 | / |
| Y: | | log | ln | | | | 4 | 5 | 6 | * |
| X: | └──────────────────────┘ | 1 | 2 | 3 | - |
└─────────────────────────────────┘ ┌─────── Trigo ────────┐ | 0 | . | Enter | + |
┌──────── Display Buttons ────────┐ | sin | cos | tan | └─────────────────────────────────┘
| Mode | Paste | Up | Const | | asin | acos | atan |
| | Right | Down | Right | └──────────────────────┘
└─────────────────────────────────┘
+26
View File
@@ -0,0 +1,26 @@
┌──────────── Display ────────────┐
| T: |
| Z: |
| Y: |
| X: |
└─────────────────────────────────┘
┌──────── Display Buttons ────────┐
| Mode | Paste | Up | Const |
| | Right | Down | Right |
└─────────────────────────────────┘
┌──────────── Keypad ─────────────┐
| +/- | Clear | Esc | backspace |
| 7 | 8 | 9 | / |
| 4 | 5 | 6 | * |
| 1 | 2 | 3 | - |
| 0 | . | Enter | + |
└─────────────────────────────────┘
┌─────────── Functions ───────────┐
| x^2 | y^x | 1/x | % |
| √x | y√x | 10^x | |
| log | ln | | |
└─────────────────────────────────┘
┌───────────── Trigo ─────────────┐
| sin | cos | tan | |
| asin | acos | atan | |
└─────────────────────────────────┘