diff --git a/tests/js/panel-view.test.js b/tests/js/panel-view.test.js new file mode 100644 index 0000000..6379fa2 --- /dev/null +++ b/tests/js/panel-view.test.js @@ -0,0 +1,56 @@ +// Details panel helpers: bar fill widths as the GDI panel computed them and +// the browser shortcuts the panel suppresses. + +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; + +import { isBrowserAccelerator, panelFillWidth } from '../../ui/panel-view.js'; + +/** + * A key event with no modifiers unless given. + * @param {string} key Key value. + * @param {{ctrlKey?: boolean, metaKey?: boolean}} [mods] Modifiers. + * @returns {import('../../ui/panel-view.js').KeyLike} Key event stand-in. + */ +const key = (key, mods = {}) => ({ key, ctrlKey: false, metaKey: false, ...mods }); + +describe('panelFillWidth', () => { + it('rounds the clamped percent of the 120 px bar', () => { + assert.equal(panelFillWidth(0), 0); + assert.equal(panelFillWidth(50), 60); + assert.equal(panelFillWidth(100), 120); + assert.equal(panelFillWidth(72.5), 87); + assert.equal(panelFillWidth(0.4), 0); + assert.equal(panelFillWidth(0.5), 1); + }); + + it('clamps out-of-range and non-finite values', () => { + assert.equal(panelFillWidth(-5), 0); + assert.equal(panelFillWidth(150), 120); + assert.equal(panelFillWidth(Number.NaN), 0); + assert.equal(panelFillWidth(Number.POSITIVE_INFINITY), 0); + }); +}); + +describe('isBrowserAccelerator', () => { + it('catches the function keys on their own', () => { + for (const k of ['F5', 'F7', 'F12']) { + assert.equal(isBrowserAccelerator(key(k)), true, k); + } + assert.equal(isBrowserAccelerator(key('F1')), false); + }); + + it('catches Ctrl or Cmd with a browser command key, in either case', () => { + for (const k of ['r', 'R', 'p', 'f', 'g', 's', 'u', 'o', '+', '=', '-', '0']) { + assert.equal(isBrowserAccelerator(key(k, { ctrlKey: true })), true, `Ctrl+${k}`); + assert.equal(isBrowserAccelerator(key(k, { metaKey: true })), true, `Cmd+${k}`); + } + }); + + it('lets other keys through', () => { + assert.equal(isBrowserAccelerator(key('r')), false); + assert.equal(isBrowserAccelerator(key('a', { ctrlKey: true })), false); + assert.equal(isBrowserAccelerator(key('c', { ctrlKey: true })), false); + assert.equal(isBrowserAccelerator(key('Escape')), false); + }); +}); diff --git a/ui/panel-view.js b/ui/panel-view.js new file mode 100644 index 0000000..a4dba72 --- /dev/null +++ b/ui/panel-view.js @@ -0,0 +1,44 @@ +// Pure helpers for the details panel page, kept free of `window.__TAURI__` +// so they run under `node --test`. + +/** Width of a panel usage bar in CSS px. */ +export const PANEL_BAR_WIDTH = 120; + +/** + * Width of a panel bar fill: the clamped percent of the bar, rounded to + * whole px, as the GDI panel drew it. The bar is square, so 1% stays 1 px. + * @param {number} pct Usage percent; anything not finite counts as 0. + * @returns {number} Fill width in CSS px, 0..120. + */ +export function panelFillWidth(pct) { + const value = Number.isFinite(pct) ? Math.min(Math.max(pct, 0), 100) : 0; + return Math.round((value / 100) * PANEL_BAR_WIDTH); +} + +/** Keys that act as browser commands with Ctrl (Cmd on macOS) held. */ +const CTRL_ACCELERATORS = new Set(['r', 'p', 'f', 'g', 's', 'u', 'o', '+', '=', '-', '_', '0']); + +/** Keys that act as browser commands on their own. */ +const PLAIN_ACCELERATORS = new Set(['F5', 'F7', 'F12']); + +/** + * The parts of a `KeyboardEvent` the accelerator check reads. + * @typedef {object} KeyLike + * @property {string} key Key value. + * @property {boolean} ctrlKey Ctrl held. + * @property {boolean} metaKey Cmd (Meta) held. + */ + +/** + * Whether a key press would run a browser command (reload, print, find, + * save, view source, open, zoom, caret browsing, dev tools). The panel + * ignores all input, so these are suppressed. + * @param {KeyLike} event Key event. + * @returns {boolean} True for a browser accelerator. + */ +export function isBrowserAccelerator(event) { + if (PLAIN_ACCELERATORS.has(event.key)) { + return true; + } + return (event.ctrlKey || event.metaKey) && CTRL_ACCELERATORS.has(event.key.toLowerCase()); +} diff --git a/ui/panel.css b/ui/panel.css new file mode 100644 index 0000000..0cedde0 --- /dev/null +++ b/ui/panel.css @@ -0,0 +1,100 @@ +/* + * Details panel, 280x120 logical px, laid out on the GDI panel's exact + * coordinates. The webview zoom scales it to the monitor DPI. Accent and + * fill colours come from the payload. No transitions, no pulse. + */ +:root[data-theme='dark'] { + --bg: #1f1f1f; + --text: #eaeaea; + --track: #3a3a3a; +} + +:root[data-theme='light'] { + --bg: #fafafa; + --text: #1f1f1f; + --track: #d6d6d6; +} + +html, +body { + width: 100%; + height: 100%; + margin: 0; + overflow: hidden; + background: var(--bg); + color: var(--text); + font-family: 'Segoe UI', system-ui, sans-serif; + cursor: default; + user-select: none; + -webkit-user-select: none; +} + +.stripe { + position: absolute; + left: 0; + top: 0; + width: 4px; + height: 100%; +} + +/* One line, vertically centred, clipped at the box edge without an ellipsis. */ +.text { + position: absolute; + height: 18px; + overflow: hidden; + font-size: 11px; + font-weight: 400; + line-height: 18px; + white-space: pre; + text-overflow: clip; +} + +.header { + left: 14px; + top: 14px; + width: 252px; + font-size: 13px; + font-weight: 600; +} + +.row { + position: absolute; + left: 0; + width: 100%; + height: 14px; +} + +.row.session { + top: 38px; +} + +.row.weekly { + top: 68px; +} + +.row-label { + left: 14px; + top: -2px; + width: 28px; +} + +.bar { + position: absolute; + left: 46px; + top: 0; + width: 120px; + height: 14px; + overflow: hidden; + background: var(--track); +} + +.fill { + width: 0; + height: 100%; +} + +.row-text { + left: 174px; + top: -2px; + width: 96px; +} diff --git a/ui/panel.html b/ui/panel.html new file mode 100644 index 0000000..7ecdad5 --- /dev/null +++ b/ui/panel.html @@ -0,0 +1,23 @@ + + +
+ +