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.
This commit is contained in:
2026-05-16 04:04:01 +02:00
parent e5f50aee0a
commit 003d4fde1b
4 changed files with 69 additions and 91 deletions
+3 -21
View File
@@ -1,5 +1,4 @@
:root {
--bg0: #10151e;
--bg1: #1b2432;
@@ -42,7 +41,6 @@ body {
background: var(--bg0);
}
.app-shell {
min-height: 100vh;
display: grid;
@@ -113,9 +111,6 @@ body {
max-height: 138px;
margin-bottom: 0;
}
.display-grid {
height: 100%;
display: grid;
@@ -125,7 +120,6 @@ body {
gap: 2px;
}
.stack-cell {
display: grid;
grid-template-columns: 2.2ch auto minmax(0, 1fr);
@@ -137,7 +131,6 @@ body {
padding-block: 0;
}
.stack-label {
text-align: right;
opacity: 0.78;
@@ -152,8 +145,6 @@ body {
justify-self: end;
font-size: 20px;
}
.display-buttons-panel {
grid-area: display-buttons;
padding: 8px;
@@ -183,7 +174,6 @@ body {
box-shadow: none;
}
.display-button-symbol {
display: inline-flex;
align-items: center;
@@ -203,8 +193,7 @@ body {
padding: 6px 8px;
}
.mode-menu {
.menu-popup {
position: fixed;
z-index: 20;
display: flex;
@@ -218,7 +207,7 @@ body {
backdrop-filter: blur(2px);
}
.mode-menu-item {
.menu-popup-item {
width: 100%;
min-width: 0;
padding: 6px 10px;
@@ -228,7 +217,7 @@ body {
font-weight: 700;
}
.mode-menu-item.is-active {
.menu-popup-item.is-active {
outline: 1px solid rgba(207, 224, 174, 0.7);
outline-offset: 0;
}
@@ -295,10 +284,6 @@ body {
.functions-grid,
.trigo-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.functions-grid,
.trigo-grid {
grid-template-rows: repeat(2, minmax(0, 1fr));
}
@@ -321,7 +306,6 @@ button:hover {
filter: brightness(1.06);
}
button:active {
transform: none;
box-shadow: none;
@@ -342,8 +326,6 @@ button:active {
opacity: 0.7;
}
}
.stack-copy-button {
padding: 4px;
min-width: 24px;
+50 -59
View File
@@ -338,40 +338,49 @@ function handleKeyboard(event) {
}
const modeOptions = ['deg', 'rad', 'grad'];
let modeMenuEl = null;
let activeMenuEl = null;
function closeModeMenu() {
if (modeMenuEl) {
modeMenuEl.remove();
modeMenuEl = null;
if (activeMenuEl) {
activeMenuEl.remove();
activeMenuEl = null;
}
}
function openMenu(anchorButton, items, onSelect) {
const rect = anchorButton.getBoundingClientRect();
const menu = document.createElement('div');
menu.className = 'menu-popup';
menu.style.top = `${rect.bottom + 6 + window.scrollY}px`;
menu.style.left = `${rect.left + window.scrollX}px`;
menu.style.minWidth = `${rect.width}px`;
for (const item of items) {
const button = document.createElement('button');
button.type = 'button';
button.className = `menu-popup-item${item.active ? ' is-active' : ''}`;
button.textContent = item.label;
button.addEventListener('click', () => onSelect(item.value));
menu.appendChild(button);
}
document.body.appendChild(menu);
return menu;
}
function toggleModeMenu() {
if (modeMenuEl) {
if (activeMenuEl) {
closeModeMenu();
return;
}
closeConstMenu();
const rect = modeButton.getBoundingClientRect();
modeMenuEl = document.createElement('div');
modeMenuEl.className = 'mode-menu';
modeMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
modeMenuEl.style.minWidth = `${rect.width}px`;
modeOptions.forEach((mode) => {
const button = document.createElement('button');
button.type = 'button';
button.className = `mode-menu-item${mode === calc.angleMode ? ' is-active' : ''}`;
button.textContent = mode;
button.addEventListener('click', () => {
calc.angleMode = mode;
render();
closeModeMenu();
});
modeMenuEl.appendChild(button);
activeMenuEl = openMenu(modeButton, modeOptions.map((mode) => ({
label: mode,
value: mode,
active: mode === calc.angleMode,
})), (mode) => {
calc.angleMode = mode;
render();
closeModeMenu();
});
document.body.appendChild(modeMenuEl);
modeMenuEl.style.left = `${rect.left + window.scrollX}px`;
}
modeButton.addEventListener('click', (event) => {
@@ -396,10 +405,8 @@ window.addEventListener('click', (event) => {
if (label) copyStackValue(label);
return;
}
if (modeMenuEl && !event.target.closest('.mode-menu') && event.target !== modeButton) {
if (activeMenuEl && !event.target.closest('.menu-popup') && event.target !== modeButton && event.target !== constButton) {
closeModeMenu();
}
if (constMenuEl && !event.target.closest('.mode-menu') && event.target !== constButton) {
closeConstMenu();
}
});
@@ -431,48 +438,34 @@ const constantLabels = {
};
const constantOrder = ['pi', 'e', 'phi', 'g', 'c'];
let constMenuEl = null;
function closeConstMenu() {
if (constMenuEl) {
constMenuEl.remove();
constMenuEl = null;
if (activeMenuEl) {
activeMenuEl.remove();
activeMenuEl = null;
}
}
function toggleConstMenu() {
if (constMenuEl) {
if (activeMenuEl) {
closeConstMenu();
return;
}
closeModeMenu();
const rect = constButton.getBoundingClientRect();
constMenuEl = document.createElement('div');
constMenuEl.className = 'mode-menu';
constMenuEl.style.top = `${rect.bottom + 6 + window.scrollY}px`;
constMenuEl.style.minWidth = `${rect.width}px`;
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';
if (!Object.prototype.hasOwnProperty.call(availableConstants, name)) {
return;
}
button.textContent = constantLabels[name] ?? name;
button.addEventListener('click', () => {
pushEditingValueIfNeeded();
calc.push(availableConstants[name]);
render();
clearStatus();
closeConstMenu();
focusInput();
});
constMenuEl.appendChild(button);
const keys = [...constantOrder, ...Object.keys(availableConstants).filter((name) => !constantOrder.includes(name))]
.filter((name) => Object.prototype.hasOwnProperty.call(availableConstants, name));
activeMenuEl = openMenu(constButton, keys.map((name) => ({
label: constantLabels[name] ?? name,
value: name,
})), (name) => {
pushEditingValueIfNeeded();
calc.push(availableConstants[name]);
render();
clearStatus();
closeConstMenu();
focusInput();
});
document.body.appendChild(constMenuEl);
constMenuEl.style.left = `${rect.left + window.scrollX}px`;
}
constButton.addEventListener('click', (event) => {
@@ -481,7 +474,6 @@ constButton.addEventListener('click', (event) => {
});
leftButton.addEventListener('click', () => {});
downButton.addEventListener('click', () => {
if (!calc.isEditing && calc.isValidIndex(0)) {
const value = calc.stack[0];
@@ -503,7 +495,6 @@ window.addEventListener('pageshow', focusInput);
window.addEventListener('focus', focusInput);
window.addEventListener('pointerdown', focusInput, true);
window.addEventListener('mousedown', focusInput, true);
window.addEventListener('click', focusInput, true);
hiddenInput.setAttribute('inputmode', 'none');
hiddenInput.setAttribute('readonly', 'readonly');
@@ -518,7 +509,7 @@ hiddenInput.addEventListener('focus', () => {
});
document.addEventListener('click', (event) => {
if (!isTouchDevice && !event.target.closest('.mode-menu')) {
if (!isTouchDevice && !event.target.closest('.menu-popup')) {
focusInput();
}
});