diff --git a/samples/calc-01/index.css b/samples/calc-01/index.css
new file mode 100644
index 0000000..6c885dd
--- /dev/null
+++ b/samples/calc-01/index.css
@@ -0,0 +1,338 @@
+:root {
+ --body-top: #1d2430;
+ --body-bottom: #0c1017;
+ --shell-top: #4b5567;
+ --shell-bottom: #252d39;
+ --shell-edge: #141922;
+ --screen: #dce7b3;
+ --screen-top: #e8f0c3;
+ --screen-text: #1c2910;
+ --screen-dim: #60714a;
+ --key-text: #f4f7fb;
+ --key-fn-top: #677287;
+ --key-fn-bottom: #424b5d;
+ --key-num-top: #5d6470;
+ --key-num-bottom: #383f49;
+ --key-op-top: #4f6b95;
+ --key-op-bottom: #304661;
+ --key-danger-top: #7a5050;
+ --key-danger-bottom: #553636;
+ --key-enter-top: #7d9079;
+ --key-enter-bottom: #4d614b;
+ --border: #11151c;
+ --shadow: rgba(0, 0, 0, 0.35);
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0;
+ min-height: 100vh;
+ font-family: Arial, sans-serif;
+ background:
+ radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 30%),
+ linear-gradient(180deg, var(--body-top), var(--body-bottom));
+ color: #f3f6fb;
+}
+
+.wrap {
+ max-width: 760px;
+ margin: 0 auto;
+ padding: 28px 18px 40px;
+}
+
+.calc {
+ background: linear-gradient(180deg, var(--shell-top), var(--shell-bottom));
+ border: 1px solid var(--shell-edge);
+ border-radius: 28px;
+ padding: 18px;
+ box-shadow:
+ 0 24px 60px var(--shadow),
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
+ inset 0 -2px 0 rgba(0, 0, 0, 0.28);
+}
+
+.brand {
+ color: #f7f8fc;
+ display: flex;
+ justify-content: space-between;
+ align-items: baseline;
+ margin-bottom: 14px;
+ gap: 12px;
+}
+
+.brand h1 {
+ margin: 0;
+ font-size: 20px;
+ letter-spacing: 0.08em;
+}
+
+.brand small {
+ color: #cfd6e2;
+}
+
+.screen {
+ background: linear-gradient(180deg, var(--screen-top), var(--screen));
+ color: var(--screen-text);
+ border: 3px solid #829366;
+ border-radius: 14px;
+ padding: 14px 16px;
+ min-height: 196px;
+ font-family: "Courier New", monospace;
+ display: grid;
+ grid-template-rows: auto auto 1fr;
+ gap: 10px;
+ box-shadow:
+ inset 0 2px 8px rgba(60, 80, 28, 0.18),
+ 0 8px 16px rgba(0, 0, 0, 0.18);
+}
+
+.screen-top {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ font-size: 12px;
+ color: var(--screen-dim);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+.stack {
+ border-top: 1px solid rgba(28, 41, 16, 0.24);
+ padding-top: 10px;
+ line-height: 1.5;
+ font-size: 18px;
+ white-space: pre-wrap;
+}
+
+.stack-line {
+ display: grid;
+ grid-template-columns: 26px 1fr;
+ gap: 8px;
+ padding: 2px 4px;
+ border-radius: 4px;
+}
+
+.stack-line.selected {
+ background: rgba(28, 41, 16, 0.12);
+ outline: 1px dashed rgba(28, 41, 16, 0.4);
+}
+
+.stack-line.moving {
+ background: rgba(117, 160, 90, 0.2);
+ outline: 1px solid rgba(28, 41, 16, 0.5);
+}
+
+.stack-line .label {
+ text-align: right;
+ color: var(--screen-dim);
+}
+
+.stack-line.selected .label,
+.stack-line.moving .label {
+ color: var(--screen-text);
+ font-weight: bold;
+}
+
+#display {
+ font-size: 18px;
+ font-weight: bold;
+ letter-spacing: 0.04em;
+}
+
+.hidden-input {
+ position: absolute;
+ left: -9999px;
+ width: 1px;
+ height: 1px;
+ opacity: 0;
+ pointer-events: none;
+}
+
+.toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ gap: 14px;
+ margin-top: 14px;
+}
+
+.status {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ color: #edf2fa;
+ font-size: 12px;
+}
+
+.pill {
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 999px;
+ padding: 6px 10px;
+ background: rgba(255, 255, 255, 0.06);
+}
+
+.mode-select {
+ display: grid;
+ gap: 6px;
+ min-width: 148px;
+ font-size: 12px;
+ color: #d9e1ec;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+}
+
+select,
+button {
+ border-radius: 12px;
+ border: 1px solid var(--border);
+ font: inherit;
+}
+
+select {
+ padding: 10px 12px;
+ background: #edf1f7;
+ color: #111;
+}
+
+.keyboard-layout {
+ margin-top: 18px;
+ display: grid;
+ grid-template-columns: 4fr 3fr 2fr;
+ gap: 12px;
+}
+
+.key-group {
+ background: rgba(6, 10, 16, 0.18);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ border-radius: 18px;
+ padding: 12px;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05);
+}
+
+.group-title {
+ margin: 0 0 10px;
+ color: #dce4f0;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.14em;
+}
+
+.key-grid {
+ display: grid;
+ gap: 10px;
+}
+
+.functions-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ grid-template-rows: repeat(5, 56px);
+}
+
+.numbers-grid {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ grid-template-rows: repeat(5, 56px);
+}
+
+.operators-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ grid-template-rows: repeat(5, 56px);
+}
+
+.key-spacer {
+ border-radius: 12px;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px dashed rgba(255, 255, 255, 0.04);
+}
+
+button {
+ padding: 10px 8px;
+ color: var(--key-text);
+ cursor: pointer;
+ font-weight: bold;
+ box-shadow:
+ inset 0 1px 0 rgba(255, 255, 255, 0.12),
+ 0 3px 0 rgba(0, 0, 0, 0.25);
+ transition: filter 120ms ease, transform 120ms ease;
+}
+
+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);
+}
+
+.key-function {
+ background: linear-gradient(180deg, var(--key-fn-top), var(--key-fn-bottom));
+}
+
+.key-number {
+ background: linear-gradient(180deg, var(--key-num-top), var(--key-num-bottom));
+}
+
+.key-operator {
+ background: linear-gradient(180deg, var(--key-op-top), var(--key-op-bottom));
+}
+
+.key-danger {
+ background: linear-gradient(180deg, var(--key-danger-top), var(--key-danger-bottom));
+}
+
+.key-enter {
+ background: linear-gradient(180deg, var(--key-enter-top), var(--key-enter-bottom));
+ grid-row: span 2;
+}
+
+.error {
+ margin-top: 12px;
+ min-height: 20px;
+ color: #ff9f9f;
+ font-family: "Courier New", monospace;
+ font-size: 13px;
+}
+
+.hint {
+ color: #cfd7e3;
+ margin-top: 10px;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
+@media (max-width: 860px) {
+ .keyboard-layout {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 640px) {
+ .wrap {
+ padding: 14px;
+ }
+
+ .calc {
+ padding: 14px;
+ border-radius: 22px;
+ }
+
+ .brand {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ .toolbar {
+ flex-direction: column;
+ }
+
+ .mode-select {
+ min-width: 0;
+ width: 100%;
+ }
+
+ .functions-grid,
+ .numbers-grid,
+ .operators-grid {
+ grid-template-rows: repeat(5, 52px);
+ }
+}
+
diff --git a/samples/calc-01/index.html b/samples/calc-01/index.html
new file mode 100644
index 0000000..34a9e6e
--- /dev/null
+++ b/samples/calc-01/index.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+ HP48-style RPN Calculator
+
+
+
+
+
+
+
HP48-style RPN
+ powered by src/rpn-calculator.js
+
+
+
+
+
+
+
+
+
+
+
+
Keyboard: digits, numpad, Enter, Backspace, Delete, Esc, ↑, ↓, →, +, -, *, /, %, ^, q, n, r, i, g, l, s, c, S, C, x, y, z, t
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/calc-01/index.js b/samples/calc-01/index.js
new file mode 100644
index 0000000..302a5f9
--- /dev/null
+++ b/samples/calc-01/index.js
@@ -0,0 +1,594 @@
+const calc = new RpnCalculator({ angleMode: 'deg' });
+const input = document.getElementById('input');
+const screen = document.getElementById('screen');
+const stackEl = document.getElementById('stack');
+
+const displayEl = document.getElementById('display');
+const errorEl = document.getElementById('error');
+const inputValueLabel = document.getElementById('inputValueLabel');
+const editingLabel = document.getElementById('editingLabel');
+const modeLabel = document.getElementById('modeLabel');
+const angleMode = document.getElementById('angleMode');
+
+const keyLayouts = {
+ functions: [
+ [
+ { type: 'command', value: 'pi', label: 'π', className: 'key-function' },
+ { type: 'command', value: 'e', label: 'e', className: 'key-function' },
+ null,
+ null,
+ ],
+ [
+ { type: 'command', value: 'sqrt', label: 'sqrt', className: 'key-function' },
+ { type: 'command', value: 'pow', label: 'y^x', className: 'key-function' },
+ { type: 'command', value: 'sqr', label: 'x²', className: 'key-function' },
+ { type: 'command', value: 'recip', label: '1/x', className: 'key-function' },
+ ],
+ [
+ { type: 'command', value: 'log', label: 'log', className: 'key-function' },
+ { type: 'command', value: 'ln', label: 'ln', className: 'key-function' },
+ null,
+ { type: 'command', value: 'mod', label: '%', className: 'key-function' },
+ ],
+ [
+ { type: 'command', value: 'sin', label: 'sin', className: 'key-function' },
+ { type: 'command', value: 'cos', label: 'cos', className: 'key-function' },
+ { type: 'command', value: 'tan', label: 'tan', className: 'key-function' },
+ null,
+ ],
+ [
+ { type: 'command', value: 'asin', label: 'asin', className: 'key-function' },
+ { type: 'command', value: 'acos', label: 'acos', className: 'key-function' },
+ { type: 'command', value: 'atan', label: 'atan', className: 'key-function' },
+ null,
+ ],
+ ],
+ numbers: [
+ [null, null, null],
+ [
+ { type: 'input', value: '7', label: '7', className: 'key-number' },
+ { type: 'input', value: '8', label: '8', className: 'key-number' },
+ { type: 'input', value: '9', label: '9', className: 'key-number' },
+ ],
+ [
+ { type: 'input', value: '4', label: '4', className: 'key-number' },
+ { type: 'input', value: '5', label: '5', className: 'key-number' },
+ { type: 'input', value: '6', label: '6', className: 'key-number' },
+ ],
+ [
+ { type: 'input', value: '1', label: '1', className: 'key-number' },
+ { type: 'input', value: '2', label: '2', className: 'key-number' },
+ { type: 'input', value: '3', label: '3', className: 'key-number' },
+ ],
+ [
+ { type: 'input', value: '0', label: '0', className: 'key-number' },
+ { type: 'input', value: '.', label: '.', className: 'key-number' },
+ { type: 'command', value: 'neg', label: '+/−', className: 'key-number' },
+ ],
+ ],
+ operators: [
+ [
+ { type: 'command', value: 'clear', label: 'del', className: 'key-danger' },
+ { type: 'action', value: 'escape', label: 'esc', className: 'key-danger' },
+ ],
+ [
+ { type: 'command', value: 'div', label: '/', className: 'key-operator' },
+ { type: 'command', value: 'drop', label: 'back', className: 'key-operator' },
+ ],
+ [
+ { type: 'command', value: 'mul', label: '*', className: 'key-operator' },
+ null,
+ ],
+ [
+ { type: 'command', value: 'sub', label: '-', className: 'key-operator' },
+ { type: 'command', value: 'enter', label: 'Enter', className: 'key-enter' },
+ ],
+ [
+ { type: 'command', value: 'add', label: '+', className: 'key-operator' },
+ null,
+ ],
+ ],
+};
+
+let stackCursor = null;
+let isMovingStackItem = false;
+let stackSnapshotBeforeMove = null;
+let stackViewOffset = 0;
+let editRestoreValue = null;
+
+function handleEscapeAction() {
+ if (calc.isEditing) {
+ if (editRestoreValue !== null) {
+ calc.push(editRestoreValue);
+ editRestoreValue = null;
+ }
+ calc.inputValue = '';
+ calc.isEditing = false;
+ syncInputFromState();
+ render();
+ return;
+ }
+
+ if (isMovingStackItem) {
+ cancelMoveMode();
+ clearStackSelection();
+ render();
+ return;
+ }
+
+ if (hasStackSelection()) {
+ clearStackSelection();
+ render();
+ }
+}
+
+function pressKey(key) {
+ clearStackSelection();
+ editXWithKey(key);
+ render();
+}
+
+function createButton(cell) {
+ if (!cell) {
+ const spacer = document.createElement('div');
+ spacer.className = 'key-spacer';
+ spacer.setAttribute('aria-hidden', 'true');
+ return spacer;
+ }
+
+ const button = document.createElement('button');
+ button.type = 'button';
+ button.textContent = cell.label;
+ button.className = cell.className;
+ button.addEventListener('click', () => {
+ focusScreen();
+ if (cell.type === 'input') {
+ pressKey(cell.value);
+ return;
+ }
+ if (cell.type === 'action' && cell.value === 'escape') {
+ handleEscapeAction();
+ return;
+ }
+ execute(cell.value);
+ });
+ return button;
+}
+
+function renderKeyLayout(container, rows) {
+ container.innerHTML = '';
+ rows.flat().forEach((cell) => {
+ container.appendChild(createButton(cell));
+ });
+}
+
+function getStackValue(index) {
+ return calc.isValidIndex(index) ? calc.stack[index] : undefined;
+}
+
+function getDisplayValue(index) {
+ if (calc.isEditing) {
+ if (index === 0) {
+ return calc.inputValue;
+ }
+ return getStackValue(index - 1);
+ }
+ return getStackValue(index);
+}
+
+function hasStackSelection() {
+ return stackCursor !== null && calc.isValidIndex(stackCursor);
+}
+
+function clearStackSelection() {
+ stackCursor = null;
+ isMovingStackItem = false;
+ stackSnapshotBeforeMove = null;
+ stackViewOffset = 0;
+}
+
+function ensureValidSelection() {
+ if (hasStackSelection()) {
+ return;
+ }
+ stackCursor = calc.isValidIndex(0) ? 0 : null;
+}
+
+function beginMoveMode() {
+ if (!hasStackSelection()) {
+ return;
+ }
+ isMovingStackItem = true;
+ stackSnapshotBeforeMove = calc.stack.slice();
+}
+
+function commitMoveMode() {
+ isMovingStackItem = false;
+ stackSnapshotBeforeMove = null;
+}
+
+function cancelMoveMode() {
+ if (!isMovingStackItem || !stackSnapshotBeforeMove) {
+ return;
+ }
+
+ const snapshot = stackSnapshotBeforeMove.slice();
+ calc.clear();
+ for (let index = snapshot.length - 1; index >= 0; index -= 1) {
+ calc.push(snapshot[index]);
+ }
+
+ isMovingStackItem = false;
+ stackSnapshotBeforeMove = null;
+ stackCursor = calc.isValidIndex(stackCursor) ? stackCursor : (calc.isValidIndex(0) ? 0 : null);
+ syncInputFromState();
+}
+
+function reactivateEditOnX() {
+ clearStackSelection();
+ if (calc.isValidIndex(0)) {
+ const value = getStackValue(0);
+ calc.remove(0);
+ calc.inputValue = calc.formatNumber(value);
+ calc.isEditing = true;
+ editRestoreValue = value;
+ } else {
+ calc.inputValue = '';
+ calc.isEditing = true;
+ editRestoreValue = null;
+ }
+ syncInputFromState();
+}
+
+function moveStackSelection(direction) {
+ if (!hasStackSelection()) {
+ if (direction === 'up') {
+ ensureValidSelection();
+ } else if (direction === 'down') {
+ reactivateEditOnX();
+ }
+ return;
+ }
+
+ const nextIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1;
+ if (calc.isValidIndex(nextIndex)) {
+ stackCursor = nextIndex;
+ }
+}
+
+function moveStackItem(direction) {
+ if (!hasStackSelection()) {
+ return;
+ }
+
+ const targetIndex = direction === 'up' ? stackCursor + 1 : stackCursor - 1;
+ if (!calc.isValidIndex(targetIndex)) {
+ return;
+ }
+
+ calc.swap(stackCursor, targetIndex);
+ stackCursor = targetIndex;
+}
+
+function getVisibleStackIndex(visualLine) {
+ return stackViewOffset + visualLine;
+}
+
+function clampStackViewOffset() {
+ const maxOffset = Math.max(0, calc.stack.length - 4);
+ if (stackViewOffset < 0) {
+ stackViewOffset = 0;
+ } else if (stackViewOffset > maxOffset) {
+ stackViewOffset = maxOffset;
+ }
+}
+
+function ensureSelectionVisible() {
+ if (!hasStackSelection()) {
+ stackViewOffset = 0;
+ return;
+ }
+
+ if (stackCursor < stackViewOffset) {
+ stackViewOffset = stackCursor;
+ } else if (stackCursor > stackViewOffset + 3) {
+ stackViewOffset = stackCursor - 3;
+ }
+
+ clampStackViewOffset();
+}
+
+function render() {
+ const names = ['T', 'Z', 'Y', 'X'];
+ const lines = [];
+ const showStackIndexes = hasStackSelection() || isMovingStackItem;
+
+ clampStackViewOffset();
+ ensureSelectionVisible();
+
+ for (let visualLine = 3; visualLine >= 0; visualLine -= 1) {
+ const stackIndex = getVisibleStackIndex(visualLine);
+ const value = getDisplayValue(stackIndex);
+ const isSelected = stackCursor === stackIndex;
+ const classes = ['stack-line'];
+ const label = showStackIndexes ? String(stackIndex) : names[3 - visualLine];
+
+ if (isSelected) {
+ classes.push(isMovingStackItem ? 'moving' : 'selected');
+ }
+
+ lines.push(`${label}
${value !== undefined && value !== '' ? calc.formatNumber(value) : ''}
`);
+ }
+
+ stackEl.innerHTML = lines.join('');
+ if (calc.isEditing) {
+ displayEl.textContent = `ENTERING: ${calc.inputValue}`;
+ } else if (isMovingStackItem && hasStackSelection()) {
+ displayEl.textContent = `MOVING: ${stackCursor}`;
+ } else if (hasStackSelection()) {
+ displayEl.textContent = `SELECTED: ${stackCursor}`;
+ } else {
+ displayEl.textContent = 'READY';
+ }
+ inputValueLabel.textContent = calc.inputValue || '∅';
+ editingLabel.textContent = String(calc.isEditing);
+ modeLabel.textContent = calc.angleMode;
+ angleMode.value = calc.angleMode;
+ errorEl.textContent = '';
+}
+
+function pushEditingValueIfNeeded() {
+ if (!calc.isEditing) return;
+ if (calc.inputValue !== '') {
+ const value = calc.parseInputValue(calc.inputValue);
+ calc.push(value);
+ }
+ calc.inputValue = '';
+ calc.isEditing = false;
+ editRestoreValue = null;
+ syncInputFromState();
+}
+
+function execute(name) {
+ try {
+ if (name === 'enter') {
+ if (calc.isEditing) {
+ pushEditingValueIfNeeded();
+ } else if (calc.isValidIndex(0)) {
+ calc.push(getStackValue(0));
+ }
+ } else if (name === 'swap') {
+ pushEditingValueIfNeeded();
+ clearStackSelection();
+ if (calc.isValidIndex(1)) calc.swap(0, 1);
+ } else if (name === 'drop') {
+ pushEditingValueIfNeeded();
+ if (hasStackSelection()) {
+ calc.remove(stackCursor);
+ stackCursor = calc.isValidIndex(stackCursor) ? stackCursor : (calc.isValidIndex(stackCursor - 1) ? stackCursor - 1 : null);
+ } else if (calc.isValidIndex(0)) {
+ calc.remove(0);
+ }
+ commitMoveMode();
+ } else if (name === 'clear') {
+ calc.clear();
+ clearStackSelection();
+ } else {
+ pushEditingValueIfNeeded();
+ clearStackSelection();
+ calc.command(name);
+ }
+ syncInputFromState();
+ render();
+ } catch (error) {
+ errorEl.textContent = error.message;
+ }
+}
+
+function isInputChar(key) {
+ return /^[0-9a-fA-F.]$/.test(key);
+}
+
+function shouldIgnoreKeyboardEvent(event) {
+ const target = event.target;
+ if (!target) return false;
+
+ const tagName = target.tagName;
+ return (
+ tagName === 'INPUT' ||
+ tagName === 'TEXTAREA' ||
+ tagName === 'SELECT' ||
+ target.isContentEditable
+ );
+}
+
+function getKeyboardAction(event) {
+ const numpadMap = {
+ Numpad0: { type: 'input', value: '0' },
+ Numpad1: { type: 'input', value: '1' },
+ Numpad2: { type: 'input', value: '2' },
+ Numpad3: { type: 'input', value: '3' },
+ Numpad4: { type: 'input', value: '4' },
+ Numpad5: { type: 'input', value: '5' },
+ Numpad6: { type: 'input', value: '6' },
+ Numpad7: { type: 'input', value: '7' },
+ Numpad8: { type: 'input', value: '8' },
+ Numpad9: { type: 'input', value: '9' },
+ NumpadDecimal: { type: 'input', value: '.' },
+ NumpadAdd: { type: 'command', value: 'add' },
+ NumpadSubtract: { type: 'command', value: 'sub' },
+ NumpadMultiply: { type: 'command', value: 'mul' },
+ NumpadDivide: { type: 'command', value: 'div' },
+ NumpadEnter: { type: 'command', value: 'enter' },
+ };
+
+ if (numpadMap[event.code]) {
+ return numpadMap[event.code];
+ }
+
+ if (isInputChar(event.key)) {
+ return { type: 'input', value: event.key };
+ }
+
+ const keyMap = {
+ Enter: { type: 'enterKey' },
+ Backspace: { type: 'stackOrEdit', value: 'drop' },
+ Delete: { type: 'command', value: 'clear' },
+ Escape: { type: 'escapeKey' },
+ ArrowUp: { type: 'stackArrow', value: 'up' },
+ ArrowDown: { type: 'stackArrow', value: 'down' },
+ ArrowRight: { type: 'command', value: 'swap' },
+ '+': { type: 'command', value: 'add' },
+ '-': { type: 'command', value: 'sub' },
+ '*': { type: 'command', value: 'mul' },
+ '/': { type: 'command', value: 'div' },
+ '%': { type: 'command', value: 'mod' },
+ '^': { type: 'command', value: 'pow' },
+ q: { type: 'command', value: 'sqr' },
+ n: { type: 'command', value: 'neg' },
+ r: { type: 'command', value: 'sqrt' },
+ i: { type: 'command', value: 'recip' },
+ g: { type: 'command', value: 'log' },
+ l: { type: 'command', value: 'ln' },
+ s: { type: 'command', value: 'sin' },
+ c: { type: 'command', value: 'cos' },
+ S: { type: 'command', value: 'asin' },
+ C: { type: 'command', value: 'acos' },
+ x: { type: 'stackSelect', value: 0 },
+ y: { type: 'stackSelect', value: 1 },
+ z: { type: 'stackSelect', value: 2 },
+ t: { type: 'stackSelect', value: 3 },
+ X: { type: 'stackSelect', value: 0 },
+ Y: { type: 'stackSelect', value: 1 },
+ Z: { type: 'stackSelect', value: 2 },
+ T: { type: 'stackSelect', value: 3 },
+ };
+
+ return keyMap[event.key] || null;
+}
+
+function focusScreen() {
+ screen.focus();
+}
+
+function syncInputFromState() {
+ input.value = calc.inputValue;
+}
+
+function editXWithKey(key) {
+ if (!calc.isEditing) {
+ pushEditingValueIfNeeded();
+ calc.isEditing = true;
+ calc.inputValue = '';
+ editRestoreValue = null;
+ }
+ if (key === 'Backspace') {
+ calc.inputValue = calc.inputValue.slice(0, -1);
+ } else {
+ calc.inputValue += key;
+ }
+ if (calc.inputValue === '') {
+ calc.isEditing = false;
+ editRestoreValue = null;
+ }
+ syncInputFromState();
+}
+
+function handleKeydown(event) {
+ if (shouldIgnoreKeyboardEvent(event)) {
+ return;
+ }
+
+ const action = getKeyboardAction(event);
+ if (!action) {
+ return;
+ }
+
+ try {
+ if (action.type === 'escapeKey') {
+ event.preventDefault();
+ handleEscapeAction();
+ return;
+ }
+
+ event.preventDefault();
+
+ if (action.type === 'input') {
+ clearStackSelection();
+ editXWithKey(action.value);
+ render();
+ return;
+ }
+
+ if (action.type === 'stackSelect') {
+ if (calc.isEditing || isMovingStackItem) {
+ return;
+ }
+ stackCursor = calc.isValidIndex(action.value) ? action.value : null;
+ render();
+ return;
+ }
+
+ if (action.type === 'stackArrow') {
+ if (calc.isEditing) {
+ render();
+ return;
+ }
+
+ if (isMovingStackItem) {
+ moveStackItem(action.value);
+ } else {
+ moveStackSelection(action.value);
+ }
+ render();
+ return;
+ }
+
+ if (action.type === 'stackOrEdit') {
+ if (calc.isEditing) {
+ editXWithKey('Backspace');
+ render();
+ } else {
+ execute(action.value);
+ }
+ return;
+ }
+
+ if (action.type === 'enterKey') {
+ if (hasStackSelection()) {
+ if (isMovingStackItem) {
+ commitMoveMode();
+ } else {
+ beginMoveMode();
+ }
+ render();
+ return;
+ }
+ execute('enter');
+ return;
+ }
+
+ if (action.type === 'command') {
+ execute(action.value);
+ }
+ } catch (error) {
+ errorEl.textContent = error.message;
+ }
+}
+
+window.addEventListener('keydown', handleKeydown);
+
+screen.addEventListener('click', focusScreen);
+window.addEventListener('load', focusScreen);
+
+angleMode.addEventListener('change', (event) => {
+ calc.angleMode = event.target.value;
+ render();
+});
+
+renderKeyLayout(document.getElementById('functionsButtons'), keyLayouts.functions);
+renderKeyLayout(document.getElementById('numbersButtons'), keyLayouts.numbers);
+renderKeyLayout(document.getElementById('operatorsButtons'), keyLayouts.operators);
+
+render();
+focusScreen();
diff --git a/samples/calc-01/visual.txt b/samples/calc-01/visual.txt
new file mode 100644
index 0000000..4732b1a
--- /dev/null
+++ b/samples/calc-01/visual.txt
@@ -0,0 +1,7 @@
+┌───────────── Functions ────────────┬──── Numbers ────┬─── Operators ───┐
+| consts | | | | | | | del | esc |
+| sqrt | y^x | x² | 1/x | 7 | 8 | 9 | / | backspace |
+| log | ln | | % | 4 | 5 | 6 | * | |
+| sin | cos | tan | | 1 | 2 | 3 | - | Enter |
+| asin | acos | atan | | 0 | . | +/- | + | Enter |
+└────────────────────────────────────┴─────────────────┴─────────────────┘