Agents (llms.txt)

Inspector

A right-edge hover-reveal panel for properties / settings sidebars.

Hover the right edge →
import { Palette } from '@untitledui-pro/icons/solid';
import { Input } from '@/components/input';
import { Inspector } from '@/components/inspector';

export default function InspectorPreview() {
  return (
    <div className="relative h-[480px] w-full overflow-hidden rounded-xl border border-border bg-surface-sunken">
      <div className="grid h-full place-items-center text-foreground-secondary text-sm">
        Hover the right edge →
      </div>

      <Inspector defaultPinned width="18rem">
        <Inspector.Handle />
        <Inspector.Panel>
          <Inspector.Header title="Page style" icon={<Palette />} />
          <Inspector.Content>
            <Inspector.Section label="Background">
              <Inspector.Field label="Color">
                <Input className="w-32" defaultValue="#ffffff" />
              </Inspector.Field>
              <Inspector.Field label="Padding">
                <Input className="w-20" defaultValue="0" />
              </Inspector.Field>
            </Inspector.Section>

            <Inspector.Section label="Body">
              <Inspector.Field label="Text">
                <Input className="w-32" defaultValue="#000000" />
              </Inspector.Field>
              <Inspector.Field label="Width">
                <Input className="w-20" defaultValue="600" />
              </Inspector.Field>
              <Inspector.Field label="Radius">
                <Input className="w-20" defaultValue="8" />
              </Inspector.Field>
            </Inspector.Section>
          </Inspector.Content>
        </Inspector.Panel>
      </Inspector>
    </div>
  );
}

export const meta = {
  layout: 'padded',
};

Dependencies

Source Code

'use client';

import { Pin01, Pin02, X } from '@untitledui-pro/icons/solid';
import { createContext, use, useState } from 'react';

import { IconButton } from '@/components/button';
import { cn } from '@/lib/utils/classnames';

interface InspectorContextValue {
  pinned: boolean;
  setPinned: (pinned: boolean) => void;
  hovered: boolean;
  setHovered: (hovered: boolean) => void;
  width: string;
  height: string;
}

const InspectorContext = createContext<InspectorContextValue | null>(null);

const useInspector = () => {
  const ctx = use(InspectorContext);
  if (!ctx) throw new Error('Inspector parts must be inside <Inspector>');
  return ctx;
};

interface InspectorProps extends React.ComponentPropsWithRef<'section'> {
  /** Open state when pinned. Uncontrolled if omitted. */
  defaultPinned?: boolean;
  /** Width of the panel when open. Default 20rem. */
  width?: string;
  /** Height of the panel. Default fills the container minus 2rem. */
  height?: string;
}

/**
 * Right-edge hover-reveal panel. Renders three parts:
 *   - a thin handle on the viewport's right edge
 *   - the floating panel itself, translated off-screen by default
 *   - a backdrop-free hover region that keeps the panel open while the
 *     pointer is over it
 *
 * Hovering the handle slides the panel in. Clicking the pin in the panel
 * header keeps it open even after the pointer leaves.
 *
 * Place once inside an `AppShell` (or any positioned ancestor). The shell's
 * main content stays unaffected — the panel floats over it.
 */
const Inspector = ({
  ref,
  className,
  defaultPinned = false,
  width = '20rem',
  height = 'calc(100% - 2rem)',
  children,
  ...rest
}: InspectorProps) => {
  const [pinned, setPinned] = useState(defaultPinned);
  const [hovered, setHovered] = useState(false);

  const open = pinned || hovered;

  return (
    <InspectorContext
      value={{ pinned, setPinned, hovered, setHovered, width, height }}
    >
      <section
        ref={ref}
        data-inspector
        data-open={open ? '' : undefined}
        data-pinned={pinned ? '' : undefined}
        aria-label="Inspector"
        style={
          {
            '--inspector-w': width,
            '--inspector-h': height,
          } as React.CSSProperties
        }
        className={cn(
          'pointer-events-none absolute inset-y-0 right-0 z-30 flex items-center pr-3',
          className
        )}
        onMouseEnter={() => setHovered(true)}
        onMouseLeave={() => setHovered(false)}
        {...rest}
      >
        {children}
      </section>
    </InspectorContext>
  );
};

/**
 * The small vertical pill on the right edge that signals "hover me to reveal
 * the panel". It's still visible (subtle) when the panel is open so users
 * know what they're hovering.
 */
const InspectorHandle = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'div'>) => {
  const { pinned } = useInspector();
  return (
    <div
      ref={ref}
      aria-hidden="true"
      className={cn(
        // Visible pill — small, inset from the viewport edge.
        'pointer-events-auto absolute top-1/2 right-3 h-24 w-1.5 -translate-y-1/2 rounded-full bg-foreground/20',
        'transition-opacity duration-(--duration-hover) ease-(--ease)',
        // Invisible hit zone via ::after — extends far beyond the visible
        // pill so the user doesn't lose hover when the pointer wobbles off
        // the 1.5px line on its way to the panel.
        "after:absolute after:-inset-x-4 after:-inset-y-16 after:content-['']",
        pinned && 'opacity-0',
        className
      )}
      {...rest}
    />
  );
};

interface InspectorPanelProps extends React.ComponentPropsWithRef<'aside'> {}

const InspectorPanel = ({
  ref,
  className,
  children,
  ...rest
}: InspectorPanelProps) => {
  const { hovered, pinned } = useInspector();
  const open = pinned || hovered;
  return (
    <aside
      ref={ref}
      data-state={open ? 'open' : 'closed'}
      className={cn(
        'pointer-events-auto flex h-(--inspector-h) w-(--inspector-w) flex-col overflow-hidden rounded-2xl border border-border bg-surface-raised shadow-lg',
        'transition-transform duration-(--duration-hover) ease-(--ease)',
        'data-[state=closed]:translate-x-[calc(100%+1rem)]',
        'data-[state=open]:translate-x-0',
        className
      )}
      {...rest}
    >
      {children}
    </aside>
  );
};

interface InspectorHeaderProps
  extends Omit<React.ComponentPropsWithRef<'div'>, 'title'> {
  title: React.ReactNode;
  icon?: React.ReactNode;
  /** Show the pin toggle. Default `true`. */
  showPin?: boolean;
  /** Show the close button (which un-pins). Default `false`. */
  showClose?: boolean;
}

const InspectorHeader = ({
  ref,
  className,
  title,
  icon,
  showPin = true,
  showClose = false,
  ...rest
}: InspectorHeaderProps) => {
  const { pinned, setPinned } = useInspector();
  return (
    <div
      ref={ref}
      className={cn(
        'flex shrink-0 items-center gap-2 border-border border-b px-4 py-3',
        className
      )}
      {...rest}
    >
      {icon && (
        <span className="grid size-4 shrink-0 place-items-center text-foreground-secondary [&>svg]:size-4">
          {icon}
        </span>
      )}
      <span className="flex-1 truncate font-semibold text-foreground text-sm">
        {title}
      </span>
      {showPin && (
        <IconButton
          variant="ghost"
          size="xs"
          aria-label={pinned ? 'Unpin' : 'Pin open'}
          aria-pressed={pinned}
          onClick={() => setPinned(!pinned)}
        >
          {pinned ? <Pin02 /> : <Pin01 />}
        </IconButton>
      )}
      {showClose && (
        <IconButton
          variant="ghost"
          size="xs"
          aria-label="Close"
          onClick={() => setPinned(false)}
        >
          <X />
        </IconButton>
      )}
    </div>
  );
};

const InspectorContent = ({
  ref,
  className,
  ...rest
}: React.ComponentPropsWithRef<'div'>) => (
  <div
    ref={ref}
    className={cn('flex-1 overflow-y-auto p-4', className)}
    {...rest}
  />
);

interface InspectorSectionProps extends React.ComponentPropsWithRef<'section'> {
  label: React.ReactNode;
}

const InspectorSection = ({
  ref,
  className,
  label,
  children,
  ...rest
}: InspectorSectionProps) => (
  <section
    ref={ref}
    className={cn(
      'flex flex-col gap-3 border-border border-t pt-5 first-of-type:border-t-0 first-of-type:pt-0',
      'mt-5 first-of-type:mt-0',
      className
    )}
    {...rest}
  >
    <p className="font-semibold text-foreground text-sm">{label}</p>
    <div className="flex flex-col gap-3">{children}</div>
  </section>
);

interface InspectorFieldProps extends React.ComponentPropsWithRef<'div'> {
  label: React.ReactNode;
}

const InspectorField = ({
  ref,
  className,
  label,
  children,
  ...rest
}: InspectorFieldProps) => (
  <div
    ref={ref}
    className={cn('flex items-center justify-between gap-3', className)}
    {...rest}
  >
    <label className="text-foreground-secondary text-sm">{label}</label>
    <div className="flex min-w-0 flex-shrink-0 items-center gap-1">
      {children}
    </div>
  </div>
);

const CompoundInspector = Object.assign(Inspector, {
  Handle: InspectorHandle,
  Panel: InspectorPanel,
  Header: InspectorHeader,
  Content: InspectorContent,
  Section: InspectorSection,
  Field: InspectorField,
});

export type {
  InspectorFieldProps,
  InspectorHeaderProps,
  InspectorPanelProps,
  InspectorProps,
  InspectorSectionProps,
};
export { CompoundInspector as Inspector, useInspector };

Inspector is a floating right-edge panel that slides in on hover and stays until the pointer leaves. A pin button locks it open. Use it for property inspectors, settings sidebars, or any contextual controls that shouldn’t permanently consume layout space.

The panel renders as a fixed-position overlay — main content beneath it never shifts. Place once inside an AppShell (or any positioned ancestor).

Anatomy


          <Inspector>
  <Inspector.Handle />
  <Inspector.Panel>
    <Inspector.Header title="Page style" />
    <Inspector.Content>
      <Inspector.Section label="Background">
        <Inspector.Field label="Color">
          <Input />
        </Inspector.Field>
      </Inspector.Section>
    </Inspector.Content>
  </Inspector.Panel>
</Inspector>
        

Behavior

  • Default state: panel translated off-screen, only Handle visible as a thin pill on the right edge.
  • Hover: hovering the handle or panel slides the panel in via --duration-hover.
  • Pin: clicking the pin in Inspector.Header keeps the panel open. Hover state is then ignored.
  • Unpin / close: clicking the pin again (or showClose’s X) un-pins. The panel hides as soon as the pointer leaves.

The panel respects prefers-reduced-motion via the shared family token.

API Reference

Inspector

Extends the div element.

Prop Default Type
defaultPinned false boolean
width '20rem' string

Inspector.Header

Prop Default Type
title * - ReactNode
icon - ReactNode
showPin true boolean
showClose false boolean

Inspector.Section

Prop Default Type
label * - ReactNode

Inspector.Field

Prop Default Type
label * - ReactNode

Examples

Default

Hover the right edge →
import { Palette } from '@untitledui-pro/icons/solid';
import { Input } from '@/components/input';
import { Inspector } from '@/components/inspector';

export default function InspectorPreview() {
  return (
    <div className="relative h-[480px] w-full overflow-hidden rounded-xl border border-border bg-surface-sunken">
      <div className="grid h-full place-items-center text-foreground-secondary text-sm">
        Hover the right edge →
      </div>

      <Inspector defaultPinned width="18rem">
        <Inspector.Handle />
        <Inspector.Panel>
          <Inspector.Header title="Page style" icon={<Palette />} />
          <Inspector.Content>
            <Inspector.Section label="Background">
              <Inspector.Field label="Color">
                <Input className="w-32" defaultValue="#ffffff" />
              </Inspector.Field>
              <Inspector.Field label="Padding">
                <Input className="w-20" defaultValue="0" />
              </Inspector.Field>
            </Inspector.Section>

            <Inspector.Section label="Body">
              <Inspector.Field label="Text">
                <Input className="w-32" defaultValue="#000000" />
              </Inspector.Field>
              <Inspector.Field label="Width">
                <Input className="w-20" defaultValue="600" />
              </Inspector.Field>
              <Inspector.Field label="Radius">
                <Input className="w-20" defaultValue="8" />
              </Inspector.Field>
            </Inspector.Section>
          </Inspector.Content>
        </Inspector.Panel>
      </Inspector>
    </div>
  );
}

export const meta = {
  layout: 'padded',
};

Previous

Input

Next

Kbd