NODEDC_DESIGN_GUIDELINE/packages/ui-react/src/Inspector.tsx

103 lines
3.2 KiB
TypeScript

import { useMemo, useState, type ReactNode } from "react";
import { cn } from "./cn.js";
export interface InspectorSectionSpec {
id: string;
label: string;
description?: string;
group?: string;
content: ReactNode;
disabled?: boolean;
tone?: "default" | "accent";
}
export interface InspectorProps {
sections: InspectorSectionSpec[];
defaultOpen?: string[];
activeId?: string;
singleOpen?: boolean;
className?: string;
onActiveChange?: (id: string) => void;
}
export function Inspector({
sections,
defaultOpen = [],
activeId,
singleOpen = false,
className,
onActiveChange,
}: InspectorProps) {
const [openIds, setOpenIds] = useState(() => new Set(defaultOpen));
const groups = useMemo(() => {
const result: Array<{ label?: string; sections: InspectorSectionSpec[] }> = [];
sections.forEach((section) => {
const previous = result[result.length - 1];
if (previous && previous.label === section.group) {
previous.sections.push(section);
} else {
result.push({ label: section.group, sections: [section] });
}
});
return result;
}, [sections]);
const toggle = (id: string) => {
setOpenIds((current) => {
const next = singleOpen ? new Set<string>() : new Set(current);
if (current.has(id)) next.delete(id);
else next.add(id);
return next;
});
onActiveChange?.(id);
};
return (
<div className={cn("nodedc-inspector", className)}>
{groups.map((group, groupIndex) => (
<div className="nodedc-inspector__group" key={`${group.label ?? "root"}-${groupIndex}`}>
{group.sections.map((section) => {
const isOpen = openIds.has(section.id);
return (
<section className="nodedc-inspector__section" key={section.id}>
<button
type="button"
className="nodedc-inspector__section-trigger"
data-open={isOpen ? "true" : undefined}
data-active={activeId === section.id ? "true" : undefined}
data-tone={section.tone === "accent" ? "accent" : undefined}
aria-expanded={isOpen}
disabled={section.disabled}
onClick={() => toggle(section.id)}
>
<span className="nodedc-inspector__section-label">{section.label}</span>
{section.description ? (
<span className="nodedc-inspector__section-description">{section.description}</span>
) : null}
</button>
{isOpen ? <div className="nodedc-inspector__section-content">{section.content}</div> : null}
</section>
);
})}
</div>
))}
</div>
);
}
export interface ControlRowProps {
label: ReactNode;
children: ReactNode;
layout?: "inline" | "stack";
className?: string;
}
export function ControlRow({ label, children, layout = "inline", className }: ControlRowProps) {
return (
<div className={cn("nodedc-control-row", className)} data-layout={layout === "inline" ? undefined : layout}>
<div className="nodedc-control-row__label">{label}</div>
<div className="nodedc-control-row__control">{children}</div>
</div>
);
}