Skip to content
6

Inspector Panel

Design-tool inspector with a header, collapsible sections and label/control rows. Composable parts for property panels and sidebars.

Preview

Installation

npx shadcn@latest add https://hirael.com/r/inspector-panel.json

Code

components/blocks/inspector-panel.tsx
'use client';

import * as React from 'react';
import { ChevronDown } from 'lucide-react';

import { cn } from '@/lib/utils';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/hirael/bases/radix/ui/collapsible';

type InspectorPanelProps = React.ComponentProps<'aside'>;

const InspectorPanel = ({ className, ...props }: InspectorPanelProps) => {
  return (
    <aside
      data-slot="inspector-panel"
      className={cn(
        'flex w-full max-w-xs flex-col overflow-hidden rounded-lg border border-border bg-card text-card-foreground',
        className,
      )}
      {...props}
    />
  );
};

type InspectorPanelHeaderProps = React.ComponentProps<'div'>;

const InspectorPanelHeader = ({ className, ...props }: InspectorPanelHeaderProps) => {
  return (
    <div
      data-slot="inspector-panel-header"
      className={cn('flex items-center justify-between gap-2 border-b border-border px-3 py-2.5', className)}
      {...props}
    />
  );
};

type InspectorPanelTitleProps = React.ComponentProps<'p'>;

const InspectorPanelTitle = ({ className, ...props }: InspectorPanelTitleProps) => {
  return (
    <p
      data-slot="inspector-panel-title"
      className={cn('font-mono text-[11px] uppercase tracking-[0.1em] text-muted-foreground', className)}
      {...props}
    />
  );
};

interface InspectorPanelSectionProps extends Omit<React.ComponentProps<'div'>, 'title'> {
  title: React.ReactNode;
  defaultOpen?: boolean;
}

const InspectorPanelSection = ({
  title,
  defaultOpen = true,
  className,
  children,
  ...props
}: InspectorPanelSectionProps) => {
  return (
    <Collapsible
      defaultOpen={defaultOpen}
      data-slot="inspector-panel-section"
      className={cn('border-b border-border last:border-b-0', className)}
      {...props}
    >
      <CollapsibleTrigger className="group flex w-full items-center justify-between gap-2 px-3 py-2 text-start transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
        <span className="text-xs font-medium text-foreground">{title}</span>
        <ChevronDown
          aria-hidden
          className="size-3.5 text-muted-foreground transition-transform duration-200 group-data-[state=open]:rotate-180"
        />
      </CollapsibleTrigger>
      <CollapsibleContent className="flex flex-col gap-2 px-3 pb-3 pt-1">{children}</CollapsibleContent>
    </Collapsible>
  );
};

interface InspectorPanelRowProps extends React.ComponentProps<'div'> {
  label: React.ReactNode;
}

const InspectorPanelRow = ({ label, className, children, ...props }: InspectorPanelRowProps) => {
  return (
    <div
      data-slot="inspector-panel-row"
      className={cn('grid grid-cols-[minmax(0,1fr)_minmax(0,1.4fr)] items-center gap-2', className)}
      {...props}
    >
      <span className="truncate text-xs text-muted-foreground">{label}</span>
      <div className="flex items-center justify-end gap-1.5">{children}</div>
    </div>
  );
};

export { InspectorPanel, InspectorPanelHeader, InspectorPanelTitle, InspectorPanelSection, InspectorPanelRow };

const InspectorField = ({ className, children }: { className?: string; children: React.ReactNode }) => {
  return (
    <span
      className={cn(
        'inline-flex h-7 w-full items-center rounded-md border border-border bg-background px-2 font-mono text-xs text-foreground',
        className,
      )}
    >
      {children}
    </span>
  );
};

const InspectorPanelBlock = () => {
  return (
    <section data-slot="inspector-panel-block" className="flex w-full justify-center bg-background p-6 sm:p-10">
      <InspectorPanel>
        <InspectorPanelHeader>
          <InspectorPanelTitle>Inspector</InspectorPanelTitle>
          <span className="text-xs text-muted-foreground">Frame 12</span>
        </InspectorPanelHeader>

        <InspectorPanelSection title="Layout">
          <InspectorPanelRow label="Width">
            <InspectorField>320</InspectorField>
          </InspectorPanelRow>
          <InspectorPanelRow label="Height">
            <InspectorField>192</InspectorField>
          </InspectorPanelRow>
          <InspectorPanelRow label="Radius">
            <InspectorField>12</InspectorField>
          </InspectorPanelRow>
        </InspectorPanelSection>

        <InspectorPanelSection title="Appearance">
          <InspectorPanelRow label="Fill">
            <span className="relative w-full">
              <span
                aria-hidden
                className="absolute start-2 top-1/2 size-4 -translate-y-1/2 rounded-sm border border-border bg-foreground/80"
              />
              <InspectorField className="ps-8">#18181B</InspectorField>
            </span>
          </InspectorPanelRow>
          <InspectorPanelRow label="Opacity">
            <InspectorField>100%</InspectorField>
          </InspectorPanelRow>
        </InspectorPanelSection>

        <InspectorPanelSection title="Typography" defaultOpen={false}>
          <InspectorPanelRow label="Size">
            <InspectorField>14</InspectorField>
          </InspectorPanelRow>
          <InspectorPanelRow label="Weight">
            <InspectorField>Medium</InspectorField>
          </InspectorPanelRow>
        </InspectorPanelSection>
      </InspectorPanel>
    </section>
  );
};

export default InspectorPanelBlock;

Dependencies

shadcn registry

collapsible

npm

lucide-react