Compare commits

...

4 Commits

5 changed files with 105 additions and 28 deletions
+9 -8
View File
@@ -1,8 +1,9 @@
# Project memory
- RPN calculator JS project.
- Read `.memory/state.md` for current state.
- Keep names and commands in English.
- Update memory files based on events: engine, demo, API, commands, exports, docs, or tasks.
- Core arithmetic now includes `root` for y-th roots, and `samples/calc-02/` uses it for `y√x`.
- `samples/calc-02/` in portrait mode remains the active responsive demo.
# State
- Core engine: `src/rpn-calculator.js`
- Active demo: `samples/calc-02/` portrait-only HP48GX layout; display-adjacent button row stays in 4 columns and now shares the same base button styling as the function keys
- Mode button shows the current angle mode only; selecting a mode uses a popup menu
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant`
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic now includes `root` for y-th roots; constants can now be added or removed dynamically through the core API and the calc-02 constant menu reads from the engine
- Demo actions: keyboard focus is kept on the hidden input on desktop so typing keeps working; the keypad layout places Enter in the bottom-left, ± in the former Enter position, and Esc before Clear for safety; paste parses clipboard text as a number before pushing it to the stack; Ctrl+V is supported via the hidden input paste event; backspace is ignored when the stack is empty; operation errors are shown as an overlay bar on top of the calculator with a shorter timeout and darker red; the display button row uses a 4-column grid with a spacer cell to preserve alignment
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+2 -2
View File
@@ -2,8 +2,8 @@
- Core engine: `src/rpn-calculator.js`
- Active demo: `samples/calc-02/` portrait-only HP48GX layout; display-adjacent button row stays in 4 columns and now shares the same base button styling as the function keys
- Mode button shows the current angle mode only; selecting a mode uses a popup menu
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant`
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic now includes `root` for y-th roots
- Commands: arithmetic, stack, trigonometry, constants `pi`, `e`, `phi`, `g`, and `c`; arithmetic now includes `root` for y-th roots; constants can now be added or removed dynamically through the core API
- Demo actions: keyboard focus is kept on the hidden input on desktop so typing keeps working; the keypad layout places Enter in the bottom-left, ± in the former Enter position, and Esc before Clear for safety; paste parses clipboard text as a number before pushing it to the stack; Ctrl+V is supported via the hidden input paste event; backspace is ignored when the stack is empty; operation errors are shown as an overlay bar on top of the calculator with a shorter timeout and darker red; the display button row uses a 4-column grid with a spacer cell to preserve alignment
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+22 -8
View File
@@ -39,11 +39,10 @@ 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));
background: var(--bg0);
}
.app-shell {
min-height: 100vh;
display: grid;
@@ -58,8 +57,7 @@ body {
display: grid;
padding: 8px;
border-radius: 8px;
background: linear-gradient(180deg, var(--panel2), var(--panel));
border: 1px solid var(--edge);
background: var(--panel);
box-shadow: 0 26px 70px var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08);
grid-template-columns: 1fr;
grid-template-rows: auto auto auto auto auto;
@@ -105,7 +103,7 @@ body {
width: 100%;
padding: 16px;
padding-bottom: 8px;
background: linear-gradient(180deg, var(--display), var(--display2));
background: var(--display);
color: var(--displayText);
font-family: "Courier New", monospace;
overflow: hidden;
@@ -279,12 +277,11 @@ body {
.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));
grid-template-rows: repeat(5, minmax(0, 1.7fr));
}
.functions-grid,
@@ -378,6 +375,23 @@ button:active {
color: #eef2f7;
}
.keypad-grid > button {
min-height: 1.7em;
}
#keypadGrid > button {
min-height: calc(1.7em * 1.7);
padding-top: calc(8px * 1.7);
padding-bottom: calc(8px * 1.7);
}
#keypadGrid > button.key-default,
#keypadGrid > button.key-accent,
#keypadGrid > button.key-danger,
#keypadGrid > button.key-enter {
min-height: calc(1.7em * 1.7);
}
.key-accent {
background: linear-gradient(180deg, var(--btnAccentTop), var(--btnAccentBottom));
color: #eef2f7;
+17 -10
View File
@@ -383,6 +383,7 @@ modeButton.addEventListener('click', (event) => {
window.addEventListener('resize', () => {
closeModeMenu();
closeConstMenu();
render();
});
window.addEventListener('scroll', () => {
closeModeMenu();
@@ -422,13 +423,14 @@ hiddenInput.addEventListener('paste', (event) => {
upButton.addEventListener('click', () => {});
const constants = [
{ label: 'π', value: Math.PI },
{ label: 'e', value: Math.E },
{ label: 'φ', value: (1 + Math.sqrt(5)) / 2 },
{ label: 'g', value: 9.80665 },
{ label: 'c', value: 299792458 },
];
const constantLabels = {
pi: 'π',
e: 'e',
phi: 'φ',
g: 'g',
c: 'c',
};
const constantOrder = ['pi', 'e', 'phi', 'g', 'c'];
let constMenuEl = null;
@@ -449,14 +451,19 @@ function toggleConstMenu() {
constMenuEl = document.createElement('div');
constMenuEl.className = 'mode-menu';
constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
constants.forEach((constant) => {
const availableConstants = calc.listConstants();
const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))];
keys.forEach((name) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'mode-menu-item';
button.textContent = constant.label;
if (!Object.prototype.hasOwnProperty.call(availableConstants, name)) {
return;
}
button.textContent = constantLabels[name] ?? name;
button.addEventListener('click', () => {
pushEditingValueIfNeeded();
calc.push(constant.value);
calc.push(availableConstants[name]);
render();
clearStatus();
closeConstMenu();
+55
View File
@@ -2,6 +2,9 @@ class RpnCalculator {
static CONSTANTS = {
pi: Math.PI,
e: Math.E,
phi: (1 + Math.sqrt(5)) / 2,
g: 9.80665,
c: 299792458,
};
static OPERATIONS = {
@@ -212,6 +215,15 @@ class RpnCalculator {
this.enabledCommands = new Set(selectedCommands.map((name) => this.normalizeCommandName(name)).filter((name) => RpnCalculator.OPERATIONS[name]));
}
static isValidConstantName(name) {
return typeof name === 'string' && name.trim() !== '';
}
static isReservedName(name) {
const normalized = typeof name === 'string' ? name.toLowerCase() : '';
return Boolean(RpnCalculator.OPERATIONS[normalized]);
}
toRadians(value) {
if (this.angleMode === 'grad') {
return (value * Math.PI) / 200;
@@ -257,6 +269,45 @@ class RpnCalculator {
return typeof name === 'string' && Object.prototype.hasOwnProperty.call(this.constants, name.toLowerCase());
}
setConstant(name, value) {
if (!RpnCalculator.isValidConstantName(name)) {
throw new Error('Invalid constant name');
}
if (!Number.isFinite(value)) {
throw new Error('Invalid constant value');
}
const normalized = name.trim().toLowerCase();
if (RpnCalculator.isReservedName(normalized)) {
throw new Error(`Constant name conflicts with a command: ${name}`);
}
this.constants[normalized] = value;
return value;
}
removeConstant(name) {
if (!RpnCalculator.isValidConstantName(name)) {
throw new Error('Invalid constant name');
}
const normalized = name.trim().toLowerCase();
if (!this.isConstantName(normalized)) {
return false;
}
delete this.constants[normalized];
return true;
}
hasConstant(name) {
if (!RpnCalculator.isValidConstantName(name)) {
return false;
}
return this.isConstantName(name.trim().toLowerCase());
}
push(value) {
if (this.stack.length >= this.maxSize) {
throw new Error('Stack overflow');
@@ -408,6 +459,10 @@ class RpnCalculator {
getConstants() {
return { ...this.constants };
}
listConstants() {
return this.getConstants();
}
}
if (typeof window !== 'undefined') {