← back to Desktop Dotbar
dotbar: horizontal contract/expand for the top bar (right-end length grip)
d88f4e1d031aa4835fe8bec492b11d9444ba91c1 · 2026-09-17 11:01:42 -0700 · Steve Abrams
Top bar was locked to full screen width. Add a left-anchored barLen config +
setBarLen IPC (main+preload) and a top-only right-end #gripx (ew-resize) that
drags the bar's horizontal span; snaps back to full at the screen edge, clamped
per display, persisted in .barcfg. Side docks unchanged (main #grip owns width).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mx9jXp5vC4sft858UfivPN
Files touched
M electron-main.jsM package-lock.jsonM package.jsonM preload.jsM public/index.html
Diff
commit d88f4e1d031aa4835fe8bec492b11d9444ba91c1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 17 11:01:42 2026 -0700
dotbar: horizontal contract/expand for the top bar (right-end length grip)
Top bar was locked to full screen width. Add a left-anchored barLen config +
setBarLen IPC (main+preload) and a top-only right-end #gripx (ew-resize) that
drags the bar's horizontal span; snaps back to full at the screen edge, clamped
per display, persisted in .barcfg. Side docks unchanged (main #grip owns width).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mx9jXp5vC4sft858UfivPN
---
electron-main.js | 26 +++++++++++++++++++++++---
package-lock.json | 4 ++--
package.json | 2 +-
preload.js | 3 ++-
public/index.html | 44 ++++++++++++++++++++++++++++++++++++++++----
5 files changed, 68 insertions(+), 11 deletions(-)
diff --git a/electron-main.js b/electron-main.js
index 2f2d9e0..6a65cf0 100644
--- a/electron-main.js
+++ b/electron-main.js
@@ -24,6 +24,7 @@ const PANEL_H = 360; // top-dock dropdown grows
const PANEL_W = 340; // side-dock dropdown grows sideways by this
const BAR_DEFAULT = 48, BAR_MIN = 22, BAR_MAX = 140; // thickness of a TOP strip (its height)
const BARW_DEFAULT = 210, BARW_MIN = 120, BARW_MAX = 420; // thickness of a SIDE strip (its width)
+const BARLEN_MIN = 260, BARLEN_MAX = 8000; // TOP strip horizontal LENGTH; 0 = span the full screen (Steve 2026-09-17)
const PEEK = 3; // px of bar left visible when auto-hidden
const EDGE_TRIGGER = 8; // cursor within this many px of the edge reveals
const REVEAL_MARGIN = 6; // keep revealed while cursor is within bounds+this
@@ -44,15 +45,18 @@ let gripDragging = false; // suppress auto-hide while
function clampBar(h) { return Math.max(BAR_MIN, Math.min(BAR_MAX, Math.round(h || BAR_DEFAULT))); }
function clampBarW(w) { return Math.max(BARW_MIN, Math.min(BARW_MAX, Math.round(w || BARW_DEFAULT))); }
+// TOP strip length: 0 (or anything <=0/NaN) means "span the whole screen"; otherwise a clamped px length.
+function clampBarLen(v) { const n = Math.round(Number(v) || 0); return n <= 0 ? 0 : Math.max(BARLEN_MIN, Math.min(BARLEN_MAX, n)); }
function readBarH() { try { return clampBar(parseInt(fs.readFileSync(BARH_FILE, 'utf8'), 10)); } catch { return BAR_DEFAULT; } }
function writeBarH() { try { fs.writeFileSync(BARH_FILE, String(barH)); } catch {} }
function readCfg() {
- const d = { orientation: 'top', autohide: false, barW: BARW_DEFAULT };
+ const d = { orientation: 'top', autohide: false, barW: BARW_DEFAULT, barLen: 0 };
try {
const j = JSON.parse(fs.readFileSync(CFG_FILE, 'utf8'));
if (ORIENTS.includes(j.orientation)) d.orientation = j.orientation;
d.autohide = !!j.autohide;
d.barW = clampBarW(j.barW);
+ d.barLen = clampBarLen(j.barLen);
} catch {}
return d;
}
@@ -94,7 +98,9 @@ function boundsCore(display, open, hidden) {
if (o === 'top') {
const height = hidden ? barH : (open ? barH + PANEL_H : barH);
const y = hidden ? wa.y + PEEK - barH : wa.y; // slide up, PEEK visible at top
- return { x: wa.x, y, width: wa.width, height };
+ // barLen 0 = span the screen; otherwise a left-anchored length, clamped to this display's width.
+ const width = cfg.barLen > 0 ? Math.min(cfg.barLen, wa.width) : wa.width;
+ return { x: wa.x, y, width, height };
}
const bw = cfg.barW;
const width = hidden ? bw : (open ? bw + PANEL_W : bw);
@@ -122,7 +128,7 @@ function applyBoundsFor(w) {
}
function applyAll() { for (const w of wins) applyBoundsFor(w); }
function pushBarH(w) { try { if (!w.isDestroyed()) w.webContents.send('dotbar:barH', barH); } catch {} }
-function pushCfg(w) { try { if (!w.isDestroyed()) w.webContents.send('dotbar:config', { orientation: cfg.orientation, autohide: cfg.autohide, barW: cfg.barW }); } catch {} }
+function pushCfg(w) { try { if (!w.isDestroyed()) w.webContents.send('dotbar:config', { orientation: cfg.orientation, autohide: cfg.autohide, barW: cfg.barW, barLen: cfg.barLen }); } catch {} }
// setBarH/setBarW fire on EVERY pointermove during a grip drag, so keep them cheap: resize each
// strip and mirror the value to its renderer live, but do NOT re-assert always-on-top per frame
@@ -147,6 +153,19 @@ function setBarW(wpx) {
}
clearTimeout(_persistWTimer); _persistWTimer = setTimeout(writeCfg, 300);
}
+// TOP-strip horizontal length (right-end grip drag). Dragging out to/past the screen width snaps back
+// to "full" (barLen 0) so it re-spans the screen and survives a resolution change. Applies to all strips.
+let _persistLTimer = null;
+function setBarLen(px) {
+ const full = screen.getPrimaryDisplay().workArea.width;
+ cfg.barLen = (Math.round(Number(px) || 0) >= full - 4) ? 0 : clampBarLen(px);
+ for (const w of wins) {
+ if (w.isDestroyed()) continue;
+ w.setBounds(boundsFor(w, displayOf(w), !!openState.get(w.id)));
+ pushCfg(w);
+ }
+ clearTimeout(_persistLTimer); _persistLTimer = setTimeout(writeCfg, 300);
+}
function setOrientation(o) {
if (!ORIENTS.includes(o) || o === cfg.orientation) return;
@@ -240,6 +259,7 @@ async function createWindows() {
});
ipcMain.on('dotbar:setBarH', (_e, h) => setBarH(h)); // grip drag (top): applies to all strips
ipcMain.on('dotbar:setBarW', (_e, w) => setBarW(w)); // grip drag (side): applies to all strips
+ ipcMain.on('dotbar:setBarLen', (_e, px) => setBarLen(px)); // right-end grip (top): horizontal length, applies to all strips
ipcMain.on('dotbar:gripDrag', (_e, on) => { gripDragging = !!on; if (!on && cfg.autohide) revealed.clear(); });
ipcMain.on('dotbar:setOrientation', (_e, o) => setOrientation(o));
ipcMain.on('dotbar:cycleOrientation', () => setOrientation(ORIENTS[(ORIENTS.indexOf(cfg.orientation) + 1) % ORIENTS.length]));
diff --git a/package-lock.json b/package-lock.json
index accbb6c..20a0a0e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "desktop-dotbar",
- "version": "1.1.0",
+ "version": "1.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "desktop-dotbar",
- "version": "1.1.0",
+ "version": "1.2.0",
"dependencies": {
"electron": "^44.4.0"
}
diff --git a/package.json b/package.json
index 0bc2519..e4273c9 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "desktop-dotbar",
- "version": "1.1.0",
+ "version": "1.2.0",
"private": true,
"main": "electron-main.js",
"scripts": {
diff --git a/preload.js b/preload.js
index f32e96d..03663d2 100644
--- a/preload.js
+++ b/preload.js
@@ -3,8 +3,9 @@ contextBridge.exposeInMainWorld('dotbar', {
setOpen: (open) => ipcRenderer.send('dotbar:setOpen', !!open),
setBarH: (h) => ipcRenderer.send('dotbar:setBarH', h), // absolute strip height (grip edge-drag, top dock)
setBarW: (w) => ipcRenderer.send('dotbar:setBarW', w), // absolute strip width (grip edge-drag, side dock)
+ setBarLen: (px) => ipcRenderer.send('dotbar:setBarLen', px), // absolute TOP-strip horizontal length (right-end grip; 0/full = span the screen)
onBarH: (cb) => ipcRenderer.on('dotbar:barH', (_e, h) => cb(h)),
- onConfig: (cb) => ipcRenderer.on('dotbar:config', (_e, c) => cb(c)), // { orientation, autohide, barW }
+ onConfig: (cb) => ipcRenderer.on('dotbar:config', (_e, c) => cb(c)), // { orientation, autohide, barW, barLen }
gripDrag: (on) => ipcRenderer.send('dotbar:gripDrag', !!on), // suppress auto-hide during a grip drag
setOrientation: (o) => ipcRenderer.send('dotbar:setOrientation', o), // 'top' | 'left' | 'right'
cycleOrientation: () => ipcRenderer.send('dotbar:cycleOrientation'),
diff --git a/public/index.html b/public/index.html
index 12625e9..0ef6b05 100644
--- a/public/index.html
+++ b/public/index.html
@@ -41,6 +41,15 @@
body.orient-left #grip::after, body.orient-right #grip::after { top:50%; left:2px; width:2px; height:46px; transform:translateY(-50%); }
#grip:hover::after { opacity:1; background:var(--dim); }
+ /* Right-end LENGTH grip — TOP dock only: drag the bar's right edge to contract/expand its horizontal
+ span (ew-resize), left-anchored. Hidden on side docks (there the main #grip changes width). Stops
+ 8px short of the bottom so it never fights the height grip in the corner. Steve 2026-09-17. */
+ #gripx { position:fixed; z-index:50; -webkit-app-region:no-drag; display:none; }
+ body.orient-top #gripx { display:block; top:0; right:0; width:8px; height: calc(var(--bar-h) - 8px); cursor:ew-resize; }
+ #gripx::after { content:""; position:absolute; right:2px; top:20%; height:60%; width:2px; border-radius:2px;
+ background:var(--line); opacity:.5; }
+ #gripx:hover::after { opacity:1; background:var(--dim); }
+
.chip { -webkit-app-region:no-drag; cursor:pointer; display:flex; align-items:center; gap:6px;
padding:6px 10px; border-radius:9px; border:1px solid transparent; line-height:1; white-space:nowrap; }
.chip:hover { background:var(--bg2); border-color:var(--line); }
@@ -92,13 +101,14 @@
</head>
<body class="orient-top">
<div id="bar"></div>
- <div id="grip" title="drag to resize the bar"></div>
+ <div id="grip" title="drag to resize the bar's thickness"></div>
+ <div id="gripx" title="drag to contract/expand the bar's width"></div>
<div id="panel"></div>
<script>
const BAR_H = 48, PANEL_H = 360;
-let openColor = null, data = null, _miss = 0, curBarH = BAR_H, curBarW = 210, arranging = false, arrangeMsg = '';
-// Live config from the main process (orientation + auto-hide). Defaults match a fresh install.
-let cfg = { orientation: 'top', autohide: false, barW: 210 };
+let openColor = null, data = null, _miss = 0, curBarH = BAR_H, curBarW = 210, curBarLen = 0, arranging = false, arrangeMsg = '';
+// Live config from the main process (orientation + auto-hide + top-bar length). Defaults match a fresh install.
+let cfg = { orientation: 'top', autohide: false, barW: 210, barLen: 0 };
// The four needs-Steve states pulse; green/pink stay solid (Steve 2026-09-15 dot-flash directive).
const WAIT = new Set(['lightblue','orange','purple','yellow']);
const ORIENT_LABEL = { top: '▲ Top', left: '◀ Left', right: '▶ Right' };
@@ -200,6 +210,7 @@ function applyCfg(c){
cfg = Object.assign(cfg, c || {});
document.body.className = 'orient-' + (cfg.orientation || 'top');
if (cfg.barW) { curBarW = cfg.barW; document.documentElement.style.setProperty('--bar-w', cfg.barW + 'px'); }
+ curBarLen = cfg.barLen || 0; // 0 = full-screen span; main owns the actual top-bar window width
if (data) renderBar();
}
if (window.dotbar && window.dotbar.onConfig) window.dotbar.onConfig(applyCfg);
@@ -236,6 +247,31 @@ if (window.dotbar && window.dotbar.onBarH) {
grip.addEventListener('pointercancel', end);
})();
+// ---- Contract/expand the TOP bar horizontally: drag the right-end grip. Left-anchored, so dragging
+// right widens and left narrows; screen-X deltas + pointer capture keep it correct as the right edge
+// (where the grip lives) moves during the drag. Full span when barLen is 0 (== the window width). ----
+(function wireGripX(){
+ const grip = document.getElementById('gripx');
+ if (!grip) return;
+ let dragging = false, startX = 0, startLen = 0;
+ grip.addEventListener('pointerdown', e => {
+ dragging = true; startX = e.screenX;
+ startLen = (cfg.barLen && cfg.barLen > 0) ? cfg.barLen : window.innerWidth; // full -> current window width
+ try { grip.setPointerCapture(e.pointerId); } catch(_) {}
+ if (window.dotbar && window.dotbar.gripDrag) window.dotbar.gripDrag(true);
+ e.preventDefault();
+ });
+ grip.addEventListener('pointermove', e => {
+ if (!dragging) return;
+ const len = startLen + (e.screenX - startX);
+ if (window.dotbar && window.dotbar.setBarLen) window.dotbar.setBarLen(len);
+ });
+ const end = e => { dragging = false; try { grip.releasePointerCapture(e.pointerId); } catch(_) {}
+ if (window.dotbar && window.dotbar.gripDrag) window.dotbar.gripDrag(false); };
+ grip.addEventListener('pointerup', end);
+ grip.addEventListener('pointercancel', end);
+})();
+
// optional deep-link: ?open=orange pre-opens that dropdown
const _pre = new URLSearchParams(location.search).get('open');
resize(false);
← 2415402 chore: v1.1.0 — nav bar edge auto-hide + orientation toggle
·
back to Desktop Dotbar
·
desktop-dotbar: collapsed PARKED chip/panel from the durable b102cca →