Skip to main content

validate-ui

Drive Color Lab's real UI in a browser and verify a feature area actually works — routes, controls, selectors, and the exact observable that proves each step. Use this whenever the user asks to validate, check, verify, exercise, smoke-test, or "click through" any part of the app, and whenever they name a feature area to validate ("validate scale", "validate groups", "check the preview still works", "does export still work?", "make sure saving a palette works"). Also use before shipping a UI change, when reproducing a reported UI bug, or any time you are about to explore the app in a browser to find out how it works — that exploration is what this skill exists to replace. Do NOT use for `pnpm validate` (typecheck + lint + tests) — that is a different thing entirely.

Informações da origem

Repositório
gilbarbara/color-lab
Última atividade na origem
15 de julho de 2026 às 18:04
Idioma detectado do SKILL.md
inglês
Estrelas
2
Forks
1

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
12 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
validate-ui
description
Drive Color Lab's real UI in a browser and verify a feature area actually works — routes, controls, selectors, and the exact observable that proves each step. Use this whenever the user asks to validate, check, verify, exercise, smoke-test, or "click through" any part of the app, and whenever they name a feature area to validate ("validate scale", "validate groups", "check the preview still works", "does export still work?", "make sure saving a palette works"). Also use before shipping a UI change, when reproducing a reported UI bug, or any time you are about to explore the app in a browser to find out how it works — that exploration is what this skill exists to replace. Do NOT use for `pnpm validate` (typecheck + lint + tests) — that is a different thing entirely.
# Validate the Color Lab UI Drive the real app as a user and prove a feature works. Assert between actions — never chain two on an assumption; the loop below is why. Read this file, then read **exactly one** `references/<area>.md` for the area you're validating. Don't load them all. ## The loop ``` 1. act — one command 2. observe — snapshot (or get the specific observable) 3. assert — the thing you expected to change actually changed 4. repeat ``` You cannot see the page. You are working from a mental model, and **the model is wrong the moment you act.** So you re-read after every action — not when something looks broken, because it will not look broken. ### `✓ Done` is not a result `agent-browser` prints `✓ Done` when the **command ran**. It says nothing about whether it *did* anything. Every trap in this file is an instance of that: | Command | Prints | Actually | |---|---|---| | `fill` on a slider | `✓ Done` | value jumped to the range's **midpoint** | | `type` / `keyboard type` / `select` on a slider | `✓ Done` | **nothing at all** | | `focus` on a control in a collapsed panel | `✓ Done` | focus went nowhere (`inert`) — every later `press` is swallowed | | clicking a **`[disabled]`** button | `✓ Done` | **nothing at all** — and the unchanged state reads as a real result | | `find role button click --name "Save"` | `✓ Done` | hit `Save palette`, not the modal's `Save` | | clicking a toggle whose state was **persisted** open | `✓ Done` | you just **closed** it | None of these error. All of them produce a green, confident, wrong result. The only defence is to look. **So:** never chain two actions on an assumption. After a click that opens something, assert it opened. After a click that toggles something, assert which way it went. Re-`snapshot` before reusing any `@ref` — refs die on every re-render. A **covered** control is not one of these traps: any click on one **errors** and names the covering element (`✗ covered by <header.fixed.top-0>`). That's a signal, not a failure — read it and clear the coverer. Prefer `click @ref` over `find role … click` anyway: `find` matches by **substring**, so a name that looks unique often isn't — `--name "Save"` hits `Save palette`, and a bare `Select` matches `Select <Name>`, `Select variant` and `Select lock` at once. A `@ref` names exactly one. ## What the app is An OKLCH color-scale generator. You pick base colors; it generates a tonal scale (3–20 steps) for each. Everything else — curves, groups, presets, export, contrast tools — feeds or renders that. **The palette IS the URL.** Every structural edit rewrites the address bar; back/forward is undo/redo. This is why most assertions are URL assertions: if the URL didn't change, the edit didn't happen. Routes: `/` redirects to `/p`. `/p/<slug>` is the entire generator. `/palettes` is the saved list (auth-gated). `/about`, `/privacy`, `/terms`, `/oklch-vs-hsl`, `/custom-color-scales` are static. ## Boot The dev server is **`https://color-lab.localhost`** (portless proxy) — *not* `localhost:3000`. ```bash pnpm dev # if it isn't already up agent-browser open https://color-lab.localhost/p/Primary-73.0_0.23001_321 agent-browser set viewport 1440 900 agent-browser wait --load networkidle ``` Two seed palettes, from `e2e/__setup__/constants.ts`: - **single** — `/p/Primary-73.0_0.23001_321` - **palette** — `/p/Primary-60_0.21_150/Secondary-60_0.139_227/Tertiary-60_0.266_304` Each area file names the seed it wants. **The `single` seed rewrites itself on load. That is expected.** Plain precision rounding, in P3 and sRGB alike — `formatOklchUrl` (`src/utils/color.ts`) rounds L to 2 decimals and C to 3: ``` /p/Primary-73.0_0.23001_321 → /p/Primary-73_0.23_321 ^^^^ trailing zero ^^^^^^^ 0.23001 → 0.23 ``` Assert against `Primary-73_0.23_321`. The `palette` seed is canonical and stays put. Gamut has nothing to do with it — `e2e/basic.spec.ts:56` asserts the rewritten form *while forcing P3*. ### Step zero: read the opening state, don't assume it `appStore` persists to `localStorage` (key `color-lab`). A pre-paint script in `app/layout.tsx` stamps some of it onto `<html>` before React mounts. **The starting state is restorable, not fixed.** Persisted: `gamut`, `showSidebar`, `showPreview`, `showColorOptionsPanel`, `showPaletteOptionsPanel`, `view`, `colorSpacing`, `exportFormatType`, `exportColorFormat`. **`view`, `colorSpacing` and the two export settings are persisted but are *not* on `<html>`.** A reload restores whatever you last left — not List view, not Wide spacing, not the default export tabs. **A toggle you click may already be on, so your click turns it off.** Read the state first; if it isn't what the area file expects, set it explicitly or clear storage. ## The three observables Don't eyeball screenshots. Screenshots are for showing the human. **1. The URL — the palette state.** Colors are path segments (`/p/Name-L_C_H`); options are short query keys. Per-color overrides ride on the segment after a `-`, comma-separated (`Primary-60_0.21_150-f:1.2,k:400`). | Key | Means | | Key | Means | |---|---|---|---|---| | `i` | steps (3–20) | | `s` / `o` | saturation / apply-to-all | | `m` | mode `l`/`d`/`r` | | `k` | locked step | | `v` | variant | | `f` | lightness curve | | `c` | chroma curve | | `h` | hue shift | | `n` / `x` | min / max lightness | | `g` | group: `b`/`n`/`s`/`d` | Defaults are dropped from the URL entirely — so "reset" is asserted as *the param disappearing*, not as it returning to a value. **2. `<html>` — the app's chrome state.** Global layout flags with no accessible surface, so there is nothing in the tree to read instead: ```bash agent-browser eval "JSON.stringify(document.documentElement.dataset)" # {"gamut":"srgb","p3Supported":"false","sidebar":"open","preview":"open", # "colorOptions":"closed","paletteOptions":"closed"} ``` `data-gamut` (`p3`|`srgb`) · `data-sidebar` · `data-preview` · `data-color-options` · `data-palette-options` (each `open`|`closed`) · `data-p3-supported`. **`dataset` camelCases them** — `dataset.colorOptions`, not `dataset['data-color-options']`. Dark mode is `class="dark"`, not a data attribute. **Three different surfaces say "color options" — keep them apart.** `data-color-options` tracks *only* the global **Advanced Options** panel (`showColorOptionsPanel`). The per-color **Change color options** Collapse has no chrome flag — it's proven open by its inputs being mounted. Steps/variant/lock live under `data-palette-options`. Headless Chrome reports `p3Supported: "false"`, so the app opens in `gamut: srgb` — the environment, not a bug. It does mean you validate the sRGB path by default, which the e2e suite never exercises (it forces P3 on). **3. ARIA — element state.** `aria-current` on the active `ColorItem` · `aria-pressed` on group chips · `aria-selected` on tabs · `aria-checked` on radios · `aria-expanded` on `Toggle Bottom Bar`. ```bash agent-browser snapshot -i # your default read agent-browser get count '[data-testid=Scale]' agent-browser get attr @e5 aria-pressed agent-browser get url ``` ## The `eval` rule **Read state through the accessibility tree, not the DOM.** `eval` has exactly three legitimate uses: 1. Reading `<html>`'s `data-*` chrome state (observable 2 — it has no ARIA surface by design). 2. Patching `navigator.clipboard` (clipboard reads are blocked). 3. **`setRange`** — setting a channel slider's value, because agent-browser genuinely cannot (see Sliders). Everything else — element counts, `aria-*`, text, tab selection, panel contents — comes from `snapshot` / `get`. > **Reaching for `eval` to prove a control's state — `className`, `getComputedStyle`, `getBoundingClientRect`, a bare `data-*` — means stop.** That control has no accessible state, and that is an accessibility bug. Report it. Class-sniffing hides the defect and trains the next agent to hide it too. The distinction that matters: `eval` is banned as a *substitute for looking*. It is correct as an *implementation of an action agent-browser can't perform*. `setRange` is the second; it is not a licence for the first. **`eval` reuses one JS context**, so a bare `const x = …` throws `Identifier 'x' has already been declared` on the second call. Wrap every body in an IIFE. ## Sliders Every slider is an `<input type="range">`, but they come from two libraries that **commit on different events**. That, and only that, is why they differ. | Slider | Where | Commits on | Drive with | |---|---|---|---| | **Channel** — `Lightness`, `Chroma`, `Hue` | sidebar `ColorItem` | `input` | **`setRange`** | | **Option** — steps, saturation, every curve | Palette Options, Advanced Options, per-color overrides | `onChangeEnd` | **`focus` + `press`** | Both land exactly, in one step. **Never `fill` either.** ### Option sliders → `focus` + `press` HeroUI/react-aria. They commit on `onChangeEnd`, which only real input fires — a synthetic value change does nothing, so **`setRange` will not move the URL here.** e2e drives them this way and explains why at `e2e/scale.spec.ts:44-48`. ```bash agent-browser focus 'input[name="lightnessCurve"]' agent-browser press PageDown # 1.3 → 1.2, URL gains ?f=1.2 ``` `press` takes a **key, not a selector** — it acts on whatever is focused, so every keyboard step is two commands. Focus persists across presses. One press = one tick: PageUp/PageDown = 0.1 on the curve sliders, 10 on `hueShift`; arrows = 1 on `steps`; `Home`/`End` = min/max. **Assert the panel is open before you `focus`.** A collapsed panel's content is `inert`, and an `inert` input cannot take focus — `focus` still prints `✓ Done`, and every `press` is silently swallowed. *How* you assert open depends on the panel: **Advanced Options** → `data-color-options` (it flips in lockstep with `inert`); **Palette Options** → `data-palette-options`; a **per-color** panel → its inputs being present (it unmounts them when closed — see `scale.md`). Don't read `data-color-options` for the per-color case; it reports the global panel. ### Channel sliders → `setRange` **agent-browser cannot set a range input's value.** `fill` is broken (below); `type`, `keyboard type`, `keyboard inserttext` and `select` are silent no-ops. Keyboard *works* but can't reach a target — the step is `0.001` on Lightness/Chroma and `0.01` on Hue (3,600 `PageUp`s to cross 0–360). Don't. So `setRange` is the tool. It is not a trick: it is *precisely* the branch Playwright's `fill()` takes for range inputs (`kInputTypesToSetValue = {color, date, time, datetime-local, month, range, week}` → set the value, dispatch `input` + `change`, never clear). Same operation `e2e/basic.spec.ts` performs. ```bash cat <<'EOF' | agent-browser eval --stdin (() => { const el = document.querySelector('input[type=range][aria-label="Hue"]'); const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set; set.call(el, '150'); el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); })() EOF ``` The *native* setter is required: React patches the instance's `value` setter to track changes, so a plain `el.value = …` is swallowed and `onChange` never fires. > **Assert on the URL, never on `el.value`.** The picker commits through `requestAnimationFrame` while React synchronously restores the controlled value, so reading it back in the same `eval` returns the **old** value even though the edit landed. Use `wait --url "**/*_150"`. Channel sliders are **1px × 1px** and clipped (the visible track is the parent `div`). So `find role slider --name "Hue"` **fails to resolve them**, even though they *do* appear as `slider` in a `snapshot`. Use `@ref` or `input[type=range][aria-label="Hue"]`. ### Never `fill` a slider `fill` is "clear the field, then fill it" — and **the clear alone destroys the value**. Per the HTML spec, a range input set to `''` resets to its default, `min + (max-min)/2`: ``` fill Hue "150" (min 0, max 360) → 180 ← midpoint, not 150 fill lightnessCurve "2.0" (min 0.1, max 5) → 2.55 ← midpoint, not 2.0 ``` An **agent-browser bug, not app behavior** (Playwright fills these fine — hence `setRange`). Pick a target near the midpoint and you get a green assertion that proves nothing. ## URL writes are not debounced **There is no debounce.** `useUrlSync` calls `pushState` synchronously, so after a discrete edit (`Add Color`, a menu selection, a name blur) `get url` already returns the new URL — no wait needed. The one thing that defers a write is a **slider gesture**: while any element carries `data-interacting="true"`, `useUrlSync` pauses and flushes exactly one entry on release (the invariant that stops a drag flooding the back button). Prefer `wait --url "**/<expected>"` over a fixed sleep — it costs nothing when the URL is already right. But don't wait on a timeout that doesn't exist: if the URL never arrives, something is genuinely wrong and waiting longer won't fix it. ## Selectors **Precedence:** role + accessible name → `data-testid` → `input[name=…]`. Every selector in the area files is one the e2e suite drives, so a rename breaks CI before it breaks this skill. Prefer them over anything you invent. **Per-color controls carry a `for <Name>` suffix the area files omit.** The real names are `View Color Info for Primary`, `Change color options for Primary`, `Color group for Primary`, `Export scale for Primary`, `Random color for Primary`. The area files name the *stem*, and substring matching makes them work — but **an exact-name lookup will fail**, and substring matching is also how `--name "Save"` hits the wrong button. When a stem doesn't resolve, snapshot and read the real name. ## Reading the clipboard `navigator.clipboard.readText()` throws `NotAllowedError` here. Two options:
Ver no GitHub
Este SKILL.md e muito grande, entao o SkillsMP mostra aqui apenas a primeira secao. Ver no GitHub