Skip to main content
Audience: AI coding agents (and teams running them). This page is part of the deterministic agent workflow for building and verifying Velt UI customizations. Customizing by hand? Start with the Approaches and the feature overviews. This is the workflow the UI Customization Plugin runs for you.
A customization is done when four things are true, in this order: it matches the design, Velt’s behavior is still intact, the code is rule-compliant, and its family’s real paths work (R30). This page is the step-ordered flow for confirming all four on one surface: the executable companion to R15 (verify after each surface) and R16 (build by family, verify per block).
Golden rule of verification: measure the style of every element, not the screenshot of the surface, and name every difference. The design is dummy data; the live app has real data (2 comments vs 11, placeholder text vs real messages), so they never match pixel-for-pixel. A whole-surface pixel comparison against the mock is not a valid gate. Fidelity is a content-independent, per-element problem: verify the look of each template element, true whether the app shows 2 cards or 200.Check these and require all to be clean: (1) style, rendered computed styles vs the designSpec (non-structural lengths ±2px, colour CIEDE2000 ΔE < 2, keywords exact); (2) layout, each element’s surface-relative box, sibling gaps (especially the inter-card gap), and relations, plus missing/extra elements. The checklist is auto-derived from every mapped element, and a thin spec is INCOMPLETE, not a pass. (3) A visual side-by-side is advisory, it flags regions worth a look, but a pixel region is only a defect once you can name it as a style or layout row. Pure content differences (comment count, text, names, timestamps) are never a FAIL. There is no aggregate score, and “looks close” is a FAIL. The single mechanical exception is R29: sparse sub-pixel glyph residue with verified-identical asset identity.Collect evidence first, then decide the verdict. A build step or runtime check should not declare a surface matched without the full design, behavior, rules, and real-path evidence.
Use the same standard whether verification is automated or manual: cover every mapped element, every required state, and every behavior contract before marking the surface done.
Run this flow per surface, finish it, then move to the next surface. Don’t batch (R16).

What you need first (preconditions)

You can’t verify what isn’t rendering. Before anything else:
  1. Velt is initialized. The app boots, the user is identified, documents are set, and the default version of this surface renders. Gate logic on useVeltInitState() if needed. (See Debugging.)
  2. The surface has data. Create a comment / open a thread / trigger a notification through the app’s own UI so the surface has something to show. If you genuinely can’t seed data, verify the empty / loading state only and record that limitation in the report.
  3. You have the design reference for this surface (the Figma frame / screenshot) and know which states it specifies.
If the app won’t build or run, the visual and behavior checks can’t be performed. The surface is BLOCKED: it is neither a pass nor a fail, and the design match is simply unverified. You can still run the static rules scan (step 4) on the produced code. Do not fake a pass.

The flow

Step 1: Drive every state the design specifies

For each visual goal, render the surface in each state the design covers and capture it. The states that matter per surface are in the matrix below. Common ones:
  • default, hover, selected/focused
  • empty, loading / skeleton, filtered-to-zero
  • unread, resolved (and OPEN / IN_PROGRESS / RESOLVED if the design themes by status)
  • long content (truncation), private (if shown)
Capture each state as evidence. A visual goal with states ["default","resolved"] is only checkable once you’ve driven both.

Step 2: Whole-surface measured check + visual gate (match the design)

For each state, do all four: this is the gate:
  1. Consume or assemble the complete checklist (R26: do not hand-pick samples). Build the checklist from every distinct styled appearance in the design (deduped by its declarations: the teal mention, the placeholder, each filter row), every mustSupply slot, every mount-map part, and every required state. Resolve each element’s live selector by inspection: use the manifest cssClasses for measured leaves and inspect to the leaf for the rest. Add relations + gaps from the manifest layout. Produce a disposition for every checklist element plus the per-state visual artifact. A report that samples fewer elements than the checklist is INCOMPLETE, not PASS, so the loop cannot end on it.
  2. Gross-mismatch pre-check first: compare total content height / element count / surface extents vs the designSpec. Grossly off ⇒ FAIL immediately (don’t let per-element props “pass” on a broken surface).
  3. Measure live browser output: read each live node’s getComputedStyle plus surface-relative getBoundingClientRect (never the 0-size *-wireframe template) and produce a delta table with a verdict covering style (Delta E below 2, ±1px, keywords exact) and layout (box ±2-3px, gaps, relations, missing or extra elements).
  4. Visual side-by-side: advisory, not a gate: capture the full-surface screenshot beside the Figma frame to spot regions worth investigating. It does not FAIL on its own, the live app’s real data differs from the mock’s dummy data, so raw pixel regions and content differences are expected, not defects. For any region that reflects a genuine style issue, find the property/relation you didn’t measure, add it to the delta spec, and let that gate it. A clean delta table plus an obviously-wrong screenshot means you’re missing a style/layout assertion, add it; never fail on the pixels alone.
Hard gates: any console error / unbuilt page / mapped element with width===0BLOCKED/FAIL. Every mustSupply slot must be present and carry the design’s content: an icon slot must contain the design’s exported SVG (compare identity), not a Velt default or hand-drawn glyph (R17 FAIL). A popup must be styled on its content slot, never its container/trigger (R23). Horizontal padding must not compound across nested wrappers (R22). No feature/prop whose UI the design doesn’t show (R24). Colours still must trace to a --velt-* token / documented class (an accidentally-matching hard-coded colour breaks in dark mode: fail it even if ΔE passes). The delta table’s failing rows ARE the feedback (advisory pixel regions only matter once named as a delta row). Mark the goal met only when, for every state, the gross check is clean and the style + layout tables are empty, the spec having actually covered every visible slot + the inter-card gap (a thin spec is INCOMPLETE, not a pass), and content differences excluded; otherwise not met, listing each diff.

Step 3: Behavior check (Velt still works), including interaction stability

Customizing presentation must never break Velt’s behavior: you never disabled it, so it must still work (R0, R7). Two halves, both required: 3a: the action functions, end-to-end. Perform the surface’s real actions (in the matrix) and confirm each still functions through to its outcome: place a pin, open a dialog, type a reply and click Send and confirm it posts, change status, filter, sidebar sync; open a notifications panel, switch tab, mark read, click through. Perform the action by a real on-screen click at the visible element’s box (a JS .click() often won’t fire Velt’s handler). If an action is dead, something was hidden with CSS instead of a prop (R7), interactivity was put in wireframe markup (R4), or a slot was dropped: fix it; it is not a “design gap.” One passing path is not proof, drive the exact reported interaction, not a convenient neighbour. 3b: the target doesn’t MOVE mid-interaction (R27). A static per-state capture proves the surface looks right while it sits still; it does not prove it holds still during a click. The failure: a visibility/layout rule keyed on a transient state (:focus/:hover/:active, or a Velt twin like velt-composer-input-focused) flips at the instant of the click, the element loses focus, a hidden piece re-appears, the control shifts out from under the cursor, the click misses. On every interactive affordance the surface renders: record its box, drop the transient state the click would drop (blur the focused element), reflow, re-measure. Any shift > 1px ⇒ FAIL, re-anchor the rule on a stable, persistent state (e.g. velt-composer-open). Passing the static capture does not exempt this gate.

Step 3c: Family real-path smoke suite (R30), after the family’s blocks pass

Fixture-green is not done: a run can pass every seeded-fixture block while real interaction paths are broken. Once a family’s blocks are clean, run its real-path suite:
  • a short message AND a max-length message (never only the canonical fixture text);
  • the surface in every dialog context it appears in (sidebar card / popover open-dialog / hover preview, shared classes leak across contexts);
  • every affordance clicked once (reply, resolve, edit, options), asserting the outcome and no dead band;
  • one viewport resize; zero console errors throughout.
A missing or failing smoke suite means the family is not done.

Step 4: Rules-compliance scan (static, on the produced code)

Walk the Quick gate in Rules against what you wrote. The checks that catch the most:
  • R0: no hacks: no setTimeout / MutationObserver on Velt internals, no scraped internal markup, no timing/DOM shims.
  • R1 / R2: exactly one <VeltWireframe>; the live feature component is mounted.
  • R4 / R5: no onClick / useState / hooks inside wireframe markup; UI-library components wrap around primitives, not inside wireframes.
  • R6 / R7 / R8 / R9: selector CSS only with shadow off or injectCustomCss; no display:none to remove features; one stylesheet; dark values scoped to :root[data-velt-theme="dark"].
  • R10: every identifier (slot, prop, variable, class, hook, API) verified against reference/. If it isn’t there, it doesn’t exist.
  • R11 / R16: files under components/velt/ui-customization/; only this one surface touched this step.
  • Verified gotchas: ThreadCard nested in Body → Threads; container slots declare their full child tree; correct shadow root-vs-nested handling; pin index/number filled via velt-data; VeltCommentDialog, never the deprecated VeltCommentThread. (All in Wireframe components.)
A rule violation is a fail, even if the surface looks right: a patchy fix that “looks right” today breaks silently tomorrow (R0).

Step 5: Verdict

A goal is only allowed to be PARTIAL (not FAIL) once you’ve confirmed there is no clean supported path for it. Run the blocked / gap flow before downgrading a fail to a gap. Never convert a fixable miss into a “gap” to escape the loop.

Per-surface state & behavior matrix

The concrete “what to drive” for each v1 surface. Drive the states for the visual check (step 1-2); perform the behaviors for step 3. Verify only the states the design actually specifies, but never skip the behavior column.

Comments

Notifications

Also run the cross-cutting checks for any surface with custom markup: dark mode on/off, RTL, mobile width, keyboard navigation with visible focus, and working scroll behavior (R14). Full checklist: Cross-cutting concerns section Testing. Those checks are part of “matches the design,” not an afterthought.

After an SDK upgrade

Re-run the visual + rules checks. Variable-based theming (--velt-*) is upgrade-safe; class/selector overrides and wireframe slot names are the most drift-prone pieces, so re-verify them first (Cross-cutting concerns).