← Commit history

Publish 1.7.2

John Lauer ·b97fbfdd91 ·1mo ago ·parent 4df1cdb
3 files changed +104−14
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-ui-design",-  "version": "1.7.1",+  "version": "1.7.2",   "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": {},
page.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-ui-design",-  "version": "1.7.1",+  "version": "1.7.2",   "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/settings-layout/SKILL.md+102−12
@@ -18,7 +18,8 @@ description: >-   Trigger words: settings dialog, settings modal, settings layout, preferences pane, config ui,   settings card, options dialog, app settings, settings panel, mimic ah settings, cog wheel,   gear icon, settings backdrop, dim the background, blur the background, settings sidebar,-  jump to section, scroll spy, restore defaults, dashboard settings.+  jump to section, scroll spy, restore defaults, dashboard settings, settings screenshots,+  native resolution capture, devicePixelRatio, on off pair, settings pitfalls. ---  Parent skill: **adom-ui-design**@@ -641,14 +642,30 @@ fabricated screenshot is a lie with a picture attached. 1. Put the app in the real state. Set the option, restart if the option needs it, generate the    condition the feature responds to. If the feature only appears while something is happening    (a progress bar during a long operation, a badge on an unread item), trigger that too.-2. Capture the running app with a real screenshot verb. On the desktop that is-   `desktop_screenshot_window` (works on a background window, so it never steals focus) or a-   `pup_screenshot` for a browser surface. Never a synthetic render.-3. Crop tight to the feature, at 1:1 pixels. Do not scale a full-window capture down into the-   card, and do not upscale a small crop; both make the difference unreadable, which defeats the-   whole point.-4. Look at the crop. If you cannot tell the options apart from the images alone, your crop is-   wrong or the images are of the wrong moment. Re-capture.+2. Capture the running app with a real screenshot verb, **at the display's native+   resolution**. This is where "real" quietly stops being "1:1": the convenience verbs+   (`desktop_screenshot_screen`, `desktop_screenshot_window`, `pup_screenshot`) return a+   DOWNSCALED frame to keep payloads small (a 2560-wide display came back 1568 wide, a 0.61x+   frame). A crop of that is grainy and every feature in it is 60% of its real size, which a+   user reads instantly as "did you fake this?". Before you crop anything, compare the+   capture's pixel width with the display's native width (`GetSystemMetrics(0)` after+   `SetProcessDPIAware`, or `screen.width * devicePixelRatio` in a page). If they differ, use+   the full-resolution path: `pup_screenshot_full_res` for a browser surface, or a DPI-aware+   `Graphics.CopyFromScreen` run ON the box and pulled back with `pull_file` for the desktop.+   Never a synthetic render.+3. Crop tight to the feature, at 1:1 device pixels. Do not scale a full-window capture down+   into the card, and do not upscale a small crop; both make the difference unreadable, which+   defeats the whole point. If two real-size images do not fit side by side, WIDEN THE DIALOG;+   never shrink the pictures to fit.+4. Render at device pixels, not CSS pixels. A 272-device-pixel capture given `width="272"`+   is upscaled 1.5x by the browser on a 150% display and comes out soft. Size each shot to+   `naturalWidth / devicePixelRatio` (a few lines of JS on open and on resize), so the picture+   is exactly the physical size of the thing it shows. Record the capture machine's scale+   factor next to the assets so a re-capture matches.+5. Look at the crop, in a FULL-RESOLUTION capture of the rendered dialog, not the downscaled+   one (the same verbs downscale your verification shot too, so a soft image can look fine).+   If you cannot tell the options apart from the images alone, your crop is wrong or the+   images are of the wrong moment. Re-capture.  **If you cannot produce the state, ship no image.** Prose with no picture is honest and useful; a picture you invented is neither. Say so plainly in the card ("screenshot pending")@@ -663,9 +680,12 @@ Rules that keep these from rotting: - **Capture in the app's own dark theme** so the shot does not fight the dialog around it. If   your app ships light and dark and the setting looks materially different in each, show the   one matching the CURRENT scheme, or show both side by side and label them.-- **A before/after pair beats a single shot** when the setting is a choice rather than an-  on/off: two images, each captioned, same crop and same zoom so the difference is the only-  thing that changes.+- **Show BOTH states, including for an on/off toggle.** A single "on" picture next to a toggle+  leaves the user guessing what "off" removes (John, 2026-08-23, looking at one badge shot: "you+  have to show both states here of overlay on and overlay off so user knows what you're talking+  about"). Two images, each captioned On / Off (or the option names), same crop, same zoom, same+  moment, so the difference is the only thing that changes. The earlier version of this rule+  exempted on/off settings; it was wrong. - **The alt text describes the DIFFERENCE**, not the app ("rows sitting closer together", not   "screenshot of the thread list"). A screen reader user gets the same information. - **`loading="lazy"`**, and keep each shot small (tens of KB). A settings dialog that stutters@@ -773,6 +793,69 @@ If the alternatives are NOT visual (a polling interval, a log level), skip the c enum control with a Default hint is right, and inventing screenshots for it is exactly the failure mode this section forbids. +## Field notes: what actually went wrong shipping a settings modal (kicad-bridge, 2026-08-23)++The KiCad dashboard rebuilt its settings to this skill in one sitting. Every item below cost a+round trip with John, and none of them is visible from the code alone. Read them before your+first screenshot, not after your first review.++### Capturing the screenshots++- **Pin the crop rectangle from the baseline state and reuse it.** Locating the feature by+  colour or shape works for the plain state and then fails on the very states you are+  documenting: a badge covers part of the icon, a tint changes its colours, and the locator+  drifts to a neighbouring element (it found a Google Messages icon instead of the KiCad+  button). Find the feature once in the plain state, record the rectangle, and crop every+  other state at the same coordinates. That is also what makes the set "same crop, same zoom".+- **Drive the state through the product's real mechanism, then capture.** For the KiCad+  taskbar set: the bar through the same taskbar API the bridge uses, the badge through the+  same overlay call, the flash through `FlashWindowEx`, each with the SETTING actually set to+  that mode so the capture is of the app in that configuration, not a simulation of it.+- **Windows 11 specifics, so you do not lose an hour:** the "orange flash" is a salmon tint on+  the button plus a highlighted underline, and it does NOT blink, so one frame is enough. It+  also survives `FLASHW_STOP`; the button only clears when it is rebuilt. Hide and re-show the+  window (`ShowWindow(SW_HIDE)` then `SW_SHOWNA`, never on the foreground window) to clear it+  without activating anything. Check it actually cleared with a fresh capture; it took two+  attempts once.+- **Leave the user's machine as you found it, and prove it.** Restore every setting you flipped+  for a capture, clear every bar, badge and tint, and take one last capture to verify. A+  settings dialog is not worth a stray highlight on someone's taskbar for the rest of the day.++### Making the dialog honest++- **Read the stored value back; never trust the 200.** The dashboard had a working control,+  a working action arm and a toast that said "saved", and the bridge behind it was coercing+  every value with `bool()`, so the enum `"flash"` persisted as `True` and the feature ran its+  fallback branch for days. The test that catches this is: change the setting in the UI, then+  read the settings file (or the GET endpoint) on the box and compare the literal value. Type+  each key by the type of its default; reject values outside an enum's choices.+- **Status text is recomputed truth.** The inline card status ("Progress bar", "on") comes+  from the last status poll of the box, never from what the page last clicked.+- **Test the four dismissals with real input, not by calling `close()`.** Dispatch an actual+  Escape key, a real click outside the box, a real click on the X, and ALSO a real click on the+  dialog's own padding (which must NOT close it; a naive backdrop handler closes on both). Read+  `dialog.open` after each. Then read `getComputedStyle(dlg, '::backdrop').backgroundColor` and+  `backdropFilter` and check the numbers, because "it looked dim" is how a 55% scrim ships.+- **Use the page's own click channel to open the cog for verification.** If your app has a+  `/ui/click` style channel (the AI-drivable contract), drive the cog through it. The page+  performs the click, so what you screenshot is what a user gets, including the autofocus+  scroll and the open-at-top fix.++### Shipping the assets++- **The installer is part of the feature.** The package published fine and the installed app+  had no screenshots: the install script copied two named files (`index.html`,+  `kicad-logo.png`) and silently skipped the new `assets/` tree. Copy the whole UI directory,+  or the shots will never leave your machine. Verify on the INSTALLED build: curl each asset+  URL for a 200 with an image content type, and in the live page check every+  `img.naturalWidth > 0`.+- **Serve assets by suffix, not by exact path.** Behind the adom.cloud proxy the app is+  mounted under a sub-path, so match `/assets/...` anywhere in the path (and the favicon by+  `endswith`), otherwise the proxied page shows broken images and a blank tab icon while the+  local one looks perfect.+- **A native `<dialog>` does not need the portal**; it already renders in the top layer. The+  portal rule above is for hand-rolled overlays living inside transformed ancestors.+ ## Saving: local-first, optimistic, bounded  Every rule here came from a real bug, and every one of them is invisible until it bites:@@ -807,12 +890,19 @@ Every rule here came from a real bug, and every one of them is invisible until i - [ ] Every row shows its default, using the option LABEL for enums - [ ] Settings that change something visual carry a tight, current screenshot of it - [ ] Every screenshot is a REAL capture of the running app, never drawn, generated, or borrowed+- [ ] Capture width equals the display's native width (no downscaled screenshot verb), cropped at 1:1 device pixels+- [ ] Shots render at `naturalWidth / devicePixelRatio`; the dialog widened rather than the pictures shrunk+- [ ] A visual on/off setting shows BOTH states as a captioned pair, same crop, same moment+- [ ] The machine used for captures was restored afterwards (settings, bars, badges, tints) and a final capture proves it - [ ] Mutually exclusive visual options are choice cards with one real screenshot each, same crop and zoom, including the off state - [ ] Dependent settings are indented under their parent with a spine, dimmed and disabled (never hidden) when the parent is off, and say why - [ ] Booleans are toggles (`role="switch"`), never a native checkbox - [ ] Long actions stream a live console; no silent multi-minute buttons - [ ] Footer note plus two-click auto-disarming Restore defaults - [ ] Saves are local-first, optimistic, bounded at 5s+- [ ] Each setting was changed in the UI and the STORED value read back from the box, literal for literal+- [ ] Dismissals tested with real input events, including a padding click that must NOT close; backdrop colour read from computed style+- [ ] The INSTALLED build serves every screenshot (asset URLs 200 with an image type, `img.naturalWidth > 0` in the live page), also through the proxy sub-path - [ ] House scrollbar, not the OS one; radius element is not the scroller - [ ] Hover, :active and :focus-visible on every interactive element - [ ] No hardcoded colours where a token exists; light scheme checked