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.
Inspecting
- Inspect the LIVE rendered node, not the registry template. A wireframe is cloned: the
velt-*-wireframecustom-element tags are the hidden registry copy (0-size, empty).document.querySelector('.vc-x')may hit that copy. Always pick the element withgetBoundingClientRect().width > 0(the visible clone) and read its classes/computed styles. Measuring the wrong node is how you “verify” something that’s actually broken.
Wireframe clone behavior
- Some slots OVERWRITE their inner markup with their own label.
ToggleReplyreplaced custom<svg/> + <span>Reply</span>with a plain “Reply” text node;CopyLinkdid the same. Two verified fixes: (a) inject the icon via CSS::before(a data-URI SVG on your.vc-*class), which survives the clone; or (b) put the custom icon in a plain sibling wrapper next to the slot’s.Textsubslot, never inside the slot itself. If you use a different reply slot with nested children, live-verify the children survive adoption before certifying, overwrite behavior is per-slot. - Wireframe MARKUP changes need a FULL page reload to take effect, not just new wireframes. CSS edits hot-reload fine, but the
<VeltWireframe>registry is built at mount: changing an existing template’s markup (e.g. switching the composer’s send to a self-closingActionButton, or swapping Cancel to aVeltButtonWireframe) re-renders under Fast Refresh but does NOT re-register the template: the browser keeps rendering the OLD wireframe, so your fix “doesn’t work” until a hard reload. After ANY*Wf.tsxchange: hard-reload (Cmd-Shift-R), re-auth, reopen, and verify in a freshly-loaded tab, never a hot-reloaded one. (This masked a correct composer-submit fix as “still broken”.) - Container slots drop undeclared children: declare the full child tree you intend to use.
velt-if/velt-classattributes on plain HTML elements NEVER fire (R28). Avelt-if="…"on a<div>/<span>(or a helper spread like{...veltIf("…")}) survives the clone as inert markup, the element renders unconditionally, the class never toggles, nothing errors. Directives resolve only on Velt elements. Gate custom HTML by wrapping it in<VeltIf condition="{…}">; toggle classes viavelt-classon a Velt wireframe element or key CSS off Velt’s own state classes (e.g..velt-composer-open, the submit button’s:disabled).
Styling / scoping
- Class CSS needs shadow off +
!important(R6/R9b). WithshadowDom={false}the live classes are reachable; Velt’s runtime CSS is high-specificity, so overrides need!important. - The page-mode composer renders the WHOLE dialog wireframe (your
.vc-cardand all). It inherits the card chrome (border/shadow/resolve-icon) and crushes the input. Fix: scope the card chrome off in that context:.velt-comment-dialog--page-mode-composer .vc-card { border:none; box-shadow:none; padding:0; background:transparent }, leaving just the composer pill. (Alternatively give the page-mode composer its own variant viapageModeComposerVariant.) - Composer “active” state = the
.velt-composer-openancestor class (focus/compose), not a “has-text” class (there is none). Style the send button: grey/disabled by default, dark/enabled under.velt-composer-open. - Avatar fill color is user-data-driven, not CSS: “User 1” renders peach. To match a design that shows a flat dark avatar, override
.…s-user-avatar-initial-container { background }+ the initial color (this overrides per-user colors: a deliberate choice to flag). - The send-arrow indigo lives on an inner element (
.velt-composer--input-button), not the outer.velt-composer--submit-button: override the inner one.
Composer actions, collapsed replies, resolved state
- Cancel button: use
VeltButtonWireframe, never a raw<button>. The dialog/reply composer has no native Cancel slot.- Render Cancel as a
VeltButtonWireframe(a Velt-owned button) and wire it in the host viauseVeltEventCallback("veltButtonClick")to clear and collapse the composer. - A raw
<button onClick>in wireframe markup does not run because it is cloned to plain DOM (R4). It breaks specifically in the in-thread reply composer: page mode can look fine while the reply composer is dead. - The host handler must scope to both composers, but
commentAnnotationdoes not distinguish them because both carry one. The real distinguisher is the annotation’s comment count: the page-mode composer fires with a fresh draft (commentAnnotation.comments.length === 0); a reply composer fires with the existing thread (comments.length > 0). Route the clear accordingly. - Key the Send button’s enabled/dark state off the submit button’s
:disabledattribute rather than only.velt-composer-open; it tracks empty-vs-filled exactly.
- Render Cancel as a
- Send button → leave
Composer.ActionButtonSELF-CLOSING; paint the arrow with CSS. Injecting a child (<svg>/<span>) intoActionButtonis dropped by the clone and can kill the native submit in the reply composer. Leave it empty so Velt renders the functional submit, then mask the up-arrow via CSS (::after/maskon the live button class: grey idle → dark under.velt-composer-open). - “Show N replies…” (MoreReply): the clone DROPS trailing text, so add the ellipsis in CSS. Velt renders the text in
.velt-hidden-count; the cloner drops every node after the last velt element, so the trailing…is gone.- Re-add it via
::after { content:'…' }onvelt-comment-dialog-more-reply-text-internal. - Gate the row on
:has(.velt-hidden-count)(NOT:not(:empty)) so a fully-expanded thread (empty MoreReply) doesn’t show a lone chevron. - Indent to the message column and draw the rail-line continuation (a 1px line in the avatar gutter joining the chevron to the next avatar); suppress Velt’s default full-width divider.
- Collapse the EMPTY slots: a single comment / fully-expanded thread renders an empty
velt-comment-dialog-more-reply-internalAND emptyvelt-comment-dialog-toggle-reply-internalthat still take vertical space inside the card border, leaving dead space below “Reply”. Addvelt-comment-dialog-more-reply-internal:not(:has(.velt-hidden-count)), velt-comment-dialog-toggle-reply-internal:not(:has(…count…:not(:empty))) { display:none !important }so only a populated “Show N replies” row occupies space (on open AND resolved cards).
- Re-add it via
- Resolved card = muted AND no reply. Per the design a resolved comment hides its reply affordance. Velt adds no
--resolvedclass: detect resolved via the rendered unresolve button (.velt-comment-dialog:has(velt-comment-dialog-unresolve-button-internal .vc-icon-btn)), then mute colours/avatar ANDdisplay:nonethe reply +velt-comment-dialog-toggle-reply-internal+ any reply composer. Leaving them shows empty bottom space.
Interaction-transition traps (the surface moves DURING a click) (R27)
These don’t show up in a static per-state capture; they only bite mid-interaction. The whole class: a layout/visibility decision keyed on a state that changes at the worst moment.- Send/Cancel “shifts down and never fires”, visibility keyed on a TRANSIENT state. The reply composer’s “Reply” link was hidden via
.velt-composer-input-focused(focus). Focus drops the instant the pointer leaves the input to press a button → the rule re-shows the “Reply” link → everything below shoves down by a line → the composer (Send/Cancel) jumps out from under the cursor, so the click lands on empty air. Symptom reads as “the button doesn’t work”; the real bug is it moved. Fix: anchor on a STABLE state that holds through the whole interaction, hide the link via.velt-comment-dialog--selected(the card is open the entire time you click) instead of…input-focused. Same visual result, zero flicker, the target never moves. Never key hide/show or re-layout on:focus/.velt-composer-input-focused/:hover/:active, they flip on pointer-down. (Send-button enabled/dark may key off the submit’s:disabled, that tracks empty-vs-filled, not pointer position, so it’s safe.) Prove it: measure the target’s box, blur the input (what the click does), re-measure → must be 0px, then do the real thing, type a reply, real-click Send, confirm it posts. - Flex
gapreserves space for a 0-height/empty child → trailing whitespace below the card. A card laid out asdisplay:flex; flex-direction:column; gap:16pxadds the 16px gap even when the next child (the collapsed reply composer / empty more-reply host) is 0px tall but still present. Agapcan’t tell “invisible” from “absent”. Fix:gap:0on the column and move the spacing tomargin-topon the child that should be spaced, a margin only takes effect when its element actually has height. Verify in the collapsed/empty-composer state by measuring body-bottom→card-bottom = 0 extra px, not just the populated state. - Gutter-pinned glyphs must be PINNED, not inline. The “Show N replies” chevron belongs in the avatar gutter under the avatar (design), but drawn inline it flows next to the text and pushes right. Pin it:
position:absolute; left:4pxagainst the card’s positioned ancestor, and draw it as a thin glyph via a CSSmask(a data-URI SVG), not a heavier default.
Dropdowns / selected state
- The minimal-filter
SelectedIconslot renders as ONE standalone, full-width,opacity:0element: it does NOT auto-place a tick per row. Fix: hide that slot and put a ✓ on the actually-selected row via CSS:.…content-item--selected .vc-filter-item::after { content:"✓"; margin-left:auto }. (Gating withVeltIf {isSelected}does NOT work in this V1 sort/filter context: it resolves falsy.) - A right-edge-anchored dropdown can overflow the viewport (the trigger sits at the sidebar’s right edge). The content’s default
left/rightmay push a wide menu off-screen, clipping the tick. Fix: shift the menu into view (e.g.transform: translateX(-Npx)on the menu, or right-anchor it) so it opens leftward.
Interaction driving (when verifying in the browser)
- Velt triggers need a real pointer click;
element.click()(JS) often won’t fire the Angular handler. Use a real click at the element’s coordinates. - Velt auth or
documentsReadycan stall after reloads (useCurrentUserdoesn’t emit, so nothing mounts and thevelt-*count is 0). This is an environment block, not a build failure. Wait longer for the mount, recover with a fresh tab, or re-authenticate in the app. Triage app vs. build before blaming the customization.
Brief/drive selectors must survive the BUILD (post-build stability)
Probe briefs and drive steps are authored at plan time, against the app’s pre-build DOM, but the wireframe registration replaces that DOM. A selector keyed to default Velt markup or host structure that the wireframe supersedes matches nothing the moment the first registration renders, and every downstream drive/measure dies on a selector that “worked when I verified it.” Key every brief/drive selector to something that survives the build:- a contract wireframe tag (
velt-*-wireframe, the manifestcontract.partsselectorHints exist precisely for this), - a stable
velt-*runtime class (.velt-comment-dialog--sidebar-mode,.velt-composer--submit-button, …, verified inCSS classes), - or your own
.vc-*classes from the first-shot stylesheet (you control that markup, so those classes are guaranteed post-build).
:has-text("…")) are fine only on top of a stable base selector, fixture text survives; structure doesn’t.
Wireframe-host semantics FLIP at registration (selectors AND CSS)
The moment a family’s wireframe registers, the live DOM’s shape changes, not just its content:- The live card itself starts carrying
--wireframe-host, and the live inner elements render as*-internaltags (e.g.velt-comment-dialog-header-internal), not the tag names you saw pre-registration. - The hidden 0-size registry twins disappear, the pre-registration DOM had every wireframe tag twice (a hidden registry copy under
<velt-wireframe>plus the live clone); post-registration there is one live tree. - Consequently any
:not(--wireframe-host)selector written pre-registration silently stops matching, it was excluding the live element’s own new class. The same applies to selectors assuming the twin structure (velt-wireframe > *), and to drive/probe waits keyed to a pre-registration tag.
*-internal live tags, stable velt-* classes, or your own .vc-* classes, and after the first registration of each family, re-verify any selector written before it.
