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 componentsAnimations, transitions, and reduced-motion handling.
- React Aria
(react-aria)· via shared Dethink componentsAccessibility hooks and overlay utilities used by the implementation.
- React Aria Components
(react-aria-components)· via shared Dethink componentsAccessible 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.
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
"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
"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>
);
}| Prop | What it does | Default |
|---|---|---|
valueRecord<string, unknown> | null | The selection's properties. null or undefined shows the empty state. | Not set |
onValueChange(next, { path, value }) => void | Receives an immutable copy with the committed change and the dot path that changed. | Not set |
selectionKeystring | number | Identifies 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 / readOnlyboolean | Applies to every property in the panel. | false |
emptyStateReactNode | Replaces the default compact EmptyState. | "Nothing selected" |
| Prop | What it does | Default |
|---|---|---|
title / descriptionReactNode | Section heading and optional hint. The heading names the section's group. | Not set |
collapsibleboolean | false renders a plain heading and keeps content visible. | true |
defaultOpen / open / onOpenChangeboolean / (open) => void | Uncontrolled or controlled disclosure state. | true |
headingLevel2 | 3 | 4 | 5 | 6 | Match the heading outline of the host page. | 3 |
actionsReactNode | Controls beside the heading, outside the toggle button. | Not set |
| Prop | What it does | Default |
|---|---|---|
pathstring | Dot path into value, such as layout.width or points.0.x. | Not set |
label / descriptionReactNode | Visible label (truncated when narrow) and optional hint wired to the control. | Not set |
disabled / readOnly / disabledReasonboolean / ReactNode | Locks the property and explains why. | false |
errorReactNode | Consumer error. Takes precedence over messages a control reports for invalid drafts. | Not set |
| Prop | What it does | Default |
|---|---|---|
InspectorNumbermin, max, step, precision, unit | Spinbutton. Commits on Enter/blur, reverts on Escape, clamps, and steps with arrows; Home/End jump to bounds. | step 1 |
InspectorTextvalidate?: (text) => string | null | Commits on Enter/blur. A returned message rejects the draft and keeps the previous value. | Not set |
InspectorSwitchSwitchProps | Writes true or false. | Not set |
InspectorSelectoptions, placeholder | Writes 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.