Skip to content
AI Interfaces block

Agent-Adjusted Inspector

The model edits real properties and every change is attributed and reversible — per-property provenance in words, per-property revert, and sliders whose aria-valuetext carries the unit.

381 lineslucide-reactAdded 13 Aug 2026Updated 10 Sept 2026
  • inspector
  • properties
  • fine-tune
  • provenance
  • undo

What's included

  • components/ai-inspector-panel.tsx
  • Needs lucide-react

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add ai-inspector-panel

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Start a page with this section — add more, order them, and leave with the page source.

Preview

Seasonal

Burnt honey & sea salt

Slow-churned with wildflower honey, finished with flaked salt.

Flavour card

Corner radiusset by agent
12px
Padding
20px
Accentset by agent
Title weight
Shadowset by agent

Agent adjusted 3 properties.

Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.

Source

components/ai-inspector-panel.tsx
/**
 * <AiInspectorPanel> — the model adjusts real properties in an inspector,
 * and every change is shown, reversible and attributed.
 *
 * The "agent edits your document" pattern, applied to design properties. Its
 * whole risk is silent mutation: a model that changes six values and reports
 * "done" leaves a user unable to tell what happened. So every control here
 * carries provenance — who set it, you or the agent — and a per-property
 * revert.
 *
 *  - Changed properties are marked in text ("set by agent"), not by a dot.
 *  - Revert is per property and per panel, because "undo everything" is
 *    useless when five of six changes were wanted.
 *  - The prompt field is a real form, so Enter submits it, and the result is
 *    announced through `role="status"` rather than only appearing visually.
 *  - Sliders are native `<input type="range">` with `aria-valuetext`, so the
 *    announced value is "16 pixels" rather than "16" — the unit is the part
 *    that makes it meaningful.
 *  - The colour input is a real `type="color"` paired with a text field, so
 *    it is usable both by picking and by typing a hex, and the swatch is
 *    never the only representation.
 *
 * The preview updates from state through inline styles, which is the honest
 * way to demo a live inspector — Tailwind cannot express an arbitrary
 * runtime value without a class it has never seen.
 */

'use client'

import * as React from 'react'
import { RotateCcw, Sparkles, Undo2 } from 'lucide-react'

/*
  Contrast ratio, inlined rather than imported from `@/lib/color-tools`:
  the registry serves this file standalone, and that module is app-internal
  (the Designer Tools), not part of the component catalog it can package.
  Just the WCAG relative-luminance/contrast-ratio pair this block needs.
*/
function hexToRgb(hex: string): { r: number; g: number; b: number } | null {
  const m = /^#?([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(hex.trim())
  if (!m) return null
  let s = m[1]
  if (s.length === 3) {
    s = s[0] + s[0] + s[1] + s[1] + s[2] + s[2]
  }
  return {
    r: parseInt(s.slice(0, 2), 16),
    g: parseInt(s.slice(2, 4), 16),
    b: parseInt(s.slice(4, 6), 16),
  }
}

function relativeLuminance({ r, g, b }: { r: number; g: number; b: number }): number {
  const channel = (c: number) => {
    const s = c / 255
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4)
  }
  return 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b)
}

function contrastRatio(fg: string, bg: string): number | null {
  const fgRgb = hexToRgb(fg)
  const bgRgb = hexToRgb(bg)
  if (!fgRgb || !bgRgb) return null
  const l1 = relativeLuminance(fgRgb)
  const l2 = relativeLuminance(bgRgb)
  const lighter = Math.max(l1, l2)
  const darker = Math.min(l1, l2)
  return (lighter + 0.05) / (darker + 0.05)
}

/**
 * Ink for text painted directly on a solid accent fill. The accent is a
 * live, user-picked colour (the whole point of this inspector), so a fixed
 * `text-white` is only an accident away from failing 1.4.3 — pick whichever
 * of black or white actually contrasts against the value in hand.
 */
function readableInk(hex: string): string {
  const onWhite = contrastRatio(hex, '#ffffff') ?? 0
  const onBlack = contrastRatio(hex, '#0a0a0a') ?? 0
  return onWhite >= onBlack ? '#ffffff' : '#0a0a0a'
}

export interface InspectorState {
  radius: number
  padding: number
  accent: string
  weight: string
  shadow: boolean
}

export interface AiInspectorPanelProps {
  heading?: string
  /** The agent's last instruction, replayed in the field. */
  lastPrompt?: string
  initial?: Partial<InspectorState>
  /** Which properties the agent changed, for provenance marks. */
  agentTouched?: Array<keyof InspectorState>
  /**
   * Id for the field. A server component cannot call `useId`, so this is
   * the escape hatch for a page that renders two of these: give the second
   * one its own id rather than letting both labels resolve to the first.
   */
  promptId?: string
  className?: string
}

const BASE: InspectorState = {
  radius: 12,
  padding: 20,
  accent: '#4f46e5',
  weight: '600',
  shadow: true,
}

const WEIGHTS = [
  { value: '400', label: 'Regular' },
  { value: '600', label: 'Semibold' },
  { value: '800', label: 'Extrabold' },
]

export function AiInspectorPanel({
  heading = 'Flavour card',
  lastPrompt = 'make it softer and a bit more premium',
  initial,
  promptId = 'inspector-prompt',
  agentTouched = ['radius', 'accent', 'shadow'],
  className = '',
}: AiInspectorPanelProps) {
  const start = React.useMemo(() => ({ ...BASE, ...initial }), [initial])

  const [state, setState] = React.useState<InspectorState>(start)
  const [touched, setTouched] = React.useState<Array<keyof InspectorState>>(agentTouched)
  const [prompt, setPrompt] = React.useState(lastPrompt)
  const [note, setNote] = React.useState('Agent adjusted 3 properties.')

  function set<K extends keyof InspectorState>(key: K, value: InspectorState[K]) {
    setState((s) => ({ ...s, [key]: value }))
    // A property the human has just moved is no longer the agent's.
    setTouched((list) => list.filter((k) => k !== key))
    setNote('')
  }

  function revert(key: keyof InspectorState) {
    setState((s) => ({ ...s, [key]: start[key] }))
    setTouched((list) => list.filter((k) => k !== key))
    setNote(`Reverted ${key}.`)
  }

  return (
    <div className={`mx-auto grid w-full max-w-3xl gap-5 p-6 md:grid-cols-[1fr_18rem] ${className}`}>
      {/* -- Preview ------------------------------------------------------ */}
      <div className="flex items-center justify-center rounded-2xl border border-border/60 bg-muted/30 p-8">
        <div
          style={{
            borderRadius: `${state.radius}px`,
            padding: `${state.padding}px`,
            boxShadow: state.shadow ? '0 18px 40px -18px rgb(0 0 0 / 0.45)' : 'none',
          }}
          className="w-full max-w-xs border border-border/60 bg-card"
        >
          <span
            style={{ backgroundColor: `${state.accent}1a` }}
            className="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-[11px] font-bold uppercase tracking-wide text-foreground"
          >
            Seasonal
          </span>

          <p
            style={{ fontWeight: Number(state.weight) }}
            className="mt-3 text-lg leading-tight"
          >
            Burnt honey &amp; sea salt
          </p>

          <p className="mt-1.5 text-xs leading-relaxed text-muted-foreground">
            Slow-churned with wildflower honey, finished with flaked salt.
          </p>

          <button
            type="button"
            style={{
              backgroundColor: state.accent,
              color: readableInk(state.accent),
              borderRadius: `${state.radius * 0.7}px`,
            }}
            className="mt-4 w-full px-3 py-2 text-sm font-semibold transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
          >
            Add to case
          </button>
        </div>
      </div>

      {/* -- Inspector ----------------------------------------------------- */}
      <div className="overflow-y-hidden rounded-2xl border border-border/60 bg-card">
        <div className="flex items-center gap-2 border-b border-border/60 px-4 py-3">
          <Sparkles aria-hidden className="h-4 w-4 shrink-0 text-primary" />
          {heading ? (
            <h3 data-stress-ignore className="min-w-0 flex-1 break-words text-sm font-semibold">
              {heading}
            </h3>
          ) : null}
          <button
            type="button"
            onClick={() => {
              setState(start)
              setTouched([])
              setNote('Reverted every change.')
            }}
            className="inline-flex shrink-0 items-center gap-1 rounded-lg px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
          >
            <RotateCcw aria-hidden className="h-3 w-3" />
            Reset
          </button>
        </div>

        <div className="space-y-4 px-4 py-4">
          <Row label="Corner radius" owned={touched.includes('radius')} onRevert={() => revert('radius')}>
            <input
              type="range"
              min={0}
              max={28}
              step={1}
              value={state.radius}
              aria-label="Corner radius"
              aria-valuetext={`${state.radius} pixels`}
              onChange={(e) => set('radius', Number(e.target.value))}
              className="w-full accent-primary"
            />
            <span className="w-10 shrink-0 text-end font-mono text-xs tabular-nums text-muted-foreground">
              {state.radius}px
            </span>
          </Row>

          <Row label="Padding" owned={touched.includes('padding')} onRevert={() => revert('padding')}>
            <input
              type="range"
              min={8}
              max={40}
              step={2}
              value={state.padding}
              aria-label="Padding"
              aria-valuetext={`${state.padding} pixels`}
              onChange={(e) => set('padding', Number(e.target.value))}
              className="w-full accent-primary"
            />
            <span className="w-10 shrink-0 text-end font-mono text-xs tabular-nums text-muted-foreground">
              {state.padding}px
            </span>
          </Row>

          <Row label="Accent" owned={touched.includes('accent')} onRevert={() => revert('accent')}>
            <input
              type="color"
              value={state.accent}
              aria-label="Accent colour"
              onChange={(e) => set('accent', e.target.value)}
              className="h-7 w-9 shrink-0 cursor-pointer rounded border border-border/60 bg-transparent"
            />
            {/* The hex is typeable, not just pickable — a colour input alone
                excludes anyone working from a spec. */}
            <input
              type="text"
              value={state.accent}
              aria-label="Accent hex value"
              onChange={(e) => set('accent', e.target.value)}
              className="w-full rounded-lg border border-border/60 bg-background px-2 py-1 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
            />
          </Row>

          <Row label="Title weight" owned={touched.includes('weight')} onRevert={() => revert('weight')}>
            <select
              value={state.weight}
              aria-label="Title weight"
              onChange={(e) => set('weight', e.target.value)}
              className="w-full rounded-lg border border-border/60 bg-background px-2 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              {WEIGHTS.map((w) => (
                <option key={w.value} value={w.value}>
                  {w.label}
                </option>
              ))}
            </select>
          </Row>

          <Row label="Shadow" owned={touched.includes('shadow')} onRevert={() => revert('shadow')}>
            <label className="flex cursor-pointer items-center gap-2 text-xs">
              <input
                type="checkbox"
                role="switch"
                checked={state.shadow}
                onChange={(e) => set('shadow', e.target.checked)}
                className="h-4 w-4 accent-primary"
              />
              {state.shadow ? 'On' : 'Off'}
            </label>
          </Row>
        </div>

        {/* -- Prompt -------------------------------------------------- */}
        <form
          onSubmit={(e) => {
            e.preventDefault()
            setNote(`Asked the agent to “${prompt}”.`)
          }}
          className="border-t border-border/60 bg-muted/30 p-3"
        >
          <label htmlFor={promptId} className="sr-only">
            Tell the agent what to change
          </label>
          <div className="flex gap-2">
            <input
              id={promptId}
              value={prompt}
              onChange={(e) => setPrompt(e.target.value)}
              placeholder="Describe a change…"
              className="min-w-0 flex-1 rounded-lg border border-border/60 bg-background px-2.5 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
            />
            <button
              type="submit"
              className="shrink-0 rounded-lg bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              Adjust
            </button>
          </div>

          <p role="status" className="mt-2 min-h-4 text-[11px] text-muted-foreground">
            {note}
          </p>
        </form>
      </div>
    </div>
  )
}

/**
 * One inspector row.
 *
 * `owned` marks a property the agent set. Rendered as words rather than a
 * coloured border — "which of these did the machine touch" has to be
 * answerable without seeing the colour.
 */
function Row({
  label,
  owned,
  onRevert,
  children,
}: {
  label: string
  owned: boolean
  onRevert: () => void
  children: React.ReactNode
}) {
  return (
    <div>
      <div className="mb-1.5 flex items-center gap-2">
        <span className="min-w-0 flex-1 truncate text-xs font-medium">{label}</span>

        {owned ? (
          <>
            <span className="shrink-0 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary">
              set by agent
            </span>
            <button
              type="button"
              onClick={onRevert}
              className="ms-auto inline-flex items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              <Undo2 aria-hidden className="h-3 w-3 rtl:rotate-180" />
              Revert
              <span className="sr-only"> {label}</span>
            </button>
          </>
        ) : null}
      </div>

      <div className="flex items-center gap-2">{children}</div>
    </div>
  )
}

Before you paste

  • Styling is Tailwind utility classes on semantic tokens (bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.
  • Install: npm i lucide-react
  • Every prop has a default, so it renders standalone before you wire it up.

Where it goes

Drop it at components/ai-inspector-panel.tsx and import it where you need the section:

import { AiInspectorPanel } from '@/components/ai-inspector-panel'

Customize

3 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
headingstring'Flavour card'
lastPromptThe agent's last instruction, replayed in the field.string'make it softer and a bit more premium'
initialPartial<InspectorState>—
agentTouchedWhich properties the agent changed, for provenance marks.Array<keyof InspectorState>['radius', 'accent', 'shadow']
promptIdId for the field. A server component cannot call `useId`, so this is the escape hatch for a page that renders two of these: give the second one its own id rather than letting both labels resolve to the first.string'inspector-prompt'
classNamestring''

Not using React?

The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.

This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.

ai-inspector-panel.html
<!--
  Agent-Adjusted Inspector — markup from the Hoverlab catalog.

  This is the block rendered once to HTML and wrapped as a component
  file. It is not a port of the React source: the Tailwind classes carry
  the design, which is the part that took the work, and they are the same
  in every framework.

  This block is interactive in React and the handlers are NOT here.
  Buttons, toggles and menus render in their initial state and do
  nothing until you wire them up.
-->
<div class="mx-auto grid w-full max-w-3xl gap-5 p-6 md:grid-cols-[1fr_18rem] ">
  <div class="flex items-center justify-center rounded-2xl border border-border/60 bg-muted/30 p-8">
    <div style="border-radius:12px;padding:20px;box-shadow:0 18px 40px -18px rgb(0 0 0 / 0.45)" class="w-full max-w-xs border border-border/60 bg-card">
      <span style="background-color:#4f46e51a" class="inline-flex max-w-full truncate rounded-full px-2.5 py-1 text-[11px] font-bold uppercase tracking-wide text-foreground">Seasonal</span>
      <p style="font-weight:600" class="mt-3 text-lg leading-tight">Burnt honey &amp; sea salt</p>
      <p class="mt-1.5 text-xs leading-relaxed text-muted-foreground">Slow-churned with wildflower honey, finished with flaked salt.</p>
      <button type="button" style="background-color:#4f46e5;color:#ffffff;border-radius:8.399999999999999px" class="mt-4 w-full px-3 py-2 text-sm font-semibold transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background">Add to case</button>
    </div>
  </div>
  <div class="overflow-y-hidden rounded-2xl border border-border/60 bg-card">
    <div class="flex items-center gap-2 border-b border-border/60 px-4 py-3">
      <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-sparkles h-4 w-4 shrink-0 text-primary" aria-hidden="true">
        <path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"></path>
        <path d="M20 3v4"></path>
        <path d="M22 5h-4"></path>
        <path d="M4 17v2"></path>
        <path d="M5 18H3"></path>
      </svg>
      <h3 data-stress-ignore="true" class="min-w-0 flex-1 break-words text-sm font-semibold">Flavour card</h3>
      <button type="button" class="inline-flex shrink-0 items-center gap-1 rounded-lg px-1.5 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-rotate-ccw h-3 w-3" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path><path d="M3 3v5h5"></path></svg>Reset</button>
    </div>
    <div class="space-y-4 px-4 py-4">
      <div>
        <div class="mb-1.5 flex items-center gap-2">
          <span class="min-w-0 flex-1 truncate text-xs font-medium">Corner radius</span>
          <span class="shrink-0 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary">set by agent</span>
          <button type="button" class="ms-auto inline-flex items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo2 lucide-undo-2 h-3 w-3 rtl:rotate-180" aria-hidden="true"><path d="M9 14 4 9l5-5"></path><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"></path></svg>Revert<span class="sr-only"> Corner radius</span></button>
        </div>
        <div class="flex items-center gap-2">
          <input type="range" min="0" max="28" step="1" aria-label="Corner radius" aria-valuetext="12 pixels" class="w-full accent-primary" value="12" />
          <span class="w-10 shrink-0 text-end font-mono text-xs tabular-nums text-muted-foreground">12px</span>
        </div>
      </div>
      <div>
        <div class="mb-1.5 flex items-center gap-2">
          <span class="min-w-0 flex-1 truncate text-xs font-medium">Padding</span>
        </div>
        <div class="flex items-center gap-2">
          <input type="range" min="8" max="40" step="2" aria-label="Padding" aria-valuetext="20 pixels" class="w-full accent-primary" value="20" />
          <span class="w-10 shrink-0 text-end font-mono text-xs tabular-nums text-muted-foreground">20px</span>
        </div>
      </div>
      <div>
        <div class="mb-1.5 flex items-center gap-2">
          <span class="min-w-0 flex-1 truncate text-xs font-medium">Accent</span>
          <span class="shrink-0 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary">set by agent</span>
          <button type="button" class="ms-auto inline-flex items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo2 lucide-undo-2 h-3 w-3 rtl:rotate-180" aria-hidden="true"><path d="M9 14 4 9l5-5"></path><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"></path></svg>Revert<span class="sr-only"> Accent</span></button>
        </div>
        <div class="flex items-center gap-2">
          <input type="color" aria-label="Accent colour" class="h-7 w-9 shrink-0 cursor-pointer rounded border border-border/60 bg-transparent" value="#4f46e5" />
          <input type="text" aria-label="Accent hex value" class="w-full rounded-lg border border-border/60 bg-background px-2 py-1 font-mono text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring" value="#4f46e5" />
        </div>
      </div>
      <div>
        <div class="mb-1.5 flex items-center gap-2">
          <span class="min-w-0 flex-1 truncate text-xs font-medium">Title weight</span>
        </div>
        <div class="flex items-center gap-2">
          <select aria-label="Title weight" class="w-full rounded-lg border border-border/60 bg-background px-2 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring">
            <option value="400">Regular</option>
            <option value="600" selected="">Semibold</option>
            <option value="800">Extrabold</option>
          </select>
        </div>
      </div>
      <div>
        <div class="mb-1.5 flex items-center gap-2">
          <span class="min-w-0 flex-1 truncate text-xs font-medium">Shadow</span>
          <span class="shrink-0 rounded-full bg-primary/10 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wide text-primary">set by agent</span>
          <button type="button" class="ms-auto inline-flex items-center gap-1 rounded px-1 py-0.5 text-[11px] text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-undo2 lucide-undo-2 h-3 w-3 rtl:rotate-180" aria-hidden="true"><path d="M9 14 4 9l5-5"></path><path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11"></path></svg>Revert<span class="sr-only"> Shadow</span></button>
        </div>
        <div class="flex items-center gap-2">
          <label class="flex cursor-pointer items-center gap-2 text-xs"><input type="checkbox" role="switch" class="h-4 w-4 accent-primary" checked="" />On</label>
        </div>
      </div>
    </div>
    <form class="border-t border-border/60 bg-muted/30 p-3">
      <label for="inspector-prompt" class="sr-only">Tell the agent what to change</label>
      <div class="flex gap-2">
        <input id="inspector-prompt" placeholder="Describe a change…" class="min-w-0 flex-1 rounded-lg border border-border/60 bg-background px-2.5 py-1.5 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring" value="make it softer and a bit more premium" />
        <button type="submit" class="shrink-0 rounded-lg bg-primary px-3 py-1.5 text-xs font-semibold text-primary-foreground transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring">Adjust</button>
      </div>
      <p role="status" class="mt-2 min-h-4 text-[11px] text-muted-foreground">Agent adjusted 3 properties.</p>
    </form>
  </div>
</div>
  • This is rendered HTML, not a translation of the React source. The Tailwind classes carry the design and work in any framework.
  • It is one frame: the component in its initial state, with no props applied beyond the defaults.
  • This block is interactive in React — toggles, menus or form state. None of that survives here; the markup is the closed/default state and the handlers are gone. Re-wire them in your own framework.
  • Requires Tailwind, and the design tokens the classes reference (bg-card, text-muted-foreground, and so on). The template ZIPs ship a globals.css that defines them.

What each framework gets across the whole catalog — effects convert properly; this rung is markup.

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

Used in these pages

Want the whole screen instead of this one section? Open a page and copy it entire.

5 more blocks in Inline AI Actions

All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.

Browse Inline AI Actions

Shipping one commercially

Copying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.

  • A commercial licence for everything in the catalog
  • Unlimited bundle exports, in Vue, Svelte and Tailwind
  • One payment — no subscription, nothing to renew
Pro — $79 once

More Inline AI Actions blocks

View category
Open the full page for this block

Selection Action Toolbar

Highlight a passage and hand it to the model. Positioned from the range rect relative to its container, raised by selectionchange so Shift-Arrow works, and every button prevents the mousedown that would collapse the selection.

Inline AI Actions251 lines1 dep
Open the full page for this block

Ghost Text Completion

Copilot-style completion in a plain textarea: a transparent-caret field over a metric-matched mirror, Tab to accept, Escape that actually sticks, and the suggestion announced rather than left as invisible grey text.

Inline AI Actions206 lines1 dep
Open the full page for this block

Unprompted Insight Cards

Findings the agent surfaced on its own, paged one at a time, each stating its magnitude rather than its direction, dismissible, and carrying a sparkline that is also a sentence for anyone who cannot see it.

Inline AI Actions284 lines1 dep
Open the full page for this block

Editor Slash Menu

The "/" menu inside a document: focus never leaves the editor, every row names what it acts on, and Escape cleans up the trigger text it inserted instead of leaving you to backspace it.

Inline AI Actions343 lines1 dep
Open the full page for this block

Inline AI Action Menu

The actions offered on a selection, each saying whether it rewrites in place or opens a draft beside it.

Inline AI Actions151 linesNo deps