Compare commits

..

38 Commits

Author SHA1 Message Date
matmoul a24142cd72 docs: remove obsolete temporary README file 2026-05-16 05:23:42 +02:00
matmoul 4df99d0738 docs: update README for default demo and command aliases 2026-05-16 05:01:17 +02:00
matmoul 1cef2d2d6a fix(calc-02): reorder display buttons in portrait layout 2026-05-16 04:44:18 +02:00
matmoul 442a97d573 fix: reorder calc display buttons in portrait layout 2026-05-16 04:34:20 +02:00
matmoul 44ce67c5cc docs: align calc-02 visual portrait tables 2026-05-16 04:20:22 +02:00
matmoul a7411243de fix: reorder display buttons in calc-02 sample 2026-05-16 04:14:28 +02:00
matmoul f8de4e1709 docs: update project memory for calc-02 UI and command notes 2026-05-16 04:10:19 +02:00
matmoul 003d4fde1b feat(calc-02): add shared popup menus for mode and constants
Refactor the calc-02 demo to use a single popup menu component for angle mode and constants, align the menus to their trigger buttons, and update the README/project notes to reflect the portrait-first demo layout and constant API.
2026-05-16 04:04:01 +02:00
matmoul e5f50aee0a fix: reorder calc-02 display buttons 2026-05-16 03:44:20 +02:00
matmoul 6a7a60a9bc fix: align calc menus with trigger width 2026-05-16 03:41:25 +02:00
matmoul a37ed59b40 fix: reorder calc-02 display buttons 2026-05-16 03:37:04 +02:00
matmoul c09fdc7e0f refactor(calc-02): simplify display and button styling
Remove layered shadows and pressed-state transforms from the calculator UI, and update the stack copy icon markup and active feedback to match the new flat design.
2026-05-16 03:21:49 +02:00
matmoul 849170ea49 docs: update calc-02 portrait visual reference
Move the portrait layout documentation to samples/calc-02/visual-portrait.md and refresh the project memory note to point to the new reference.
2026-05-16 03:11:00 +02:00
matmoul 0100da80bb fix(calc-02): adjust keypad grid sizing 2026-05-16 03:02:43 +02:00
matmoul e1fd730db5 fix: remove panel border from calc sample 2026-05-16 02:48:59 +02:00
matmoul ba53f51bf7 refactor(samples): simplify calc-02 background styling 2026-05-16 02:48:00 +02:00
matmoul 534bbc0afb feat: add dynamic constant management to the calculator core 2026-05-16 02:23:20 +02:00
matmoul 2504716c64 fix: tighten calc stack display layout
Reorder the stack cell grid so the copy button sits beside the label and reduce spacing to better align the value column.
2026-05-16 02:04:47 +02:00
matmoul 6c8c032f7a fix: stabilize calculator display sizing 2026-05-16 02:02:45 +02:00
matmoul d8d0556822 style: tighten calculator sample layout spacing 2026-05-16 01:48:52 +02:00
matmoul 426fd326a5 fix(calc-02): simplify button spacing and menu background 2026-05-16 01:43:00 +02:00
matmoul 5364208491 feat(calc-02): refine display button layout and icon styling 2026-05-16 01:38:53 +02:00
matmoul 1e703bebe8 fix(calc-02): adjust display button layout and enter key spacing 2026-05-16 01:28:30 +02:00
matmoul 54797f9dd9 refactor(samples/calc-02): simplify layout to portrait-only single column
Remove the responsive two-column desktop arrangement and make the calculator stack vertically with a 4-column display button row that preserves alignment. Also align the display buttons with the shared base button styling.
2026-05-16 01:16:47 +02:00
matmoul 75bf6d69df fix(calc-02): align display buttons in a grid
Wrap the display controls in a dedicated grid container, add a spacer for the missing cell, and simplify the button styling so the layout stays consistent across sizes.
2026-05-16 01:08:11 +02:00
matmoul 256e9f2b33 refactor(calc-02): move display buttons into their own grid area 2026-05-16 01:01:14 +02:00
matmoul 77fb671dcf fix: use typographic operator symbols in calculator keypad 2026-05-16 00:37:16 +02:00
matmoul ae11cb8007 fix: tighten calculator layout for full-screen mobile sizing 2026-05-16 00:33:58 +02:00
matmoul ba7fc8b4d6 fix: right-align calculator display text 2026-05-16 00:02:10 +02:00
matmoul b45cfe8091 feat: add root command and fix calc-02 exponent shortcuts 2026-05-15 23:05:43 +02:00
matmoul cb45efff43 feat: add root operation to RPN calculator 2026-05-15 22:58:21 +02:00
matmoul 4e8155b5f0 fix(calc-02): tighten calculator layout and panel styling
Wrap the display and control buttons in a shared display block so the
stack and button panels align as a single unit. Reduce corner radii and
adjust gaps/padding across the calculator to better fit the updated
portrait and desktop layouts.
2026-05-15 22:35:19 +02:00
matmoul 432523c23f fix: toggle calc sample menus on repeated clicks 2026-05-15 21:50:36 +02:00
matmoul 9cbddfa0c2 fix: align constant menu to the right edge 2026-05-15 21:46:49 +02:00
matmoul db3bee6e89 feat(calc-02): restyle enter key label and colors 2026-05-15 21:44:41 +02:00
matmoul 62221a9baa feat: add arrow key navigation to calculator sample 2026-05-15 21:38:14 +02:00
matmoul c47c46ad64 fix: clear status after successful clipboard and constant actions 2026-05-15 21:33:38 +02:00
matmoul 80bcdac320 feat: add stack copy buttons to calculator display 2026-05-15 21:32:43 +02:00
9 changed files with 530 additions and 338 deletions
+8 -6
View File
@@ -1,6 +1,8 @@
# Project memory # State
- Core engine: `src/rpn-calculator.js`
- RPN calculator JS project. - Reference demo: `samples/calc-02/` (portrait-first HP48GX layout, compact mode/constants popups; Const button comes before Mode in the display row)
- Read `.memory/state.md` for current state. - Important UI behavior: mode button shows the current angle mode; keyboard focus stays on the hidden input on desktop; clipboard paste is supported
- Keep names and commands in English. - Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`, `listConstants`, `setConstant`, `removeConstant`, `hasConstant`
- Update memory files based on events: engine, demo, API, commands, exports, docs, or tasks. - Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e`; arithmetic includes `root`; constants can be added or removed dynamically through the core API
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+4 -5
View File
@@ -1,9 +1,8 @@
# State # State
- Core engine: `src/rpn-calculator.js` - Core engine: `src/rpn-calculator.js`
- Active demo: `samples/calc-02/` responsive HP48GX layout with HP48-like X-line editing; display-adjacent button row stays in 4 columns - Default demo: `samples/dev/` classic browser demo; `samples/calc-02/` remains the portrait-first HP48GX-inspired reference
- 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`
- Public API: `push`, `pop`, `clear`, `swap`, `remove`, `edit`, `isValidIndex`, `input`, `command`, `getOperationsByCategory`, `getConstants`
- Config: `maxSize`, `base`, `angleMode`, `enabledCommands` - Config: `maxSize`, `base`, `angleMode`, `enabledCommands`
- Commands: arithmetic, stack, trigonometry, constants `pi` and `e` - Commands: arithmetic, stack, trigonometry, constants `pi`, `e`, `phi`, `g`, and `c`; arithmetic includes `root` with `y√x`, `yroot`, and `nroot` aliases; constants can 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 - Demo behavior: `samples/dev/` keeps hidden input focus on desktop and supports clipboard paste; `samples/calc-02/` uses compact mode/constants popups, a 4-column display-adjacent row, stack selection/move mode, and paste through the hidden input
- Exports: browser `window.RpnCalculator`, CommonJS `module.exports` - Exports: browser `window.RpnCalculator`, CommonJS `module.exports`
+26 -18
View File
@@ -1,30 +1,28 @@
# RPN Virtual Calculator # RPN Virtual Calculator
A browser-friendly RPN calculator built around a small, generic JavaScript API. A browser-friendly RPN calculator built around a small JavaScript API.
## Overview ## Overview
This project provides: This project provides:
- a reusable JavaScript RPN engine in `src/rpn-calculator.js` - a reusable JavaScript RPN engine in `src/rpn-calculator.js`
- a browser demo in `samples/dev/` - a classic browser demo in `samples/dev/`
- a command system centralized in a single operation dictionary - a portrait-first HP48GX-inspired demo in `samples/calc-02/`
- a small public API focused on stack operations and generic command dispatch - a centralized command system with aliases
- a compact public API focused on stack operations, editing, and command dispatch
The main class is `RpnCalculator`. The main class is `RpnCalculator`.
## Project structure ## Project structure
- `src/rpn-calculator.js`: calculator engine - `src/rpn-calculator.js`: calculator engine
- `samples/dev/index.html`: active browser demo entry point - `samples/dev/index.html`: browser demo entry point
- `samples/dev/index.css`: demo styles - `samples/dev/index.css`: demo styles
- `samples/dev/index.js`: demo UI and keyboard logic - `samples/dev/index.js`: demo UI and keyboard logic
- `samples/calc-01/index.html`: active browser demo entry point - `samples/calc-02/index.html`: portrait-first HP48GX-style demo entry point
- `samples/calc-01/index.css`: demo styles - `samples/calc-02/index.css`: portrait-first demo styles
- `samples/calc-01/index.js`: demo UI and keyboard logic - `samples/calc-02/index.js`: portrait-first HP48GX-style demo UI and keyboard logic
- `samples/calc-02/index.html`: new responsive HP48GX-style demo entry point
- `samples/calc-02/index.css`: new responsive demo styles
- `samples/calc-02/index.js`: new demo UI and keyboard logic
- `samples/calc-XX/`: placeholder name for future demo variants - `samples/calc-XX/`: placeholder name for future demo variants
## Public API ## Public API
@@ -45,6 +43,10 @@ Instance helpers also available:
- `getOperationsByCategory()` - `getOperationsByCategory()`
- `getConstants()` - `getConstants()`
- `listConstants()`
- `setConstant(name, value)`
- `removeConstant(name)`
- `hasConstant(name)`
Static helpers also available: Static helpers also available:
@@ -84,6 +86,10 @@ Available constants:
- `pi` - `pi`
- `e` - `e`
- `phi`
- `g`
- `c`
- plus any user-defined constants added through the engine API
They can be used through `command(...)`: They can be used through `command(...)`:
@@ -110,6 +116,7 @@ calc.command('e');
- `div` alias: `/` - `div` alias: `/`
- `mod` alias: `%` - `mod` alias: `%`
- `pow` aliases: `^`, `y^x` - `pow` aliases: `^`, `y^x`
- `root` aliases: `y√x`, `yroot`, `nroot`
- `sqr` - `sqr`
- `neg` - `neg`
- `sqrt` alias: `sqrt(x)` - `sqrt` alias: `sqrt(x)`
@@ -134,6 +141,7 @@ calc.command('e');
- `sqrt` accepts alias `sqrt(x)` - `sqrt` accepts alias `sqrt(x)`
- `recip` accepts alias `1/x` - `recip` accepts alias `1/x`
- `div` and `recip` throw `Division by zero` when needed - `div` and `recip` throw `Division by zero` when needed
- `root` computes the y-th root as `x^(1/y)` and throws `Invalid input for root` for invalid inputs
- `sqrt` throws `Invalid input for sqrt` for negative values - `sqrt` throws `Invalid input for sqrt` for negative values
- `asin` and `acos` throw explicit domain errors outside `[-1, 1]` - `asin` and `acos` throw explicit domain errors outside `[-1, 1]`
- `log` throws `Invalid input for log` for values `<= 0` - `log` throws `Invalid input for log` for values `<= 0`
@@ -232,7 +240,7 @@ console.log(calc.pop()); // 0.5
## Demo ## Demo
The active demo lives in `samples/dev/`. The default demo lives in `samples/dev/`.
Main UI features: Main UI features:
@@ -242,7 +250,12 @@ Main UI features:
- angle mode selector for `deg`, `rad`, and `grad` - angle mode selector for `deg`, `rad`, and `grad`
- status pills for `inputValue` and `isEditing` - status pills for `inputValue` and `isEditing`
- grouped panels for `Stack`, `Arithmetic`, `Trigonometry`, and `Constants` - grouped panels for `Stack`, `Arithmetic`, `Trigonometry`, and `Constants`
- error display area - keyboard-friendly hidden input on desktop
## Calc 02 demo
`samples/calc-02/` is a portrait-first HP48GX-inspired demo.
It keeps the display-adjacent button row aligned in four columns, uses compact popup menus for mode and constants, and supports clipboard paste plus the `y√x` root operation.
The demo loads the engine from: The demo loads the engine from:
@@ -268,11 +281,6 @@ The current demo supports:
The demo also implements stack selection and stack-item move mode in its UI layer using the public calculator methods. The demo also implements stack selection and stack-item move mode in its UI layer using the public calculator methods.
It keeps the calculator screen focused and updates the visible stack window as the selection moves. It keeps the calculator screen focused and updates the visible stack window as the selection moves.
## Calc 02 demo
`samples/calc-02/` is a new responsive HP48GX-inspired demo.
It adapts its layout to the browser window and switches between the supplied portrait and landscape arrangements.
## Exports ## Exports
`RpnCalculator` is exposed in both environments: `RpnCalculator` is exposed in both environments:
+203 -183
View File
@@ -1,3 +1,4 @@
:root { :root {
--bg0: #10151e; --bg0: #10151e;
--bg1: #1b2432; --bg1: #1b2432;
@@ -19,8 +20,8 @@
--btnDangerBottom: #402d2f; --btnDangerBottom: #402d2f;
--btnEscapeTop: #6a4a2a; --btnEscapeTop: #6a4a2a;
--btnEscapeBottom: #4a331d; --btnEscapeBottom: #4a331d;
--btnEnterTop: #465349; --btnEnterTop: #4f7f4d;
--btnEnterBottom: #303a31; --btnEnterBottom: #355a34;
--btnText: #eef2f7; --btnText: #eef2f7;
} }
@@ -37,81 +38,94 @@ body {
min-height: 100vh; min-height: 100vh;
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
color: var(--buttonText); color: var(--buttonText);
background: background: var(--bg0);
radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 32%),
linear-gradient(180deg, var(--bg1), var(--bg0));
} }
.app-shell { .app-shell {
min-height: 100vh; min-height: 100vh;
display: grid; display: grid;
place-items: center; align-items: start;
padding: clamp(12px, 2vw, 28px); justify-items: center;
padding: 0;
} }
.calculator { .calculator {
width: min(100vw - 24px, 1120px); width: 100%;
height: min(100vh - 24px, 900px); height: auto;
display: grid; display: grid;
gap: clamp(10px, 1.4vw, 18px); padding: 8px;
padding: clamp(12px, 1.8vw, 18px); border-radius: 8px;
border-radius: 28px; background: var(--panel);
background: linear-gradient(180deg, var(--panel2), var(--panel)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
border: 1px solid var(--edge); grid-template-columns: 1fr;
box-shadow: 0 26px 70px var(--shadow), inset 0 1px 0 rgba(255, 255, 255, 0.08); grid-template-rows: auto auto auto auto auto;
grid-template-columns: 1.3fr 0.9fr; row-gap: 6px;
grid-template-rows: minmax(0, 0.62fr) min-content minmax(180px, 1fr) minmax(180px, 1fr);
align-content: start;
grid-template-areas: grid-template-areas:
"display functions" "display"
"buttons functions" "display-buttons"
"keypad functions" "keypad"
"keypad trigo"; "functions"
"trigo";
} }
.display-block,
.display-panel, .display-panel,
.display-buttons-panel,
.keypad-panel, .keypad-panel,
.functions-panel, .functions-panel,
.trigo-panel, .trigo-panel {
.status-line { border-radius: 8px;
border-radius: 18px;
border: 1px solid rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(6, 10, 16, 0.16); background: rgba(6, 10, 16, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05); box-shadow: none;
}
.display-buttons-panel {
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(6, 10, 16, 0.16);
box-shadow: none;
}
.display-block {
grid-area: display;
display: grid;
gap: 0;
align-self: start;
justify-items: stretch;
grid-template-rows: auto auto;
row-gap: 0;
} }
.display-panel { .display-panel {
grid-area: display;
position: relative; position: relative;
padding: clamp(12px, 1.5vw, 16px); width: 100%;
background: linear-gradient(180deg, var(--display), var(--display2)); padding: 16px;
padding-bottom: 8px;
background: var(--display);
color: var(--displayText); color: var(--displayText);
font-family: "Courier New", monospace; font-family: "Courier New", monospace;
overflow: hidden; overflow: hidden;
height: clamp(112px, 18vw, 160px); box-sizing: border-box;
max-height: 140px; height: 138px;
align-self: start; min-height: 138px;
max-height: 138px;
margin-bottom: 0; margin-bottom: 0;
min-height: 0;
} }
.display-grid { .display-grid {
height: 100%; height: 100%;
display: grid; display: grid;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: repeat(4, minmax(0, 1fr)); grid-template-rows: repeat(4, minmax(0, auto));
align-content: start;
gap: 2px; gap: 2px;
} }
.stack-cell { .stack-cell {
display: grid; display: grid;
grid-template-columns: 2.2ch 1fr; grid-template-columns: 2.2ch auto minmax(0, 1fr);
align-items: center; align-items: center;
gap: 12px; gap: 8px;
font-size: clamp(18px, 3vw, 30px); font-size: 20px;
line-height: 1; line-height: 1;
min-height: 0; min-height: 0;
padding-block: 0; padding-block: 0;
@@ -127,59 +141,85 @@ body {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
text-align: right;
justify-self: end;
font-size: 20px;
}
.display-buttons-panel {
grid-area: display-buttons;
padding: 8px;
align-self: start;
min-height: 0;
} }
.display-buttons-panel { .display-buttons-grid {
grid-area: buttons;
padding: 8px;
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
grid-template-rows: repeat(2, auto);
gap: 8px; gap: 8px;
align-content: start; align-content: start;
align-items: stretch; align-items: stretch;
grid-auto-flow: row; }
grid-auto-rows: auto;
background: linear-gradient(180deg, #242a33, #1a1f27); .display-button-spacer {
border-color: rgba(255, 255, 255, 0.04); pointer-events: none;
margin-top: 0; visibility: hidden;
background: transparent;
border-color: transparent;
box-shadow: none;
} }
.display-button { .display-button {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 3px 0 rgba(0, 0, 0, 0.34); background: linear-gradient(180deg, var(--btnTop), var(--btnBottom));
background-clip: padding-box; color: #eef2f7;
background: linear-gradient(180deg, #3a414c, #252b34); box-shadow: none;
color: #e8edf3;
border-color: rgba(255, 255, 255, 0.05);
} }
.display-button-offset { .display-button-symbol {
grid-column-start: 2; display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.2em;
font-size: 1.05em;
line-height: 1;
font-weight: 800;
transform: translateY(-0.01em);
} }
.display-buttons-panel > button { .paste-symbol {
width: 100%; font-size: 1em;
} }
.mode-menu { .display-buttons-grid > button {
padding: 6px 8px;
}
.menu-popup {
position: fixed; position: fixed;
z-index: 20; z-index: 20;
display: grid; display: flex;
gap: 6px; flex-direction: column;
padding: 10px; gap: 4px;
border-radius: 14px; padding: 6px;
background: rgba(18, 24, 33, 0.98); border-radius: 6px;
border: 1px solid rgba(255, 255, 255, 0.08); background: rgba(16, 21, 30, 0.96);
box-shadow: 0 14px 30px rgba(0, 0, 0, 0.35); border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.32);
backdrop-filter: blur(2px);
} }
.mode-menu-item { .menu-popup-item {
min-width: 120px; width: 100%;
min-width: 0;
padding: 6px 10px;
border-radius: 5px;
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom)); background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom));
text-align: left;
font-weight: 700;
} }
.mode-menu-item.is-active { .menu-popup-item.is-active {
outline: 2px solid rgba(207, 224, 174, 0.7); outline: 1px solid rgba(207, 224, 174, 0.7);
outline-offset: 0;
} }
.key-escape { .key-escape {
@@ -187,33 +227,12 @@ body {
color: #eef2f7; color: #eef2f7;
} }
.display-button:nth-child(6) { .keypad-panel,
background: linear-gradient(180deg, #343b46, #20262e); .functions-panel,
}
.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 { .trigo-panel {
grid-area: trigo; padding: 8px;
padding: 10px;
align-self: start; align-self: start;
min-height: 0; min-height: 0;
padding-top: 10px;
} }
.status-bar { .status-bar {
@@ -255,35 +274,31 @@ body {
.trigo-grid { .trigo-grid {
display: grid; display: grid;
gap: 8px; gap: 8px;
grid-auto-rows: minmax(0, 1fr);
} }
.keypad-grid { .keypad-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); 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, .functions-grid,
.trigo-grid { .trigo-grid {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
}
.functions-grid,
.trigo-grid {
grid-template-rows: repeat(2, minmax(0, 1fr)); grid-template-rows: repeat(2, minmax(0, 1fr));
} }
button { button {
border: 1px solid rgba(14, 18, 25, 0.85); border: 1px solid rgba(14, 18, 25, 0.85);
border-radius: 12px; border-radius: 8px;
padding: 10px 8px; padding: 8px 8px;
font: inherit; font: inherit;
font-weight: 700; font-weight: 700;
color: var(--btnText); color: var(--btnText);
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35); text-shadow: 0 1px 0 rgba(0, 0, 0, 0.35);
cursor: pointer; cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18), 0 3px 0 rgba(0, 0, 0, 0.28); box-shadow: none;
transition: transform 120ms ease, filter 120ms ease, box-shadow 120ms ease; transition: filter 120ms ease, opacity 120ms ease;
line-height: 1; line-height: 1;
} }
@@ -291,17 +306,74 @@ button:hover {
filter: brightness(1.06); filter: brightness(1.06);
} }
.display-button:hover {
filter: brightness(1.08);
}
button:active { button:active {
transform: translateY(2px); transform: none;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08), 0 1px 0 rgba(0, 0, 0, 0.25); box-shadow: none;
} }
.display-button:active { .stack-copy-button:active {
transform: translateY(2px); animation: stack-copy-flash 140ms ease-out;
}
@keyframes stack-copy-flash {
0% {
opacity: 0.7;
}
50% {
opacity: 1;
}
100% {
opacity: 0.7;
}
}
.stack-copy-button {
padding: 4px;
min-width: 24px;
width: 24px;
height: 24px;
display: inline-grid;
place-items: center;
opacity: 0;
pointer-events: none;
transform: scale(1);
background: transparent;
border: none;
box-shadow: none;
color: rgba(31, 42, 18, 0.58);
margin-left: 0;
}
.stack-cell:last-child {
margin-bottom: 4px;
}
.stack-copy-button svg {
width: 15px;
height: 15px;
fill: #0a0a0a;
display: block;
}
.stack-copy-button.is-visible {
opacity: 0.7;
pointer-events: auto;
transform: scale(1);
}
.stack-copy-button:hover {
opacity: 1;
filter: none;
color: rgba(31, 42, 18, 0.85);
}
.stack-copy-button:active {
transform: scale(1);
color: rgba(31, 42, 18, 0.95);
}
.stack-copy-button:focus-visible {
outline: 1px solid rgba(31, 42, 18, 0.35);
outline-offset: 2px;
} }
.key-default { .key-default {
@@ -309,13 +381,25 @@ button:active {
color: #eef2f7; color: #eef2f7;
} }
.key-accent { .keypad-grid > button {
background: linear-gradient(180deg, var(--btnAccentTop), var(--btnAccentBottom)); min-height: 1.7em;
color: #eef2f7;
} }
.key-alt { #keypadGrid > button {
background: linear-gradient(180deg, var(--btnAltTop), var(--btnAltBottom)); 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; color: #eef2f7;
} }
@@ -327,6 +411,7 @@ button:active {
.key-enter { .key-enter {
background: linear-gradient(180deg, var(--btnEnterTop), var(--btnEnterBottom)); background: linear-gradient(180deg, var(--btnEnterTop), var(--btnEnterBottom));
color: #eef2f7; color: #eef2f7;
padding-inline: 6px;
} }
.hidden-input { .hidden-input {
@@ -338,69 +423,4 @@ button:active {
pointer-events: none; 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;
line-height: 1;
}
.display-buttons-panel {
gap: 6px;
}
}
+15 -10
View File
@@ -9,26 +9,31 @@
<body> <body>
<main class="app-shell"> <main class="app-shell">
<section class="calculator calculator-portrait" aria-label="HP48GX style RPN calculator"> <section class="calculator calculator-portrait" aria-label="HP48GX style RPN calculator">
<div class="display-block">
<div class="display-panel"> <div class="display-panel">
<div class="status-bar" id="statusLine" aria-live="polite"></div> <div class="status-bar" id="statusLine" aria-live="polite"></div>
<div class="display-frame"> <div class="display-frame">
<div class="display-grid"> <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">T:</span><button type="button" class="stack-copy-button" data-copy-stack="T" aria-label="Copy T value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><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">Z:</span><button type="button" class="stack-copy-button" data-copy-stack="Z" aria-label="Copy Z value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><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">Y:</span><button type="button" class="stack-copy-button" data-copy-stack="Y" aria-label="Copy Y value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><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 class="stack-cell"><span class="stack-label">X:</span><button type="button" class="stack-copy-button" data-copy-stack="X" aria-label="Copy X value"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M15.5 4h-7A2.5 2.5 0 0 0 6 6.5V8H5.5A1.5 1.5 0 0 0 4 9.5v8A2.5 2.5 0 0 0 6.5 20H13a1 1 0 0 0 1-1v-1.5H17.5A2.5 2.5 0 0 0 20 15V6.5A2.5 2.5 0 0 0 17.5 4h-2Zm.5 2h1.5a.5.5 0 0 1 .5.5V14a.5.5 0 0 1-.5.5H13V6.5A2.5 2.5 0 0 0 15.5 6Zm-1.5 12H6.5a.5.5 0 0 1-.5-.5v-8a.5.5 0 0 1 .5-.5h7.5V18Zm-1.5-8h-4V8.5A.5.5 0 0 1 9 8h4a.5.5 0 0 1 .5.5V10Z"/></svg></button><span id="stackX" class="stack-value"></span></div>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="display-buttons-panel"> <div class="display-buttons-panel">
<button id="modeButton" class="display-button">Mode</button> <div class="display-buttons-grid">
<button id="pasteButton" class="display-button"></button>
<button id="upButton" class="display-button"></button>
<button id="constButton" class="display-button">π</button> <button id="constButton" class="display-button">π</button>
<button id="leftButton" class="display-button display-button-offset"></button> <button id="pasteButton" class="display-button"><span class="display-button-symbol paste-symbol"></span></button>
<button id="downButton" class="display-button"></button> <button id="upButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
<button id="rightButton" class="display-button"></button> <button id="modeButton" class="display-button">Mode</button>
<div class="display-button-spacer"></div>
<button id="leftButton" class="display-button display-button-offset"><span class="display-button-symbol arrow-symbol"></span></button>
<button id="downButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
<button id="rightButton" class="display-button"><span class="display-button-symbol arrow-symbol"></span></button>
</div>
</div> </div>
<div class="keypad-panel"> <div class="keypad-panel">
+147 -74
View File
@@ -16,6 +16,13 @@ const stackEls = {
X: document.getElementById('stackX'), X: document.getElementById('stackX'),
}; };
const stackCopyButtons = {
T: document.querySelector('[data-copy-stack="T"]'),
Z: document.querySelector('[data-copy-stack="Z"]'),
Y: document.querySelector('[data-copy-stack="Y"]'),
X: document.querySelector('[data-copy-stack="X"]'),
};
const keypadGrid = document.getElementById('keypadGrid'); const keypadGrid = document.getElementById('keypadGrid');
const functionsGrid = document.getElementById('functionsGrid'); const functionsGrid = document.getElementById('functionsGrid');
const trigoGrid = document.getElementById('trigoGrid'); const trigoGrid = document.getElementById('trigoGrid');
@@ -23,22 +30,22 @@ const calculatorEl = document.querySelector('.calculator');
const statusLine = document.getElementById('statusLine'); const statusLine = document.getElementById('statusLine');
const keypadKeys = [ const keypadKeys = [
{ label: 'Enter', action: 'enter', className: 'key-enter' }, { label: 'ENTER', action: 'enter', className: 'key-enter' },
{ label: '⎋', action: 'escape', className: 'key-escape' }, { label: '⎋', action: 'escape', className: 'key-escape' },
{ label: 'C', action: 'clear', className: 'key-danger' }, { label: 'C', action: 'clear', className: 'key-danger' },
{ label: '⌫', action: 'backspace', className: 'key-danger' }, { label: '⌫', action: 'backspace', className: 'key-danger' },
{ label: '7', input: '7', className: 'key-default' }, { label: '7', input: '7', className: 'key-default' },
{ label: '8', input: '8', className: 'key-default' }, { label: '8', input: '8', className: 'key-default' },
{ label: '9', input: '9', className: 'key-default' }, { label: '9', input: '9', className: 'key-default' },
{ label: '/', action: 'div', className: 'key-accent' }, { label: '÷', action: 'div', className: 'key-accent' },
{ label: '4', input: '4', className: 'key-default' }, { label: '4', input: '4', className: 'key-default' },
{ label: '5', input: '5', className: 'key-default' }, { label: '5', input: '5', className: 'key-default' },
{ label: '6', input: '6', className: 'key-default' }, { label: '6', input: '6', className: 'key-default' },
{ label: '*', action: 'mul', className: 'key-accent' }, { label: '×', action: 'mul', className: 'key-accent' },
{ label: '1', input: '1', className: 'key-default' }, { label: '1', input: '1', className: 'key-default' },
{ label: '2', input: '2', className: 'key-default' }, { label: '2', input: '2', className: 'key-default' },
{ label: '3', input: '3', className: 'key-default' }, { label: '3', input: '3', className: 'key-default' },
{ label: '-', action: 'sub', className: 'key-accent' }, { label: '', action: 'sub', className: 'key-accent' },
{ label: '0', input: '0', className: 'key-default' }, { label: '0', input: '0', className: 'key-default' },
{ label: '.', input: '.', className: 'key-default' }, { label: '.', input: '.', className: 'key-default' },
{ label: '±', action: 'neg', className: 'key-default' }, { label: '±', action: 'neg', className: 'key-default' },
@@ -51,7 +58,7 @@ const functionKeys = [
{ label: '1/x', action: 'recip', className: 'key-default' }, { label: '1/x', action: 'recip', className: 'key-default' },
{ label: '%', action: 'mod', className: 'key-default' }, { label: '%', action: 'mod', className: 'key-default' },
{ label: '√x', action: 'sqrt', className: 'key-default' }, { label: '√x', action: 'sqrt', className: 'key-default' },
{ label: 'y√x', action: 'pow', className: 'key-default' }, { label: 'y√x', action: 'root', className: 'key-default' },
{ label: '10ˣ', action: 'pow10', className: 'key-default' }, { label: '10ˣ', action: 'pow10', className: 'key-default' },
{ label: '', spacer: true }, { label: '', spacer: true },
{ label: 'log', action: 'log', className: 'key-default' }, { label: 'log', action: 'log', className: 'key-default' },
@@ -71,7 +78,6 @@ const trigoKeys = [
{ label: '', spacer: true }, { label: '', spacer: true },
]; ];
const isTouchDevice = window.matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window; const isTouchDevice = window.matchMedia('(pointer: coarse)').matches || 'ontouchstart' in window;
function focusInput() { function focusInput() {
@@ -103,6 +109,15 @@ function setStatus(message, isError = false, timeoutMs = 1400) {
}, timeoutMs); }, timeoutMs);
} }
function clearStatus() {
clearTimeout(statusTimer);
statusTimer = null;
if (!statusLine) return;
statusLine.textContent = '';
statusLine.classList.remove('is-visible');
statusLine.classList.remove('is-error');
}
function normalizeStack() { function normalizeStack() {
while (calc.stack.length > 4) { while (calc.stack.length > 4) {
calc.stack.shift(); calc.stack.shift();
@@ -113,16 +128,40 @@ function getStackLine(indexFromTop) {
return indexFromTop >= 0 && indexFromTop < calc.stack.length ? calc.stack[indexFromTop] : ''; return indexFromTop >= 0 && indexFromTop < calc.stack.length ? calc.stack[indexFromTop] : '';
} }
function getStackDisplayValue(label) {
if (label === 'X') {
return calc.isEditing ? calc.inputValue : (calc.formatNumber(getStackLine(0)) || '');
}
if (label === 'Y') {
return calc.isEditing ? (calc.formatNumber(getStackLine(0)) || '') : (calc.formatNumber(getStackLine(1)) || '');
}
if (label === 'Z') {
return calc.isEditing ? (calc.formatNumber(getStackLine(1)) || '') : (calc.formatNumber(getStackLine(2)) || '');
}
return calc.isEditing ? (calc.formatNumber(getStackLine(2)) || '') : (calc.formatNumber(getStackLine(3)) || '');
}
function updateCopyButtons() {
for (const label of ['T', 'Z', 'Y', 'X']) {
const value = getStackDisplayValue(label);
const button = stackCopyButtons[label];
if (!button) continue;
button.classList.toggle('is-visible', Boolean(value));
button.disabled = !value;
button.setAttribute('aria-hidden', value ? 'false' : 'true');
}
}
function render() { function render() {
normalizeStack(); normalizeStack();
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);
const editingValue = calc.isEditing ? calc.inputValue : ''; stackEls.X.textContent = getStackDisplayValue('X');
stackEls.X.textContent = calc.isEditing ? editingValue : (calc.formatNumber(getStackLine(0)) || ''); stackEls.Y.textContent = getStackDisplayValue('Y');
stackEls.Y.textContent = calc.isEditing ? (calc.formatNumber(getStackLine(0)) || '') : (calc.formatNumber(getStackLine(1)) || ''); stackEls.Z.textContent = getStackDisplayValue('Z');
stackEls.Z.textContent = calc.isEditing ? (calc.formatNumber(getStackLine(1)) || '') : (calc.formatNumber(getStackLine(2)) || ''); stackEls.T.textContent = getStackDisplayValue('T');
stackEls.T.textContent = calc.isEditing ? (calc.formatNumber(getStackLine(2)) || '') : (calc.formatNumber(getStackLine(3)) || ''); updateCopyButtons();
modeButton.textContent = calc.angleMode; modeButton.textContent = calc.angleMode;
} }
@@ -189,7 +228,6 @@ function execute(name) {
} else if (calc.isValidIndex(0)) { } else if (calc.isValidIndex(0)) {
calc.remove(0); calc.remove(0);
} }
} else if (name === 'swap') { } else if (name === 'swap') {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
if (calc.isValidIndex(1)) { if (calc.isValidIndex(1)) {
@@ -203,11 +241,17 @@ function execute(name) {
} }
} else if (name === 'pow10') { } else if (name === 'pow10') {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
const exponent = calc.stack[0];
calc.remove(0);
calc.push(10); calc.push(10);
calc.push(exponent);
calc.command('pow'); calc.command('pow');
} else if (name === 'exp') { } else if (name === 'exp') {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
const exponent = calc.stack[0];
calc.remove(0);
calc.push(Math.E); calc.push(Math.E);
calc.push(exponent);
calc.command('pow'); calc.command('pow');
} else { } else {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
@@ -244,6 +288,17 @@ function buildGrid(container, keys) {
keys.forEach((key) => container.appendChild(createKeyButton(key))); keys.forEach((key) => container.appendChild(createKeyButton(key)));
} }
async function copyStackValue(label) {
const value = getStackDisplayValue(label);
if (!value) return;
try {
await navigator.clipboard.writeText(value);
clearStatus();
} catch (error) {
setStatus('Copy unavailable', true);
}
}
function handleKeyboard(event) { function handleKeyboard(event) {
if (event.defaultPrevented) return; if (event.defaultPrevented) return;
const key = event.key; const key = event.key;
@@ -265,6 +320,17 @@ function handleKeyboard(event) {
'%': 'mod', '%': 'mod',
'^': 'pow', '^': 'pow',
}; };
const arrowMap = {
ArrowUp: upButton,
ArrowDown: downButton,
ArrowLeft: leftButton,
ArrowRight: rightButton,
};
if (arrowMap[key]) {
event.preventDefault();
arrowMap[key].click();
return;
}
if (map[key]) { if (map[key]) {
event.preventDefault(); event.preventDefault();
execute(map[key]); execute(map[key]);
@@ -272,48 +338,60 @@ function handleKeyboard(event) {
} }
const modeOptions = ['deg', 'rad', 'grad']; const modeOptions = ['deg', 'rad', 'grad'];
let modeMenuEl = null; let activeMenuEl = null;
function closeModeMenu() { function closeModeMenu() {
if (modeMenuEl) { if (activeMenuEl) {
modeMenuEl.remove(); activeMenuEl.remove();
modeMenuEl = null; activeMenuEl = null;
} }
} }
function openModeMenu() { function openMenu(anchorButton, items, onSelect) {
closeModeMenu(); const rect = anchorButton.getBoundingClientRect();
const rect = modeButton.getBoundingClientRect(); const menu = document.createElement('div');
modeMenuEl = document.createElement('div'); menu.className = 'menu-popup';
modeMenuEl.className = 'mode-menu'; menu.style.top = `${rect.bottom + 6 + window.scrollY}px`;
modeMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`; menu.style.left = `${rect.left + window.scrollX}px`;
modeMenuEl.style.left = `${rect.left + window.scrollX}px`; menu.style.minWidth = `${rect.width}px`;
modeOptions.forEach((mode) => { for (const item of items) {
const button = document.createElement('button'); const button = document.createElement('button');
button.type = 'button'; button.type = 'button';
button.className = `mode-menu-item${mode === calc.angleMode ? ' is-active' : ''}`; button.className = `menu-popup-item${item.active ? ' is-active' : ''}`;
button.textContent = mode; button.textContent = item.label;
button.addEventListener('click', () => { button.addEventListener('click', () => onSelect(item.value));
menu.appendChild(button);
}
document.body.appendChild(menu);
return menu;
}
function toggleModeMenu() {
if (activeMenuEl) {
closeModeMenu();
return;
}
closeConstMenu();
activeMenuEl = openMenu(modeButton, modeOptions.map((mode) => ({
label: mode,
value: mode,
active: mode === calc.angleMode,
})), (mode) => {
calc.angleMode = mode; calc.angleMode = mode;
render(); render();
closeModeMenu(); 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) => { modeButton.addEventListener('click', (event) => {
event.stopPropagation(); event.stopPropagation();
openModeMenu(); toggleModeMenu();
}); });
window.addEventListener('resize', () => { window.addEventListener('resize', () => {
closeModeMenu(); closeModeMenu();
closeConstMenu(); closeConstMenu();
render();
}); });
window.addEventListener('scroll', () => { window.addEventListener('scroll', () => {
closeModeMenu(); closeModeMenu();
@@ -321,10 +399,14 @@ window.addEventListener('scroll', () => {
}, true); }, true);
window.addEventListener('click', (event) => { window.addEventListener('click', (event) => {
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) { const stackCopyButton = event.target.closest('.stack-copy-button');
closeModeMenu(); if (stackCopyButton) {
const label = stackCopyButton.dataset.copyStack;
if (label) copyStackValue(label);
return;
} }
if (constMenuEl && !event.target.closest('.mode-menu') && event.target !== constButton) { if (activeMenuEl && !event.target.closest('.menu-popup') && event.target !== modeButton && event.target !== constButton) {
closeModeMenu();
closeConstMenu(); closeConstMenu();
} }
}); });
@@ -333,6 +415,7 @@ pasteButton.addEventListener('click', async () => {
try { try {
const text = await navigator.clipboard.readText(); const text = await navigator.clipboard.readText();
pasteTextIntoStack(text); pasteTextIntoStack(text);
clearStatus();
} catch (error) { } catch (error) {
setStatus('Paste unavailable', true); setStatus('Paste unavailable', true);
} }
@@ -346,58 +429,51 @@ hiddenInput.addEventListener('paste', (event) => {
upButton.addEventListener('click', () => {}); upButton.addEventListener('click', () => {});
const constants = [ const constantLabels = {
{ label: 'π', value: Math.PI }, pi: 'π',
{ label: 'e', value: Math.E }, e: 'e',
{ label: 'φ', value: (1 + Math.sqrt(5)) / 2 }, phi: 'φ',
{ label: 'g', value: 9.80665 }, g: 'g',
{ label: 'c', value: 299792458 }, c: 'c',
]; };
const constantOrder = ['pi', 'e', 'phi', 'g', 'c'];
let constMenuEl = null;
function closeConstMenu() { function closeConstMenu() {
if (constMenuEl) { if (activeMenuEl) {
constMenuEl.remove(); activeMenuEl.remove();
constMenuEl = null; activeMenuEl = null;
} }
} }
function openConstMenu() { function toggleConstMenu() {
if (activeMenuEl) {
closeConstMenu(); closeConstMenu();
const rect = constButton.getBoundingClientRect(); return;
constMenuEl = document.createElement('div'); }
constMenuEl.className = 'mode-menu'; closeModeMenu();
constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`; const availableConstants = calc.listConstants();
constMenuEl.style.left = `${rect.left + window.scrollX}px`; const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))]
constants.forEach((constant) => { .filter((name) => Object.prototype.hasOwnProperty.call(availableConstants, name));
const button = document.createElement('button'); activeMenuEl = openMenu(constButton, keys.map((name) => ({
button.type = 'button'; label: constantLabels[name] ?? name,
button.className = 'mode-menu-item'; value: name,
button.textContent = constant.label; })), (name) => {
button.addEventListener('click', () => {
pushEditingValueIfNeeded(); pushEditingValueIfNeeded();
calc.push(constant.value); calc.push(availableConstants[name]);
render(); render();
setStatus(`Inserted ${constant.label}`); clearStatus();
closeConstMenu(); closeConstMenu();
focusInput(); focusInput();
}); });
constMenuEl.appendChild(button);
});
document.body.appendChild(constMenuEl);
const menuRect = constMenuEl.getBoundingClientRect();
const maxLeft = Math.max(8, window.innerWidth - menuRect.width - 8);
constMenuEl.style.left = `${Math.max(8, Math.min(maxLeft, rect.left + window.scrollX))}px`;
} }
constButton.addEventListener('click', (event) => { constButton.addEventListener('click', (event) => {
event.stopPropagation(); event.stopPropagation();
openConstMenu(); toggleConstMenu();
}); });
leftButton.addEventListener('click', () => {}); leftButton.addEventListener('click', () => {});
downButton.addEventListener('click', () => { downButton.addEventListener('click', () => {
if (!calc.isEditing && calc.isValidIndex(0)) { if (!calc.isEditing && calc.isValidIndex(0)) {
const value = calc.stack[0]; const value = calc.stack[0];
@@ -409,19 +485,16 @@ downButton.addEventListener('click', () => {
} }
}); });
rightButton.addEventListener('click', () => { rightButton.addEventListener('click', () => {
execute('swap'); execute('swap');
}); });
window.addEventListener('keydown', handleKeyboard, { capture: true }); window.addEventListener('keydown', handleKeyboard, { capture: true });
window.addEventListener('load', focusInput); window.addEventListener('load', focusInput);
window.addEventListener('pageshow', focusInput); window.addEventListener('pageshow', focusInput);
window.addEventListener('focus', focusInput); window.addEventListener('focus', focusInput);
window.addEventListener('pointerdown', focusInput, true); window.addEventListener('pointerdown', focusInput, true);
window.addEventListener('mousedown', focusInput, true); window.addEventListener('mousedown', focusInput, true);
window.addEventListener('click', focusInput, true);
hiddenInput.setAttribute('inputmode', 'none'); hiddenInput.setAttribute('inputmode', 'none');
hiddenInput.setAttribute('readonly', 'readonly'); hiddenInput.setAttribute('readonly', 'readonly');
@@ -436,7 +509,7 @@ hiddenInput.addEventListener('focus', () => {
}); });
document.addEventListener('click', (event) => { document.addEventListener('click', (event) => {
if (!isTouchDevice && !event.target.closest('.mode-menu')) { if (!isTouchDevice && !event.target.closest('.menu-popup')) {
focusInput(); focusInput();
} }
}); });
-10
View File
@@ -1,10 +0,0 @@
┌──────────── 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 | └──────────────────────┘
└─────────────────────────────────┘
@@ -1,13 +1,28 @@
# Calc 02 Visual Portrait
## Display
```
┌──────────── Display ────────────┐ ┌──────────── Display ────────────┐
| T: | | T: |
| Z: | | Z: |
| Y: | | Y: |
| X: | | X: |
└─────────────────────────────────┘ └─────────────────────────────────┘
```
## Display Buttons
```
┌──────── Display Buttons ────────┐ ┌──────── Display Buttons ────────┐
| Mode | Paste | Up | Const | | Const | Paste | Up | Mode |
| | Right | Down | Right | | | Left | Down | Right |
└─────────────────────────────────┘ └─────────────────────────────────┘
```
## Keypad
```
┌──────────── Keypad ─────────────┐ ┌──────────── Keypad ─────────────┐
| +/- | Clear | Esc | backspace | | +/- | Clear | Esc | backspace |
| 7 | 8 | 9 | / | | 7 | 8 | 9 | / |
@@ -15,12 +30,23 @@
| 1 | 2 | 3 | - | | 1 | 2 | 3 | - |
| 0 | . | Enter | + | | 0 | . | Enter | + |
└─────────────────────────────────┘ └─────────────────────────────────┘
┌─────────── Functions ───────────┐ ```
## Functions
```
┌──────────── Functions ──────────┐
| x^2 | y^x | 1/x | % | | x^2 | y^x | 1/x | % |
| √x | y√x | 10^x | | | √x | y√x | 10^x | |
| log | ln | | | | log | ln | | |
└─────────────────────────────────┘ └─────────────────────────────────┘
┌───────────── Trigo ─────────────┐ ```
## Trigo
```
┌──────────── Trigo ──────────────┐
| sin | cos | tan | | | sin | cos | tan | |
| asin | acos | atan | | | asin | acos | atan | |
└─────────────────────────────────┘ └─────────────────────────────────┘
```
+70 -1
View File
@@ -2,6 +2,9 @@ class RpnCalculator {
static CONSTANTS = { static CONSTANTS = {
pi: Math.PI, pi: Math.PI,
e: Math.E, e: Math.E,
phi: (1 + Math.sqrt(5)) / 2,
g: 9.80665,
c: 299792458,
}; };
static OPERATIONS = { static OPERATIONS = {
@@ -46,6 +49,20 @@ class RpnCalculator {
aliases: ['^', 'y^x'], aliases: ['^', 'y^x'],
execute: (calc, a, b) => Math.pow(a, b), execute: (calc, a, b) => Math.pow(a, b),
}, },
root: {
category: 'Arithmetic',
argCount: 2,
aliases: ['y√x', 'yroot', 'nroot'],
execute: (calc, a, b) => {
if (b === 0) {
throw new Error('Invalid input for root');
}
if (a < 0 && b % 2 === 0) {
throw new Error('Invalid input for root');
}
return Math.pow(a, 1 / b);
},
},
sqr: { sqr: {
category: 'Arithmetic', category: 'Arithmetic',
argCount: 1, argCount: 1,
@@ -178,7 +195,7 @@ class RpnCalculator {
static getOperationsByCategory() { static getOperationsByCategory() {
return { return {
Stack: ['dup', 'drop', 'swap', 'clear', 'enter'], Stack: ['dup', 'drop', 'swap', 'clear', 'enter'],
Arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'], Arithmetic: ['add', 'sub', 'mul', 'div', 'mod', 'pow', 'root', 'sqr', 'neg', 'sqrt', 'recip', 'log', 'ln'],
Trigonometry: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'], Trigonometry: ['sin', 'cos', 'tan', 'asin', 'acos', 'atan'],
}; };
} }
@@ -198,6 +215,15 @@ class RpnCalculator {
this.enabledCommands = new Set(selectedCommands.map((name) => this.normalizeCommandName(name)).filter((name) => RpnCalculator.OPERATIONS[name])); 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) { toRadians(value) {
if (this.angleMode === 'grad') { if (this.angleMode === 'grad') {
return (value * Math.PI) / 200; return (value * Math.PI) / 200;
@@ -243,6 +269,45 @@ class RpnCalculator {
return typeof name === 'string' && Object.prototype.hasOwnProperty.call(this.constants, name.toLowerCase()); 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) { push(value) {
if (this.stack.length >= this.maxSize) { if (this.stack.length >= this.maxSize) {
throw new Error('Stack overflow'); throw new Error('Stack overflow');
@@ -394,6 +459,10 @@ class RpnCalculator {
getConstants() { getConstants() {
return { ...this.constants }; return { ...this.constants };
} }
listConstants() {
return this.getConstants();
}
} }
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {