← Commit history

0.5.46: max-content clone measurement, preview follows requested width, clock pictures for both layouts

John Lauer ·4042e6c9d6 ·1mo ago ·parent b82b8ca
6 files changed +27−22
bin/hydrogen-widget-lib.py+18−19
@@ -14,7 +14,7 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family    tall = rows stacked to use the height; short = one horizontal row. */ .w{display:flex;align-items:center;height:100vh;padding:0 5px;gap:5px;white-space:nowrap} .mark{display:inline-flex;flex:none;width:15px;height:15px}.mark svg{width:100%%;height:100%%}-.rows{display:flex;align-items:center;gap:8px;min-width:0;flex:1}+.rows{display:flex;align-items:center;gap:8px;min-width:0;flex:1 1 auto} .row{display:flex;align-items:center;gap:4px} .lbl{opacity:.75;letter-spacing:.04em;text-transform:uppercase} .bar{position:relative;border-radius:3px;background:rgba(255,255,255,.14);overflow:hidden}@@ -36,8 +36,8 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family   .w{font-size:9px;line-height:1;padding:2px 6px}   .rows{flex-direction:column;align-items:stretch;justify-content:center;gap:2px;min-width:0}   .row{display:grid;grid-template-columns:auto minmax(56px,1fr) auto;align-items:center;gap:4px;min-width:0}-  .lbl{font-size:7.5px;max-width:5.5em;overflow:hidden;text-overflow:ellipsis}.bar{width:auto;height:5px}.v{font-size:9px;min-width:0}-  .big{font-size:15px}.err{font-size:9px;white-space:normal;line-height:1.1}+  .lbl{font-size:7.5px}.row .lbl{max-width:5.5em;overflow:hidden;text-overflow:ellipsis}.bar{width:auto;height:5px}.v{font-size:9px;min-width:0}+  .big{font-size:15px}.err{font-size:9px;white-space:normal;line-height:1.1;max-width:130px} } @media (max-height:33px){ .err{overflow:hidden;text-overflow:ellipsis;max-width:100%%} } </style><body><div class="w"><span class="mark" style="color:%(mark_color)s">%(mark_svg)s</span><div class="rows" id="w">%(title)s…</div></div>@@ -45,18 +45,14 @@ html,body{margin:0;height:100%%;background:transparent;color:#e6ebef;font-family const R=%(refresh_ms)d; function bar(l,p,sev){const c=(sev==='critical'||p>=90)?'hot':(sev==='warning'||p>=70)?'warn':'';return `<span class="row"><span class="lbl">${l}</span><span class="bar ${c}"><i style="width:${Math.max(0,Math.min(100,p))}%%"></i></span><span class="v">${Math.round(p)}%%</span></span>`;} function render(d){document.getElementById('w').innerHTML=%(render_js)s;postWidth();}-/* THE WIDGET DECIDES ITS WIDTH: measure the content in the current layout (tall rows are narrow,-   the compact row is wide) and ask the host for that width. Never fixed px; the host clamps. */-function postWidth(){try{const w=document.querySelector('.w');const rows=document.getElementById('w');if(!w||!rows)return;-  /* Intrinsic width: rows stretch to the slot, so never measure a row's box. Sum each row's children-     (a bar counts at its minimum in the tall layout, its fixed width in the short one) plus gaps. */-  const tall=window.innerHeight>=34;let need=0;-  for(const r of rows.children){const rs=getComputedStyle(r);const gap=parseFloat(rs.columnGap||rs.gap||'0')||0;let sum=0,n=0;-    for(const c of r.children){n++;if(c.classList.contains('bar')){sum+=tall?56:c.getBoundingClientRect().width;}else{sum+=Math.ceil(c.scrollWidth);}}-    if(n===0)sum=Math.ceil(r.scrollWidth);need=Math.max(need,sum+gap*Math.max(0,n-1));}-  if(!tall){const rg=parseFloat(getComputedStyle(rows).gap||'0')||0;let total=0,n=0;for(const r of rows.children){n++;const rs=getComputedStyle(r);const gap=parseFloat(rs.columnGap||rs.gap||'0')||0;let sum=0,k=0;for(const c of r.children){k++;sum+=c.classList.contains('bar')?c.getBoundingClientRect().width:Math.ceil(c.scrollWidth);}if(k===0)sum=Math.ceil(r.scrollWidth);total+=sum+gap*Math.max(0,k-1);}need=total+rg*Math.max(0,n-1);}-  const mark=w.querySelector('.mark');const cs=getComputedStyle(w);-  const total=Math.ceil(need+(mark?mark.getBoundingClientRect().width+(parseFloat(cs.gap||'0')||0):0)+parseFloat(cs.paddingLeft)+parseFloat(cs.paddingRight)+3);+/* THE WIDGET DECIDES ITS WIDTH: ask the host for the intrinsic width of what is shown in the+   current layout (tall rows are narrow, the compact row is wide). Measured on a hidden clone laid+   out at max-content, so the slot's current width never feeds back into the answer (rows stretch to+   the slot, so measuring the live boxes would only ever grow). Works for any markup, not just rows. */+function postWidth(){try{const w=document.querySelector('.w');if(!w)return;+  const c=w.cloneNode(true);c.querySelectorAll('[id]').forEach(e=>e.removeAttribute('id'));+  c.style.cssText='position:absolute;left:-10000px;top:0;width:max-content;height:100vh;visibility:hidden;pointer-events:none';+  document.body.appendChild(c);const total=Math.ceil(c.getBoundingClientRect().width)+2;c.remove();   if(total>0&&window.parent!==window)window.parent.postMessage({hdWidget:{width:total}},'*');}catch(e){}} async function tick(){try{const r=await fetch('/data.json',{cache:'no-store'});const d=await r.json();window.__last=d;render(d);}catch(e){document.getElementById('w').innerHTML='<span class="err">no data</span>';}} let tm=null;function arm(){if(tm)return;tm=setInterval(tick,R);}function disarm(){if(tm){clearInterval(tm);tm=null;}}@@ -91,18 +87,21 @@ def serve(title, data_fn, render_js, refresh_ms=60000, mark=None):         def do_GET(self):             if self.path.startswith('/data.json'): self._send(200, 'application/json', json.dumps(data()).encode())             elif self.path.startswith('/preview'):-                # /preview?w=175&h=38 : this widget's own page in a frame of exactly that size, offset+                # /preview?h=38 (or ?w=175&h=38): this widget's own page in a frame of that height, offset                 # from the corner, so a browser screenshot of the frame (at any device scale) is a-                # clean, crisp picture for docs. The Hydrogen slot never uses this; it is for humans.+                # clean, crisp picture for docs. Without w the frame follows the width the widget asks+                # for, exactly as the Title Bar slot does; with w it is pinned. The slot never uses this.                 from urllib.parse import urlparse, parse_qs                 q = parse_qs(urlparse(self.path).query)+                fixed = 'w' in q                 try: w = int(q.get('w', ['175'])[0]); h = int(q.get('h', ['38'])[0])                 except ValueError: w, h = 175, 38                 w = max(48, min(640, w)); h = max(16, min(120, h))                 page = ('<!doctype html><meta charset="utf-8"><title>%s preview</title>'                         '<body style="margin:0;background:#0f1419"><div style="padding:150px 0 40px 40px">'-                        '<iframe id="w" src="/" style="width:%dpx;height:%dpx;border:1px solid #2c323a;border-radius:6px;background:#1c2128;display:block"></iframe>'-                        '</div></body>') % (title, w, h)+                        '<iframe id="w" src="/" data-w="0" style="width:%dpx;height:%dpx;border:1px solid #2c323a;border-radius:6px;background:#1c2128;display:block"></iframe>'+                        '</div><script>const fixed=%s;window.addEventListener("message",e=>{const m=e.data&&e.data.hdWidget;if(!m||!m.width)return;'+                        'const f=document.getElementById("w");f.dataset.w=m.width;if(!fixed)f.style.width=Math.max(48,Math.min(640,m.width))+"px";});</script></body>') % (title, w, h, 'true' if fixed else 'false')                 self._send(200, 'text/html; charset=utf-8', page.encode())             elif self.path.startswith('/health'): self._send(200, 'application/json', b'{"ok":true}')             else: self._send(200, 'text/html; charset=utf-8', (PAGE % {'title': title, 'refresh_ms': refresh_ms, 'render_js': render_js, 'mark_svg': ('<svg viewBox="0 0 16 16">' + mark['svg'] + '</svg>') if mark['svg'] else '', 'mark_color': mark['color']}).encode())
package.json+1−1
@@ -4,7 +4,7 @@   "type": "bootstrap",   "title": "Adom Hydrogen (platform base)",   "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",-  "version": "0.5.45",+  "version": "0.5.46",   "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.",   "org": "adom",   "tags": [
page.json+2−2
@@ -4,7 +4,7 @@   "type": "bootstrap",   "title": "Adom Hydrogen (platform base)",   "brief": "Layer Hydrogen's workspace onto a standard Adom workstation.",-  "version": "0.5.45",+  "version": "0.5.46",   "description": "Hydrogen's PLATFORM-GENERIC workspace layer on top of adom/core. Installs adom/core for the full Adom ecosystem, then adds everything common to running inside Hydrogen on ANY platform: the runtime-neutral hydrogen-* skills and the generic editor configuration (Claude Code CLI + extension, VS Code settings, trusted domains). Platform-specific layers depend on THIS: adom/hydrogen-windows-bootstrap (WSL2), adom/hydrogen-mac-bootstrap, adom/hydrogen-ubuntu-bootstrap. The reference design other orgs copy to build their own bootstrap on adom/core.",   "org": "adom",   "tags": [@@ -49,4 +49,4 @@     "hydrogen editor configuration",     "where do hydrogen skills come from"   ]-}\ No newline at end of file+}
widgets/README.md+6
@@ -2,6 +2,8 @@  ![The clock in the full Title Bar: time over date](widgets/docs/clock.png) +![The clock in the compact Title Bar: time and date in one row](widgets/docs/clock-compact.png)+ A Hydrogen Widget is a small live page in Hydrogen Desktop's Title Bar, served by a program inside your workspace. This one shows the time and date: stacked in the full Title Bar, inline in the compact one. It is a **facet** of this repo: the bootstrap package installs `bin/hydrogen-widget-clock` and the shared helper, and the `hydrogen-widget.json` at the repo root tells Hydrogen it exists. Open it from the Hydrogen Widgets pulldown (the puzzle-piece icon left of the meter) or `POST /widgets/open {"id":"clock"}`.  ## The files in this repo that make it a widget@@ -17,6 +19,10 @@ widgets/README.md                  this page (linked from the pulldown row)  `install.sh` copies `bin/hydrogen-widget-*` into `~/.local/bin`, which is where `launch.command` finds it. +## How it sizes itself++The helper's page has two layouts chosen by the slot's height (stacked above 33 px, one row below), and after every render it measures itself on a hidden copy laid out at max-content and posts `{hdWidget:{width}}` to Hydrogen, which resizes the slot. The pictures above are that measurement: the clock asks for about 84 px tall and 106 px compact. The helper also serves `/preview?h=38` and `/preview?h=20`, its own page in a frame that follows the requested width; the pictures were made from those with headless Chrome at three times scale.+ ## Copy it  Take `hydrogen-widget-clock` and `hydrogen-widget-lib.py`, change `data()` and the render expression, add a `hydrogen-widget.json`, a page like this one, and an install line. The [hydrogen-widget skill](https://wiki.adom.inc/adom/hydrogen-bootstrap/files/skills/hydrogen-widget/SKILL.md) has every field, the launch contract, the tall and compact layouts, and how private repos work. The overview with pictures is the [Widgets page](https://wiki.adom.inc/adom/hydrogen/files/widgets/README.md) on the Hydrogen wiki.
widgets/docs/clock-compact.pngadded
⋯ 1 unchanged line ⋯
widgets/docs/clock.png
⋯ 1 unchanged line ⋯