From antigravity-awesome-skills
Builds an interactive browser studio for tuning AI-generated output by eye — sliders/pickers for visual parameters, inline editing for text. Use instead of static grids or chat Q&A.
How this skill is triggered — by the user, by Claude, or both
Slash command
/antigravity-awesome-skills:lookdevThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use when the user says "lookdev", or asks to tune / dial in / iterate on the look of something, compare variations by feel, or review / edit / annotate a blog post, doc, copy, or media set. Use whenever "show me, I'll pick" beats asking the user to specify a number, and whenever you'd otherwise hand back a static grid or a wall of prose for review.
Use when the user says "lookdev", or asks to tune / dial in / iterate on the look of something, compare variations by feel, or review / edit / annotate a blog post, doc, copy, or media set. Use whenever "show me, I'll pick" beats asking the user to specify a number, and whenever you'd otherwise hand back a static grid or a wall of prose for review.
Source: connerkward/lookdev-studio-skill (MIT).
When the user says "lookdev" — or any of: tune, dial in, iterate on the look of, compare variations of, let me adjust, let me edit/annotate/mark up, review this post/doc/copy — they mean build an interactive in-browser tool the user directly manipulates. Not a static grid of N variations. Not a Q&A where they specify numbers. Not a wall of prose they're asked to read and reply to in chat. A real-time studio where they act on the artifact and the change is captured.
Two studio shapes — pick by what's being tuned:
Any visual decision the user picks by feel, not by spec. Expand this list as needed:
If the studio shows a list, grid, or scroll-long set of variations, controls must be visible from every scroll position. The user has to be able to drag a slider while looking at row 14, not scroll back to the top each time.
Two approaches, pick by layout:
position: sticky; top: 0) at the top of the scroll container. Keep the bar visually distinct — paper background + blur backdrop + bottom border — so it doesn't muddy the specimens scrolling behind it. Sticky pins relative to the nearest scrolling ancestor with a defined boundary; if you nest it inside a sized parent (a <header> with margin-bottom, a <div> with a fixed height), it stops sticking at that parent's bottom edge. Lift it to be a direct child of <body> (or the page-wrap) so stickiness spans the whole page.position: fixed) for hotkey-toggled controls — e.g. press d to reveal. The portfolio's .debug-ctl pattern is this: pinned top-left, transparent until summoned. Use when the controls shouldn't occupy permanent screen real estate (final viewers shouldn't see them; the author can summon on demand).Anti-pattern: a top-of-page control panel that the user scrolls past and never sees again. They will tune blindly, give up, or guess. Either keep the controls in view or duplicate a compact control bar next to each variation row.
When the artifact is a blog post, doc, copy deck, script, or media set, the user is not turning knobs — they're marking up the work the way an editor marks a manuscript. The studio renders the real artifact WYSIWYG (the actual rendered blog with its real components/media, not a raw-markdown textarea) and lets the user act on it directly. Building this for a doc review is mandatory: do not paste a long file into chat and ask "what do you think?" — that's the boring wall of text the user is rejecting. Stand up the annotation studio and let them edit in place.
contentEditable per block (or click-to-swap-to-<textarea>), each block carrying a stable data-block-id that maps back to a source location (markdown/MDX line range, JSX node, or content key). Capture the edited text per block; the agent applies the diff to source. Don't make them retype in a separate field — they edit the rendered sentence.<mark>). Multiple colors = a legend the user defines (e.g. yellow "cut this", green "love it", red "wrong/fact-check"). Each highlight stores {blockId, startOffset, endOffset, color, optional note}.{anchor, text} where anchor is a block+range or a media region. This is how the user says "diagram goes here", "too long, cut to two sentences", "needs a real screenshot".{mediaId, x, y, w, h, note}); plus a per-media flag menu — "replace", "wrong model", "regenerate", "missing — generate one here". Placeholders ("DIAGRAM HERE", "MEDIA?") render as visible drop-zones the user clicks to specify what they want, directly addressing "where are the diagrams / where is the media."The studio is worthless if the agent can't read the markup back out. Every edit, highlight, comment, and media-flag must export as one machine-readable patch with a single Copy button (and persist to localStorage/URL so a refresh doesn't lose work — this is human-labeled data; see human-labeled-data-rule). Shape:
{
"edits": [{ "blockId": "p-12", "text": "new rewritten text" }],
"highlights": [{ "blockId": "p-3", "range": [40, 88], "color": "cut", "note": "boring, drop" }],
"comments": [{ "anchor": "p-7", "text": "diagram goes here — flow of the save loop" }],
"media": [{ "mediaId": "fig-2", "flag": "replace", "note": "use a real screenshot, not ASCII" }]
}
The agent ingests this and bakes: applies the inline edits to the source file, acts on every comment/flag, swaps/generates the flagged media, resolves the highlights (cut the "cut" spans, etc.). Then re-serve the updated artifact for another pass. No markup may exist that isn't in the export blob — otherwise you're back to the user narrating changes by hand.
Selection/Range API; store character offsets relative to the block's text content (not DOM node paths, which break on re-render). Re-apply highlights/comments on load by walking each block's text to the stored offsets.h), comment on c.Pick controls by what the decision actually is.
| Decision type | Control |
|---|---|
| Continuous value (intensity, size, opacity, k) | <input type=range> paired with an editable <input type=number> (not a static label) — drag OR click-and-type; they two-way sync |
| Discrete choice (mode, blend, easing kind) | segmented buttons or radio chips |
| Color | <input type=color> swatches; pre-extract dominant palette with coverage % when relevant |
| Position / size on a canvas | drag the element itself — handles, not numeric inputs |
| Crop region | draggable rectangle + aspect-lock toggle |
| Multiple discrete states | render each in a labeled card on one page |
| Font choice | searchable picker + editable sample-text input |
Spatial rule: if the user could point at the thing and drag it, that is the control. Don't add an x: slider when a drag handle is the obvious affordance.
Gesture capture — never make the gesturing hand leave the gesture. When a control toggles a live mouse action the user is performing — recording a cursor path, scrubbing, freehand-drawing, demonstrating a motion — the start/stop must not be a button they have to click. Clicking it drags the mouse off the path, pollutes the start/end of the very motion being captured, and forces a round-trip back to where they were. Bind start/stop to the keyboard (spacebar by default) — keydown on Space, e.preventDefault() to kill page scroll, toggle the same handler the button would. Keep the button too (discoverability), but the hotkey is the real control. Generalize: any modal capture where one hand is committed to the primary input gets the other modality for mode-switching — gesture→key, and conversely a keyboard-heavy capture gets a foot/mouse toggle. The test: if triggering the control would move the thing you're capturing, it's the wrong modality.
When one control sets a bound on another (a min, a max, a threshold, an allowed set), the bounded control's UI must reflect the new bound the instant you change it. A "scrub" slider whose min/max attributes drift out of sync with its declared bounds is the most common silent bug — the user moves the bounding slider, nothing visible changes downstream, they assume both are broken.
Rules:
min/max, anything else) reads from that state on every update.min/max on every state change. Don't rely on browser-cached attribute values; rewrite them via JS each render. dependent.min = state.lo; dependent.max = state.hi.snapshot() showing state changed doesn't prove the pixels did.Pattern: every time applyState() runs (or its split equivalents), call a syncBounds() helper that walks the dependent-input registry and pushes the live bounds into every min/max/disabled attribute. Clamp values into the new bounds in the same pass.
A common shape is two sliders that together define an interval — min ⟷ max, near ⟷ far, tightEnd ⟷ wideEnd, start ⟷ end. If the user can drag one past the other, the interval inverts or collapses. Downstream math typically does (x - lo) / (hi - lo) which divides by zero or returns negative t — producing NaN coordinates, collapsed views, or inverted lerps. The user sees the studio "break" but no error fires.
Both ends of the defense:
syncBounds() pass: lower.max = upper.value - MIN_SPAN and upper.min = lower.value + MIN_SPAN (small epsilon, e.g. 2 units, so they can't even touch). The user can't physically drag past the other anchor.denominator > 0 and pick a sane fallback for the degenerate case (e.g. clamp t = 1 or t = 0). UI can race the math — always assume the math could be hit with crossed bounds anyway (URL hash, JSON paste-back, programmatic state mutation).tightEnd to the same value as wideEnd, verify the scene doesn't break. Drag tightEnd past wideEnd, verify same. If you can crash the studio with two slider drags, that's a release blocker.<canvas> and/or DOM, vanilla JS, a sidebar of controls. No build step, no framework, no deps unless one is genuinely required. Lives in a project-local scratch dir (e.g. scripts/.lookdev-<name>/ or scripts/.preview-<name>/), gitignored.input event. Debounce heavy work via requestAnimationFrame. Keep the loop tight enough to feel like a real slider, not a survey.
<input type=number>, two-way synced. The drag is for exploring; the typed number is for hitting an exact value (and reading the current one). A static <span> readout is not enough — the user must be able to click it and type. Sync rule: on slider input, write the number field; on number input/change, update state and re-render — but do not overwrite a field while it has focus (guard with document.activeElement), or typing gets clobbered mid-keystroke. Clamp to [min,max] on commit (change), not on every keystroke, so intermediate values like "1" before "12" aren't snapped.DEFAULTS object; Object.assign(state, DEFAULTS) then re-render). Cheap to add, and essential once the user has wandered far from baseline.input, not per event), keep a bounded stack (~100–120 entries), and on a new edit after undo, truncate the redo branch. Restore by re-applying a snapshot through the same applyState path the loader uses (so it can't drift). Guard the key handler when focus is in an <input>/<textarea> so native text-undo still works. A lookdev without undo punishes exploration — the whole point of the tool.settings.json, so the exact state ships next to the result.applyState path as the JSON paste. Per-format hooks: binary STL → append MAGIC + uint32 len + JSON after the triangle data (CAM ignores trailing bytes; parse count at byte 80, footer at 84 + count*50) and drop a human note in the 80-byte header; PNG → a tEXt/iTXt chunk; SVG/XML → a <metadata> element or comment; JPEG/MP4 → EXIF/XMP UserComment; GLB → an extras field. The payoff: the user drops last week's STL back on the viewport and the studio re-dials itself — no "which settings made this?" archaeology. Verify the round-trip (export → reset → load → assert state matches) and confirm the artifact still opens in its native tool (the trailing/edge metadata must not corrupt it). Skip only when the format has nowhere safe to stash bytes; the sidecar zip always works as the fallback.object-fit. A generic centered canvas is not WYSIWYG.app/dev/... or equivalent) so the real components, styles, and tokens are in the comparison. Delete the route once baked.Any lookdev with a non-fixed camera (OrbitControls, trackball, free fly — anything where the user can spin/tumble the view) MUST include a CAD-style ViewCube in a corner. Free orbit alone disorients: the user loses which way is up, can't get a repeatable canonical view, and can't tell whether they're looking at the front or the back. The cube fixes both problems — it's an orientation indicator and a controller in one. Copy the Autodesk/Fusion 360 ViewCube — that's the interaction users expect; don't invent a different gizmo.
Required behaviour (this is cheap — ~70 lines of Three.js, no excuse to skip):
gizmoCam.position = (mainCam.position − target).normalize() * d; gizmoCam.up = mainCam.up; gizmoCam.lookAt(0,0,0)) so the cube always mirrors the scene's current orientation.|c|>0.55 ? sign(c) : 0) to derive a view direction. One pickable cube then yields faces → ortho views, edges → 45° edge views, corners → iso views from a single mesh — no separate hit zones needed. Animate the main camera to target + dir*currentDist with a short lerp (~0.28/frame), not an instant cut — the motion is what keeps the user oriented.pointerdown record the start and setPointerCapture; on pointermove, once travel exceeds ~4px flip into drag mode and orbit the main camera by the pointer delta (convert the camera offset to spherical around the target, theta -= dx*k; phi = clamp(phi - dy*k, ε, π−ε)); on pointerup, if it never became a drag, treat it as a snap-click. Capture means the drag keeps working when the pointer leaves the little canvas. Cancel any in-flight snap tween when a drag starts.camera.up by ±90° around the normalized position−target axis). This is the rotate users mean when they say the gizmo "can't rotate" — drag-orbit is not a substitute for it. Snap-cleanup the rolled up so near-cardinal components land exactly on 0/±1 (keep genuine diagonals). Let it roll in ANY view, including iso — do NOT gate it to face-on views or auto-snap-to-face first. (I tried that "Fusion only rolls in standard views" guard and it backfired: it stops the user rolling an isometric view into the exact orientation they want, which is a primary reason they reach for the arrows. Rolling an iso view is a valid, common move.)position/up/target, and rebuilding controls; size the ortho frustum from the current target distance (h = 2·dist·tan(fov/2)) so the switch doesn't jump scale. Handle resize for both (isPerspectiveCamera → set aspect; ortho → recompute left/right from aspect keeping height).camera.up + OrbitControls is a TRAP — read this. Three's OrbitControls (r160) captures its orbit-axis quaternion from camera.up once, at construction. If you mutate camera.up afterward (e.g. to "fix" a top view, or to roll) and leave it, the main-viewport drag silently breaks — OrbitControls keeps orbiting around the old up while the camera renders with the new one. Two consequences for the gizmo: (a) Do NOT flip camera.up for top/bottom snaps. Leave it (0,1,0) and instead nudge the snap direction a hair off the pole (dir = (0,±1,0.0009)) so lookAt with up=+Y doesn't gimbal-lock. (b) When you DO need a new up (the roll arrows), dispose and recreate OrbitControls after setting camera.up, copying target across, so it re-captures the axis. Snaps and Home should reset to up=(0,1,0) and rebuild if currently rolled.camera.up=(0,1,0), and rebuilds controls if rolled.d (1/2/3 nonzero axes), for each nonzero axis place one quad on that face at the cell offset (other-axis sign)*⅔. A corner lights 3 quads (one per adjacent face), an edge 2, a face 1. A plain whole-face tint is wrong — the user can't tell a corner-pick from a face-pick. Also set a grab/grabbing cursor so the cube reads as draggable.Orient the model so FRONT is the face the user cares about. The cube's labels are fixed to world axes, so how you place the model decides what "FRONT" shows. For a relief/panel/anything with a hero face, stand it so the hero face points world +Z (= FRONT) and image-up points +Y — don't lay it flat facing +Y, or FRONT shows a meaningless edge and TOP shows the hero (surprising and "wrong" to the user). Watch the displaced-axis sign too: Three's PlaneGeometry pushes -y, so vertex row 0 is +Y (top) — map image row 0 (top) to it with no flip, or your relief comes out upside-down. Verify by snapping FRONT and eyeballing against the source image; don't trust the index math.
Build solids, not floating sheets. A displaced PlaneGeometry is a single hollow surface — fine for a quick look, wrong the moment the user inspects it. In X-ray (or any side view) the raised bumps read as hollow domes floating above the base with a gap, and it's not watertight for STL/CAM. If the thing is a real object (relief, terrain block, carved panel), build a solid heightfield: displaced top surface + perimeter skirt walls + flat bottom, so it's rooted on its base. The user will notice "the back doesn't touch the backplate." Set the material DoubleSide so hand-wound walls never render black.
Section / X-ray for hidden internal dimensions. When a control sets something you can't see from outside — wall thickness, a backing/backplate, internal clearance, draft — add an X-ray/section toggle so the user can actually see what they're dialing. Cheapest version: ghost the outer shell (transparent, opacity~0.15, depthWrite:false) and render the measured solid (the backplate slab, the remaining wall) as an opaque distinctly-colored mesh with a bright edge line at the critical boundary; pair it with a side ortho snap so the dimension reads as a clean band. (A true clipping-plane section with caps is the fancier version; usually not worth the stencil work.) Don't make the user infer a hidden thickness from a number alone when one toggle can show it.
Keep it in world/view space aligned to how the model is displayed (account for any root rotation you applied). Verify by visualization, not math (these all bit me): click TOP then drag the main viewport and screenshot — confirm it still orbits (catches the camera.up trap); hover a corner and screenshot the cube — confirm the corner zone lights across faces, not the whole face; click a roll arrow from an iso view and screenshot — confirm it snaps to a face (not a diagonal roll); snap FRONT and confirm the hero face is upright. Genuinely-optional Fusion extras: the adjacent-face triangle arrows (drag-orbit covers them), the N/E/S/W compass ring, and the right-click "set current view as Home" menu — skip unless asked.
app/dev/... then nuke it.A worked example — an image-treatment studio:
extracts a Lab-k-means dominant palette with coverage %, exposes sliders
(resolution, colorize, saturation, gap, glyph, contrast), per-band color
pickers, a luminance-vs-nearest mapping toggle, a Copy-settings-JSON
button, and a --bake-json Python path that renders the chosen state to
committed PNG/WebP with math identical to the JS preview. The preview
canvases match the production thumb and hero shapes exactly.
lookdev is one of two flagship narratives — human-in-the-loop (you, the human, judge and tune). Its determinism-narrative sibling is deterministic-design (render → measure the UI, numbers not vibes). The family it chains with:
npx claudepluginhub sickn33/antigravity-awesome-skills --plugin antigravity-bundle-aas-mobile-app-builderAutomates visual tuning by having a vision/video model rate rendered variants in a loop until the output looks/feels right. Useful for animation easing, color grading, layout spacing, and other aesthetic parameters.
Builds animated, professional web pages that visualize software architecture, system design, data models, or product ideas using Three.js 3D scenes, GSAP scroll-driven animations, and SVG interactive diagrams.
Builds polished visual web artifacts: pages, dashboards, prototypes, slide decks, animations, UI mockups, and data visualizations using HTML/CSS/JavaScript/React. Best for browser-rendered front-end deliverables, not back-end or non-visual tasks.