Skip to content
Dethink Components

ComponentsInspector

Inspector

Edit whatever is selected in a dense side panel. Grouped sections, compact rows, and honest validation that keeps the last good value.

On this page

Install component source from the public registry, or use the local workspace package. Follow the setup guide first. The npm package is not published yet.

View registry files
  • Motion for React (motion) · via shared Dethink components

    Animations, transitions, and reduced-motion handling.

  • React Aria (react-aria) · via shared Dethink components

    Accessibility hooks and overlay utilities used by the implementation.

  • React Aria Components (react-aria-components) · via shared Dethink components

    Accessible interaction primitives, including keyboard behavior, focus handling, and ARIA semantics.

Shared foundation: React, clsx for conditional classes, tailwind-merge for resolving utility-class conflicts. Styles use Tailwind CSS and Dethink CSS variables.

Based on this component’s source and registry composition. Libraries used only in showcase examples are excluded.

Import the component into your page or component file.

Usage
import { Inspector, InspectorSection, InspectorProperty, InspectorNumber } from "@dethink/components";

Controlled values, dot paths, and controls that commit when you press Enter or leave the field.

Selected shape

Edit the selected card. Lock the layer to see disabled reasons, or click the card to clear the selection.

Show sourceexamples/inspector/shape-panel.tsx
examples/inspector/shape-panel.tsx
"use client";
import {
  Button,
  Inspector,
  InspectorNumber,
  InspectorProperty,
  InspectorSection,
  InspectorSelect,
  InspectorSwitch,
  InspectorText,
  type InspectorValue,
} from "@dethink/components";
import { useState } from "react";

type Shape = {
  name: string;
  visible: boolean;
  locked: boolean;
  layout: { width: number; height: number; rotation: number };
  appearance: { radius: number; opacity: number; fill: string };
};

const fills = [
  { value: "primary", label: "Primary", className: "bg-primary" },
  { value: "info", label: "Info", className: "bg-info" },
  { value: "success", label: "Success", className: "bg-success" },
  { value: "warning", label: "Warning", className: "bg-warning" },
] as const;

const initialShape: Shape = {
  name: "Hero card",
  visible: true,
  locked: false,
  layout: { width: 160, height: 104, rotation: -6 },
  appearance: { radius: 16, opacity: 90, fill: "primary" },
};

export function InspectorShapePanel() {
  const [shape, setShape] = useState<Shape | null>(initialShape);
  const fillClass =
    fills.find((fill) => fill.value === shape?.appearance.fill)?.className ??
    "bg-primary";

  return (
    <div className="grid w-full gap-4 md:grid-cols-[minmax(0,1fr)_18rem]">
      <div className="border-border bg-muted/30 relative grid min-h-72 place-items-center overflow-hidden rounded-lg border p-6">
        {shape ? (
          <button
            type="button"
            aria-label={`${shape.name}, selected. Click to deselect.`}
            onClick={() => setShape(null)}
            className={`${fillClass} ring-ring ring-offset-background max-w-full shadow-lg ring-2 ring-offset-2 motion-safe:transition-[width,height,rotate,border-radius,opacity] motion-safe:duration-200`}
            style={{
              width: shape.layout.width,
              height: shape.layout.height,
              rotate: `${shape.layout.rotation}deg`,
              borderRadius: shape.appearance.radius,
              opacity: shape.visible ? shape.appearance.opacity / 100 : 0.15,
            }}
          />
        ) : (
          <Button variant="outline" onClick={() => setShape(initialShape)}>
            Select the hero card
          </Button>
        )}
      </div>
      <Inspector
        aria-label="Shape properties"
        role="region"
        className="border-border bg-background rounded-lg border"
        value={shape}
        onValueChange={(next: InspectorValue) => setShape(next as Shape)}
      >
        <InspectorSection title="Layer">
          <InspectorProperty path="name" label="Name">
            <InspectorText
              validate={(text) => (text.trim() ? null : "Name the layer.")}
            />
          </InspectorProperty>
          <InspectorProperty path="visible" label="Visible">
            <InspectorSwitch />
          </InspectorProperty>
          <InspectorProperty path="locked" label="Locked">
            <InspectorSwitch />
          </InspectorProperty>
        </InspectorSection>
        <InspectorSection title="Layout">
          <InspectorProperty
            path="layout.width"
            label="Width"
            disabled={shape?.locked}
            disabledReason="Unlock the layer to resize it."
          >
            <InspectorNumber min={24} max={240} unit="px" />
          </InspectorProperty>
          <InspectorProperty
            path="layout.height"
            label="Height"
            disabled={shape?.locked}
            disabledReason="Unlock the layer to resize it."
          >
            <InspectorNumber min={24} max={200} unit="px" />
          </InspectorProperty>
          <InspectorProperty path="layout.rotation" label="Rotation">
            <InspectorNumber min={-180} max={180} unit="deg" />
          </InspectorProperty>
        </InspectorSection>
        <InspectorSection title="Appearance">
          <InspectorProperty path="appearance.fill" label="Fill">
            <InspectorSelect
              options={fills.map(({ value, label }) => ({ value, label }))}
            />
          </InspectorProperty>
          <InspectorProperty path="appearance.radius" label="Radius">
            <InspectorNumber min={0} max={64} unit="px" />
          </InspectorProperty>
          <InspectorProperty path="appearance.opacity" label="Opacity">
            <InspectorNumber min={0} max={100} step={5} unit="%" />
          </InspectorProperty>
        </InspectorSection>
      </Inspector>
    </div>
  );
}

Widget settings

A small panel with validation, units, and a description.

How often the widget reloads.

Show sourceexamples/inspector/basic.tsx
examples/inspector/basic.tsx
"use client";
import {
  Inspector,
  InspectorNumber,
  InspectorProperty,
  InspectorSection,
  InspectorSwitch,
  InspectorText,
  type InspectorValue,
} from "@dethink/components";
import { useState } from "react";

export function InspectorBasic() {
  const [value, setValue] = useState<InspectorValue>({
    title: "Quarterly revenue",
    refresh: 15,
    showLegend: true,
  });

  return (
    <Inspector
      aria-label="Widget settings"
      role="region"
      className="border-border bg-background mx-auto w-full max-w-xs rounded-lg border"
      value={value}
      onValueChange={(next) => setValue(next)}
    >
      <InspectorSection title="Widget">
        <InspectorProperty path="title" label="Title">
          <InspectorText
            validate={(text) => (text.trim() ? null : "Add a title.")}
          />
        </InspectorProperty>
        <InspectorProperty
          path="refresh"
          label="Refresh"
          description="How often the widget reloads."
        >
          <InspectorNumber min={1} max={120} unit="min" />
        </InspectorProperty>
        <InspectorProperty path="showLegend" label="Legend">
          <InspectorSwitch />
        </InspectorProperty>
      </InspectorSection>
    </Inspector>
  );
}
Inspector props
PropWhat it doesDefault
valueRecord<string, unknown> | nullThe selection's properties. null or undefined shows the empty state.Not set
onValueChange(next, { path, value }) => voidReceives an immutable copy with the committed change and the dot path that changed.Not set
selectionKeystring | numberIdentifies the selected object. Unfinished drafts are discarded when it changes. Pass a stable id when value is rebuilt without changing the selection.value identity
disabled / readOnlybooleanApplies to every property in the panel.false
emptyStateReactNodeReplaces the default compact EmptyState."Nothing selected"
InspectorSection props
PropWhat it doesDefault
title / descriptionReactNodeSection heading and optional hint. The heading names the section's group.Not set
collapsiblebooleanfalse renders a plain heading and keeps content visible.true
defaultOpen / open / onOpenChangeboolean / (open) => voidUncontrolled or controlled disclosure state.true
headingLevel2 | 3 | 4 | 5 | 6Match the heading outline of the host page.3
actionsReactNodeControls beside the heading, outside the toggle button.Not set
InspectorProperty props
PropWhat it doesDefault
pathstringDot path into value, such as layout.width or points.0.x.Not set
label / descriptionReactNodeVisible label (truncated when narrow) and optional hint wired to the control.Not set
disabled / readOnly / disabledReasonboolean / ReactNodeLocks the property and explains why.false
errorReactNodeConsumer error. Takes precedence over messages a control reports for invalid drafts.Not set
Property controls
PropWhat it doesDefault
InspectorNumbermin, max, step, precision, unitSpinbutton. Commits on Enter/blur, reverts on Escape, clamps, and steps with arrows; Home/End jump to bounds.step 1
InspectorTextvalidate?: (text) => string | nullCommits on Enter/blur. A returned message rejects the draft and keeps the previous value.Not set
InspectorSwitchSwitchPropsWrites true or false.Not set
InspectorSelectoptions, placeholderWrites the chosen option's string value."Select…"
useInspectorProperty() => { value, setValue, ids, … }Wire a custom control to the surrounding property.Not set

Inspector is controlled and keeps no copy of your data. Each property reads its dot path from value. Committing a change calls onValueChange with an immutable copy, leaving untouched branches as they are. Text and number fields keep a draft while you type and commit on Enter or blur. Escape reverts. Invalid drafts show a message next to the field and never overwrite the last good value. Rows stack into one column when the panel is narrower than 17rem.

Each section is a heading with a disclosure button, and its content is a group named by that heading. Collapsed content is inert. Property labels name their controls even when visually truncated, and descriptions, disabled reasons, and errors are wired as accessible descriptions. Numbers are spinbuttons that announce their unit. Give the panel an accessible name, for example with role="region" and aria-label.