← Commit history

2.1.0: onToggle(open, source) so graduation cannot feed back into itself; toggle tooltip swaps with state; document pass-through proxying and that selfCheck is meaningless under jsdom

John Lauer ·5aee332458 ·1mo ago ·parent e11caf9
3 files changed +50−9
assets/module/newbie-panel.js+23−8
@@ -154,6 +154,7 @@   function tipFor(node, text) {     node.setAttribute('aria-label', text);     node.removeAttribute('title');          // two tooltips on one control is a bug+    node.__anpSetTip = function (t) { text = t; if (tip && tip.classList.contains('on')) tip.textContent = t; };     node.addEventListener('mouseenter', function (e) {       clearTimeout(tipT);       tipT = setTimeout(function () {@@ -307,21 +308,32 @@     /* the header ? is a real TOGGLE, both ways, with aria-pressed and a label that swaps */     var toggleBtn = typeof cfg.toggleButton === 'string'       ? document.querySelector(cfg.toggleButton) : cfg.toggleButton;-    function setOpen(open, remember) {+    /* setOpen(open, remember, source)+     * source is 'user' when a person clicked the X or the header toggle, and 'api' when the+     * app called open()/close() itself. It is passed to onToggle, because an app with a+     * graduation policy calls open()/close() from inside its own onToggle handler: without a+     * source the callback cannot tell its own action from a person's, so graduating the panel+     * open pins it open forever and graduating it closed marks it user-dismissed. It looks+     * like "the panel sometimes ignores graduation". (lbr, 2026-08-25) */+    function setOpen(open, remember, source) {       panel.hidden = !open;       wrap.style.display = open ? '' : 'none';   /* no dead margin when collapsed */       if (toggleBtn) {+        var label = open ? 'Hide the sample prompts' : 'Show the sample prompts';         toggleBtn.setAttribute('aria-pressed', open ? 'true' : 'false');-        toggleBtn.setAttribute('aria-label', open ? 'Hide the sample prompts' : 'Show the sample prompts');+        toggleBtn.setAttribute('aria-label', label);+        /* the TOOLTIP has to swap with the state too, or a person with the panel already in+         * front of them hovers the control and is told it will show them the panel */+        if (toggleBtn.__anpSetTip) toggleBtn.__anpSetTip(label);       }       if (remember !== false) { try { localStorage.setItem(key, open ? '1' : '0'); } catch (e) {} }-      if (typeof cfg.onToggle === 'function') cfg.onToggle(open);+      if (typeof cfg.onToggle === 'function') cfg.onToggle(open, source || 'api');     }-    x.addEventListener('click', function () { setOpen(false); });+    x.addEventListener('click', function () { setOpen(false, true, 'user'); });     if (toggleBtn) {       if (!toggleBtn.innerHTML.trim()) toggleBtn.innerHTML = ICON.help;       tipFor(toggleBtn, 'Show the sample prompts');-      toggleBtn.addEventListener('click', function () { setOpen(panel.hidden); });+      toggleBtn.addEventListener('click', function () { setOpen(panel.hidden, true, 'user'); });     }     var saved = null;     try { saved = localStorage.getItem(key); } catch (e) {}@@ -345,8 +357,11 @@     }      return {-      panel: panel, open: function () { setOpen(true); }, close: function () { setOpen(false); },-      toggle: function () { setOpen(panel.hidden); }, isOpen: function () { return !panel.hidden; },+      panel: panel,+      open:   function (o) { setOpen(true,  !(o && o.remember === false), 'api'); },+      close:  function (o) { setOpen(false, !(o && o.remember === false), 'api'); },+      toggle: function () { setOpen(panel.hidden, true, 'api'); },+      isOpen: function () { return !panel.hidden; },       overBudget: over, toast: toast, selfCheck: function () { return selfCheck(panel); }     };   }@@ -413,6 +428,6 @@     return { ok: ok, rows: rows };   } -  root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, version: '2.0.5',+  root.AdomNewbiePanel = { mount: mount, toast: toast, selfCheck: selfCheck, version: '2.1.0',                            TWO_COL_MIN: TWO_COL_MIN, PHRASE_BUDGET: PHRASE_BUDGET }; })(typeof window !== 'undefined' ? window : this);
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-ui-design",-  "version": "2.0.5",+  "version": "2.1.0",   "type": "skill",   "description": "The Adom UI Design System plus a bundle of focused UI skills (tooltips, floating-windows, toasts, responsive-ui, app-shell, ai-driven-apps, clickable-paths, dark-default, input-feedback, settings-layout). One install drops the whole set of skill files onto your container, plus shared icon assets (the settings cog) so every Adom app draws the same glyph.",   "dependencies": {},
skills/newbie-panel/SKILL.md+26
@@ -88,6 +88,25 @@ the desktop-bridge-served one, where a direct fetch to the control port would be or unreachable. The dynamic control port also stays server-side rather than being baked into a page. +**PASS THE RESPONSE THROUGH UNCHANGED.** Do not reshape the JSON on the way back. The module+reads `delivered_to` itself (it is an ARRAY) and separates *delivered* from *opened but not+confirmed*; an app that "helpfully" normalises the payload collapses three states into two,+and the retry that follows double-sends a prompt that already landed. That has happened to+two apps already. Keep the 120s timeout on `run-prompt` too, so a queued call is not turned+into a phantom failure.++### onToggle tells you WHO toggled++`onToggle(open, source)` where source is `'user'` (the X or the header button) or `'api'`+(your own `open()` / `close()`). Any app with a graduation policy calls open/close from+inside its own handler, so without the source the callback cannot tell its own action from a+person's: graduating the panel open pins it open forever, graduating it closed marks it+user-dismissed, and the symptom reads as "the panel sometimes ignores graduation". Found by+lbr while integrating; fixed in the module rather than worked around per app.++`open()` and `close()` accept `{remember: false}` when you do not want the call written to+the stored preference.+ **What the module owns, so you cannot get it wrong:** the card and its accent edge, the heading and explainer, the two-column grid and its collapse, the table, the row height, the phrase typeface and the noun highlight, both icons, the single aligned action column, the@@ -139,6 +158,13 @@ light DOM, which is right for the single-panel case and silently wrong if you mo mount into a shadow root: pass the panel explicitly then. With no panel at all it returns `{ok:false, error:'no .anp panel on the page'}` rather than throwing. +**selfCheck CANNOT RUN IN A HEADLESS HARNESS, and its verdict from one means nothing.**+jsdom does not parse the module's `@container` stylesheet, so every computed value comes back+empty and selfCheck dutifully returns `ok:false` with blank rows. That is the harness+failing, not the panel, and it is exactly the kind of confidently wrong reading this skill+keeps warning about. jsdom is still the right tool for BEHAVIOUR (does it mount, are there+six rows, twelve buttons, zero swallowed ReferenceErrors); it can never speak to pixels.+ Run it in your dashboard, in ah's webview, and paste the result. It checks the card background, border, border colour, accent edge, radius, padding, column gap, whether the Copy icons resolve to a single x-position, the column count against the panel's width, and