← Commit history

settings-layout: never fabricate a screenshot, and choice cards for mutually exclusive visual options

John Lauer ·f3420fc4be ·1mo ago ·parent 3e7689d
John (2026-08-23) on both halves of the same problem. NEVER FABRICATE THE IMAGE. This is the rule an AI breaks, because drawing a plausible picture is easy and driving the app into the state is work. Forbidden without exception: CSS/SVG mockups that represent the feature, generated or illustrated images, a screenshot of a different state relabelled, borrowed images from another product, a diagram standing in for a screenshot. Each puts a picture next to a control saying this is what you get, and if it is invented the user chooses based on a thing that does not exist. Added the capture recipe that gets skipped: put the app in the real state including whatever transient condition the feature responds to, capture the running app with a real screenshot verb, crop tight at 1:1, then LOOK at the crop and re-capture if the options are not distinguishable. If the state cannot be produced, ship no image and say so; prose without a picture is honest. ALTERNATIVES. Two or three mutually exclusive options whose whole difference is visual (pup's taskbar activity: determinate progress bar, orange flash, or off) get a choice card each with its own real screenshot, not a dropdown, because a dropdown hides exactly the information needed to choose. Identical crop, zoom and surroundings across the set so the option is the only variable, the off state included as a card, a real radiogroup so keyboard and screen readers work, a teal ring for selection, and one line naming the tradeoff rather than the mechanism.
2 files changed +136−1
package.json+1−1
@@ -1,6 +1,6 @@ {   "slug": "adom-ui-design",-  "version": "1.7.0",+  "version": "1.7.1",   "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+135
@@ -618,6 +618,42 @@ so the reading order is name, explanation, picture, then the thing you flip. } ``` +### NEVER fabricate the image. Capture the real thing.++**This is the rule this section exists to enforce, and it is the one an AI breaks.** Drawing a+plausible picture is easy; driving the app into the state and capturing it is work. Do the work.++Forbidden, without exception:++- A mockup drawn in HTML/CSS/SVG that "represents" the feature.+- A generated or illustrated image of what the option looks like.+- A screenshot of a DIFFERENT state relabelled as this one.+- A stock or borrowed image of a similar feature from another product.+- A diagram standing in for a screenshot, when the setting changes something you can see.++Every one of those puts a picture next to a control and tells the user "this is what you get".+If the picture is invented, the user makes a choice based on a thing that does not exist, and it+is worse than prose because a picture is believed. The earlier rule stands and hardens here: a+fabricated screenshot is a lie with a picture attached.++**How to actually capture it**, which is the part that gets skipped:++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.++**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")+rather than filling the hole.+ Rules that keep these from rotting:  - **Crop tight to the feature.** A full-window screenshot shrunk into a card shows nothing.@@ -640,6 +676,103 @@ Rules that keep these from rotting: - **Never put text in the image that the card already says.** The picture shows, the prose   tells. +## Alternatives: one option or the other, each shown by its own real screenshot++A very common shape: two or three options that are mutually exclusive, and the difference+between them is entirely visual. The user cannot choose without seeing what each one does.++The worked example is pup's taskbar activity setting, which is exactly this: while pup is+working on a window it can paint a **determinate progress bar** on that window's taskbar+button, or **flash the button orange**, or do **nothing**. Three alternatives, one behaviour+each, and the whole decision lives in what they look like.++**Use a choice card per option, not a dropdown.** A dropdown hides every option but the one+already chosen, which is the exact information the user needs. Put the options side by side,+each with its own tight screenshot of the real result:++```html+<fieldset class="choice-set" role="radiogroup" aria-labelledby="taskbar-activity-label">+  <legend id="taskbar-activity-label" class="row-label">Taskbar activity</legend>+  <p class="subsection-description">What a window's taskbar button does while pup is working+     on it.</p>++  <label class="choice" data-selected={mode === 'progress'}>+    <input type="radio" name="taskbarActivity" value="progress" checked={mode === 'progress'}>+    <img src="/assets/shots/taskbar-progress.png" width="240" height="72"+         alt="A taskbar button filled about two thirds with a teal progress bar">+    <span class="choice-name">Progress bar</span>+    <span class="choice-desc">Fills over the operation's usual duration, so you can see how+      far along it is.</span>+  </label>++  <label class="choice" data-selected={mode === 'flash'}>+    <input type="radio" name="taskbarActivity" value="flash" checked={mode === 'flash'}>+    <img src="/assets/shots/taskbar-flash.png" width="240" height="72"+         alt="The same taskbar button highlighted solid orange">+    <span class="choice-name">Orange flash</span>+    <span class="choice-desc">Highlights the button until you look at it. Tells you something+      happened, not how long is left.</span>+  </label>++  <label class="choice" data-selected={mode === 'off'}>+    <input type="radio" name="taskbarActivity" value="off" checked={mode === 'off'}>+    <img src="/assets/shots/taskbar-off.png" width="240" height="72"+         alt="The same taskbar button with no highlight of any kind">+    <span class="choice-name">Off</span>+    <span class="choice-desc">The taskbar button never changes.</span>+  </label>+</fieldset>+```++```css+.choice-set { border: none; margin: 0; padding: 0;+              display: flex; flex-wrap: wrap; gap: 0.75rem; }+.choice {+  flex: 1 1 240px;+  display: flex; flex-direction: column; gap: 0.35rem;+  padding: 0.6rem; cursor: pointer;+  background: var(--surface); border: 1px solid var(--border); border-radius: 8px;+  transition: border-color .15s ease, background .15s ease, transform .06s ease;+}+.choice input { position: absolute; opacity: 0; pointer-events: none; }+.choice img   { display: block; width: 100%; height: auto; border-radius: 4px;+                background: var(--bg); }+.choice-name  { font-family: var(--brand-heading); font-size: 0.875rem; font-weight: 600; }+.choice-desc  { font-family: var(--brand-body); font-size: 0.75rem; color: var(--text-2);+                line-height: 1.5; }+.choice:hover { border-color: var(--accent); background: var(--elevated); }+.choice:active{ transform: translateY(1px) scale(.995); }+.choice[data-selected='true'] {+  border-color: var(--accent);+  box-shadow: inset 0 0 0 1px var(--accent);      /* the ring IS the selection */+}+.choice:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }+```++The rules:++- **Every option gets its own screenshot, captured from the real feature.** All of the+  never-fabricate rules above apply with full force here: the user is choosing BETWEEN+  pictures, so an invented one directly causes a wrong choice.+- **Identical crop, zoom, and surroundings across the set.** Capture the same window, the same+  button, the same size, at the same moment in the operation. The only thing that may differ+  between the images is the option itself. A set shot at different zooms is unreadable as a+  comparison.+- **Include the "off" or "none" option as a card with a screenshot too**, showing the plain+  state. Users need to see what nothing looks like.+- **The set is a `radiogroup`** with real radio inputs, so keyboard and screen readers work.+  Hide the input visually, never with `display: none` (that removes it from the tab order).+- **Selection is a teal ring**, not just a tint: an unringed "selected" card is invisible on a+  dense surface.+- **Name the tradeoff in one line each**, not just the mechanism. "Tells you something happened,+  not how long is left" is what actually decides it.+- **Two to four options, side by side.** Past four, this becomes a list and you should reconsider+  whether they are really alternatives.++If the alternatives are NOT visual (a polling interval, a log level), skip the cards: a plain+enum control with a Default hint is right, and inventing screenshots for it is exactly the+failure mode this section forbids.+ ## Saving: local-first, optimistic, bounded  Every rule here came from a real bug, and every one of them is invisible until it bites:@@ -673,6 +806,8 @@ Every rule here came from a real bug, and every one of them is invisible until i - [ ] Row names are breadcrumbs with a teal last part, full key in `title` - [ ] 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+- [ ] 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