Compare commits

...

6 Commits

Author SHA1 Message Date
matmoul 9abdc33713 fix(calc-02): update stack labels for navigation and move modes 2026-05-17 05:34:06 +02:00
matmoul 6dd9550890 feat: add stack navigation and reordering controls 2026-05-17 04:24:43 +02:00
matmoul f44fb8c252 docs: align calculator visual portrait table cell 2026-05-17 03:23:47 +02:00
matmoul 5cc97f754d fix(calc-02): restore edited value on cancel
Canceling stack-top editing now pushes the original value back before
leaving edit mode, including Escape and ArrowDown handling.
2026-05-17 01:31:43 +02:00
matmoul 736154110d fix: correct calc-02 function key shortcuts 2026-05-17 00:38:32 +02:00
matmoul 62a0f447c5 feat: update calculator function key labels and shortcuts
Add titles for function and trig buttons to expose keyboard hints, and remap reciprocal/power10 shortcuts to match the new key layout.
Update the portrait visual spec to reflect the revised keypad and function ordering.
2026-05-17 00:35:50 +02:00
3 changed files with 253 additions and 38 deletions
+13 -4
View File
@@ -123,10 +123,7 @@ body {
padding-block: 0; padding-block: 0;
} }
.stack-label { /* removed extra first-column styling */
text-align: right;
opacity: 0.78;
}
.stack-value { .stack-value {
min-height: 0; min-height: 0;
@@ -141,6 +138,18 @@ body {
.stack-value.is-editing { .stack-value.is-editing {
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.stack-cell.is-selected {
background: transparent;
border-radius: 6px;
outline: 2px solid rgba(31, 42, 18, 0.2);
outline-offset: -2px;
}
.stack-cell.is-moving {
background: rgba(31, 42, 18, 0.18);
}
.display-buttons-panel { .display-buttons-panel {
padding: 8px; padding: 8px;
min-height: 0; min-height: 0;
+232 -26
View File
@@ -53,28 +53,28 @@ const keypadKeys = [
]; ];
const functionKeys = [ const functionKeys = [
{ label: 'x²', action: 'sqr', className: 'key-default' }, { label: 'x²', action: 'sqr', className: 'key-default', title: 's' },
{ label: '', action: 'pow', className: 'key-default' }, { label: '√x', action: 'sqrt', className: 'key-default', title: 'r' },
{ label: '1/x', action: 'recip', className: 'key-default' }, { label: '1/x', action: 'recip', className: 'key-default', title: 'x' },
{ label: '%', action: 'mod', className: 'key-default' }, { label: '%', action: 'mod', className: 'key-default' },
{ label: '√x', action: 'sqrt', className: 'key-default' }, { label: '', action: 'pow', className: 'key-default', title: 'S' },
{ label: 'y√x', action: 'root', className: 'key-default' }, { label: 'y√x', action: 'root', className: 'key-default', title: 'R' },
{ label: '10ˣ', action: 'pow10', className: 'key-default' }, { label: '10ˣ', action: 'pow10', className: 'key-default', title: 'd' },
{ label: '', spacer: true }, { label: '', spacer: true },
{ label: 'log', action: 'log', className: 'key-default' }, { label: 'log', action: 'log', className: 'key-default', title: 'l / L' },
{ label: 'ln', action: 'ln', className: 'key-default' }, { label: 'ln', action: 'ln', className: 'key-default', title: 'n / N' },
{ label: 'eˣ', action: 'exp', className: 'key-default' }, { label: 'eˣ', action: 'exp', className: 'key-default', title: 'e / E' },
{ label: '', spacer: true }, { label: '', spacer: true },
]; ];
const trigoKeys = [ const trigoKeys = [
{ label: 'sin', action: 'sin', className: 'key-default' }, { label: 'sin', action: 'sin', className: 'key-default', title: 'i' },
{ label: 'cos', action: 'cos', className: 'key-default' }, { label: 'cos', action: 'cos', className: 'key-default', title: 'o' },
{ label: 'tan', action: 'tan', className: 'key-default' }, { label: 'tan', action: 'tan', className: 'key-default', title: 'a' },
{ label: '', spacer: true }, { label: '', spacer: true },
{ label: 'asin', action: 'asin', className: 'key-default' }, { label: 'asin', action: 'asin', className: 'key-default', title: 'I' },
{ label: 'acos', action: 'acos', className: 'key-default' }, { label: 'acos', action: 'acos', className: 'key-default', title: 'O' },
{ label: 'atan', action: 'atan', className: 'key-default' }, { label: 'atan', action: 'atan', className: 'key-default', title: 'A' },
{ label: '', spacer: true }, { label: '', spacer: true },
]; ];
@@ -95,6 +95,10 @@ function focusInput() {
let statusTimer = null; let statusTimer = null;
let editCursor = 0; let editCursor = 0;
let editRestoreValue = null;
let stackMode = 'normal';
let stackSelection = 0;
let stackMoveSnapshot = null;
function setStatus(message, isError = false, timeoutMs = 1400) { function setStatus(message, isError = false, timeoutMs = 1400) {
if (!statusLine) return; if (!statusLine) return;
@@ -132,6 +136,30 @@ function getStackDisplayValue(label) {
return calc.formatNumber(getStackLine(indexFromTop)) || ''; return calc.formatNumber(getStackLine(indexFromTop)) || '';
} }
function getVisibleStackLabel(label) {
if (stackMode === 'navigation' || stackMode === 'move') {
const indexMap = { X: 0, Y: 1, Z: 2, T: 3 };
return String(indexMap[label]);
}
return label;
}
function updateStackLabels() {
const stackLabels = ['T', 'Z', 'Y', 'X'];
for (const label of stackLabels) {
const stackCell = stackEls[label].parentElement;
if (!stackCell) continue;
const labelEl = stackCell.querySelector('.stack-label');
if (labelEl) {
labelEl.textContent = `${getVisibleStackLabel(label)}:`;
}
}
}
function stackModeToLabel(index) {
return ['X', 'Y', 'Z', 'T'][Math.max(0, Math.min(3, index))] ?? 'X';
}
function updateCopyButtons() { function updateCopyButtons() {
for (const label of ['T', 'Z', 'Y', 'X']) { for (const label of ['T', 'Z', 'Y', 'X']) {
const value = getStackDisplayValue(label); const value = getStackDisplayValue(label);
@@ -152,15 +180,20 @@ function render() {
const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860; const isPortrait = window.matchMedia('(orientation: portrait)').matches || window.innerWidth <= 860;
calculatorEl?.classList.toggle('portrait', isPortrait); calculatorEl?.classList.toggle('portrait', isPortrait);
calculatorEl?.classList.toggle('landscape', !isPortrait); calculatorEl?.classList.toggle('landscape', !isPortrait);
stackEls.X.textContent = calc.isEditing ? '' : getStackDisplayValue('X'); const stackLabels = ['T', 'Z', 'Y', 'X'];
for (const label of stackLabels) {
const isSelected = stackMode !== 'normal' && stackModeToLabel(stackSelection) === label;
const value = label === 'X' && calc.isEditing ? '' : getStackDisplayValue(label);
stackEls[label].textContent = value;
stackEls[label].classList.toggle('is-editing', label === 'X' && calc.isEditing);
stackEls[label].classList.toggle('is-caret-visible', label === 'X' && calc.isEditing);
stackEls[label].parentElement?.classList.toggle('is-selected', isSelected);
stackEls[label].parentElement?.classList.toggle('is-moving', stackMode === 'move' && stackModeToLabel(stackSelection) === label);
}
if (calc.isEditing) { if (calc.isEditing) {
renderEditValue(); renderEditValue();
} }
stackEls.Y.textContent = getStackDisplayValue('Y'); updateStackLabels();
stackEls.Z.textContent = getStackDisplayValue('Z');
stackEls.T.textContent = getStackDisplayValue('T');
stackEls.X.classList.toggle('is-editing', calc.isEditing);
stackEls.X.classList.toggle('is-caret-visible', calc.isEditing);
updateCopyButtons(); updateCopyButtons();
modeButton.textContent = calc.angleMode; modeButton.textContent = calc.angleMode;
} }
@@ -173,6 +206,113 @@ function stopEditing(clearValue = false) {
editCursor = 0; editCursor = 0;
} }
function cancelEditing() {
if (editRestoreValue !== null) {
calc.push(editRestoreValue);
}
editRestoreValue = null;
stopEditing(true);
}
function enterNavigationMode() {
if (calc.isEditing) return;
stackMode = 'navigation';
stackSelection = 0;
stackMoveSnapshot = null;
render();
}
function exitStackMode() {
stackMode = 'normal';
stackSelection = 0;
stackMoveSnapshot = null;
render();
}
function moveNavigationSelection(delta) {
const maxIndex = Math.max(0, calc.stack.length - 1);
stackSelection = Math.max(0, Math.min(maxIndex, stackSelection + delta));
render();
}
function beginStackMove() {
stackMode = 'move';
stackMoveSnapshot = calc.stack.slice();
render();
}
function restoreStackMoveSnapshot() {
if (!stackMoveSnapshot) return;
calc.stack = stackMoveSnapshot.slice();
stackMoveSnapshot = null;
render();
}
function validateStackMove() {
stackMoveSnapshot = null;
stackMode = 'normal';
render();
}
function moveSelectedStackValue(delta) {
const index = stackSelection;
const target = Math.max(0, Math.min(calc.stack.length - 1, index + delta));
if (target === index) return;
const value = calc.stack.splice(index, 1)[0];
calc.stack.splice(target, 0, value);
stackSelection = target;
render();
}
function enterNavigationMode() {
if (calc.isEditing) return;
stackMode = 'navigation';
stackSelection = 0;
render();
}
function exitStackMode() {
stackMode = 'normal';
stackSelection = 0;
stackMoveSnapshot = null;
render();
}
function moveNavigationSelection(delta) {
const maxIndex = Math.max(0, calc.stack.length - 1);
stackSelection = Math.max(0, Math.min(maxIndex, stackSelection + delta));
render();
}
function beginStackMove() {
stackMode = 'move';
stackMoveSnapshot = calc.stack.slice();
render();
}
function restoreStackMoveSnapshot() {
if (!stackMoveSnapshot) return;
calc.stack = stackMoveSnapshot.slice();
stackMoveSnapshot = null;
render();
}
function validateStackMove() {
stackMoveSnapshot = null;
stackMode = 'normal';
render();
}
function moveSelectedStackValue(delta) {
const index = stackSelection;
const target = Math.max(0, Math.min(calc.stack.length - 1, index + delta));
if (target === index) return;
const value = calc.stack.splice(index, 1)[0];
calc.stack.splice(target, 0, value);
stackSelection = target;
render();
}
function moveEditCursor(delta) { function moveEditCursor(delta) {
editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta)); editCursor = Math.max(0, Math.min(calc.inputValue.length, editCursor + delta));
} }
@@ -182,6 +322,7 @@ function pushEditingValueIfNeeded() {
if (calc.inputValue !== '') { if (calc.inputValue !== '') {
calc.push(calc.parseInputValue(calc.inputValue)); calc.push(calc.parseInputValue(calc.inputValue));
} }
editRestoreValue = null;
calc.inputValue = ''; calc.inputValue = '';
calc.isEditing = false; calc.isEditing = false;
editCursor = 0; editCursor = 0;
@@ -190,6 +331,7 @@ function pushEditingValueIfNeeded() {
function startEditingFromStackTop() { function startEditingFromStackTop() {
if (!calc.isValidIndex(0)) return false; if (!calc.isValidIndex(0)) return false;
const value = calc.stack[0]; const value = calc.stack[0];
editRestoreValue = value;
calc.remove(0); calc.remove(0);
calc.isEditing = true; calc.isEditing = true;
calc.inputValue = calc.formatNumber(value); calc.inputValue = calc.formatNumber(value);
@@ -247,7 +389,7 @@ function execute(name) {
calc.clear(); calc.clear();
stopEditing(true); stopEditing(true);
} else if (name === 'escape') { } else if (name === 'escape') {
stopEditing(true); cancelEditing();
} else if (name === 'backspace') { } else if (name === 'backspace') {
if (calc.isEditing) { if (calc.isEditing) {
inputToX('Backspace'); inputToX('Backspace');
@@ -280,7 +422,7 @@ function execute(name) {
} }
} }
function createKeyButton({ label, input, action, spacer, className }) { function createKeyButton({ label, input, action, spacer, className, title }) {
if (spacer) { if (spacer) {
const div = document.createElement('div'); const div = document.createElement('div');
return div; return div;
@@ -289,6 +431,7 @@ function createKeyButton({ label, input, action, spacer, className }) {
button.type = 'button'; button.type = 'button';
button.textContent = label; button.textContent = label;
button.className = className; button.className = className;
if (title) button.title = title;
button.addEventListener('click', () => { button.addEventListener('click', () => {
if (input) { if (input) {
inputToX(input); inputToX(input);
@@ -319,6 +462,53 @@ async function copyStackValue(label) {
function handleKeyboard(event) { function handleKeyboard(event) {
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
const key = event.key; const key = event.key;
if (stackMode === 'navigation') {
if (key === 'ArrowUp') {
event.preventDefault();
moveNavigationSelection(1);
return;
}
if (key === 'ArrowDown') {
event.preventDefault();
moveNavigationSelection(-1);
return;
}
if (key === 'Escape' || key === 'ArrowLeft') {
event.preventDefault();
exitStackMode();
return;
}
if (key === 'Enter') {
event.preventDefault();
beginStackMove();
return;
}
return;
}
if (stackMode === 'move') {
if (key === 'ArrowUp') {
event.preventDefault();
moveSelectedStackValue(1);
return;
}
if (key === 'ArrowDown') {
event.preventDefault();
moveSelectedStackValue(-1);
return;
}
if (key === 'Escape') {
event.preventDefault();
restoreStackMoveSnapshot();
exitStackMode();
return;
}
if (key === 'Enter') {
event.preventDefault();
validateStackMove();
return;
}
return;
}
if (/^[0-9.]$/.test(key)) { if (/^[0-9.]$/.test(key)) {
event.preventDefault(); event.preventDefault();
inputToX(key); inputToX(key);
@@ -340,8 +530,8 @@ function handleKeyboard(event) {
S: 'pow', S: 'pow',
r: 'sqrt', r: 'sqrt',
R: 'root', R: 'root',
v: 'recip', x: 'recip',
u: 'pow10', d: 'pow10',
l: 'log', l: 'log',
L: 'log', L: 'log',
n: 'ln', n: 'ln',
@@ -382,6 +572,11 @@ function handleKeyboard(event) {
} }
if (key === 'ArrowDown') { if (key === 'ArrowDown') {
event.preventDefault(); event.preventDefault();
if (calc.isEditing) {
cancelEditing();
render();
return;
}
downButton.click(); downButton.click();
return; return;
} }
@@ -481,7 +676,12 @@ hiddenInput.addEventListener('paste', (event) => {
pasteTextIntoStack(text); pasteTextIntoStack(text);
}); });
upButton.addEventListener('click', () => {}); upButton.addEventListener('click', () => {
if (!calc.isEditing && stackMode === 'normal') {
enterNavigationMode();
focusInput();
}
});
const constantLabels = { const constantLabels = {
pi: 'π', pi: 'π',
@@ -532,6 +732,11 @@ leftButton.addEventListener('click', () => {
moveEditCursor(-1); moveEditCursor(-1);
render(); render();
focusInput(); focusInput();
return;
}
if (stackMode === 'navigation') {
exitStackMode();
focusInput();
} }
}); });
downButton.addEventListener('click', () => { downButton.addEventListener('click', () => {
@@ -542,6 +747,7 @@ downButton.addEventListener('click', () => {
}); });
rightButton.addEventListener('click', () => { rightButton.addEventListener('click', () => {
if (calc.isEditing) { if (calc.isEditing) {
moveEditCursor(1); moveEditCursor(1);
+5 -5
View File
@@ -24,11 +24,11 @@
``` ```
┌──────────── Keypad ─────────────┐ ┌──────────── Keypad ─────────────┐
| +/- | Clear | Esc | backspace | | Clear | Backspace | Esc | Enter |
| 7 | 8 | 9 | / | | 7 | 8 | 9 | / |
| 4 | 5 | 6 | * | | 4 | 5 | 6 | * |
| 1 | 2 | 3 | - | | 1 | 2 | 3 | - |
| 0 | . | Enter | + | | 0 | . | +/- | + |
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```
@@ -36,9 +36,9 @@
``` ```
┌──────────── Functions ──────────┐ ┌──────────── Functions ──────────┐
| x^2 | y^x | 1/x | % | | x^2 | √x | 1/x | % |
| √x | y√x | 10^x | | | y^x | y√x | 10^x | |
| log | ln | | | | log | ln | e^x | |
└─────────────────────────────────┘ └─────────────────────────────────┘
``` ```