[object Object]

← 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

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 →