mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-03-24 03:37:51 -04:00
Redesign experiment designer workspace and seed Bucknell data
- Overhauled designer UI: virtualized flow, slim action panel, improved drag - Added Bucknell studies, users, and NAO plugin to seed-dev script - Enhanced validation panel and inspector UX - Updated wizard-actions plugin options formatting - Removed Minio from docker-compose for local dev - Numerous UI and code quality improvements for experiment design
This commit is contained in:
@@ -89,9 +89,21 @@ export function InspectorPanel({
|
||||
/* ------------------------------------------------------------------------ */
|
||||
/* Local Active Tab State (uncontrolled mode) */
|
||||
/* ------------------------------------------------------------------------ */
|
||||
const INSPECTOR_TAB_STORAGE_KEY = "hristudio-designer-inspector-tab-v1";
|
||||
const [internalTab, setInternalTab] = useState<
|
||||
"properties" | "issues" | "dependencies"
|
||||
>(() => {
|
||||
try {
|
||||
const raw =
|
||||
typeof window !== "undefined"
|
||||
? localStorage.getItem(INSPECTOR_TAB_STORAGE_KEY)
|
||||
: null;
|
||||
if (raw === "properties" || raw === "issues" || raw === "dependencies") {
|
||||
return raw;
|
||||
}
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
if (selectedStepId) return "properties";
|
||||
return "issues";
|
||||
});
|
||||
@@ -103,6 +115,25 @@ export function InspectorPanel({
|
||||
if (!autoFocusOnSelection) return;
|
||||
if (selectedStepId || selectedActionId) {
|
||||
setInternalTab("properties");
|
||||
// Scroll properties panel to top and focus first field
|
||||
requestAnimationFrame(() => {
|
||||
const activeTabpanel = document.querySelector(
|
||||
'[role="tabpanel"][data-state="active"]',
|
||||
);
|
||||
if (!(activeTabpanel instanceof HTMLElement)) return;
|
||||
const viewportEl = activeTabpanel.querySelector(
|
||||
'[data-slot="scroll-area-viewport"]',
|
||||
);
|
||||
if (viewportEl instanceof HTMLElement) {
|
||||
viewportEl.scrollTop = 0;
|
||||
const firstField = viewportEl.querySelector(
|
||||
"input, select, textarea, button",
|
||||
);
|
||||
if (firstField instanceof HTMLElement) {
|
||||
firstField.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [selectedStepId, selectedActionId, autoFocusOnSelection]);
|
||||
|
||||
@@ -113,6 +144,11 @@ export function InspectorPanel({
|
||||
onTabChange?.(val);
|
||||
} else {
|
||||
setInternalTab(val);
|
||||
try {
|
||||
localStorage.setItem(INSPECTOR_TAB_STORAGE_KEY, val);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -164,9 +200,12 @@ export function InspectorPanel({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background/40 flex h-full flex-col border-l backdrop-blur-sm",
|
||||
"bg-background/40 border-border relative flex h-full min-w-0 flex-col overflow-hidden border-l backdrop-blur-sm",
|
||||
className,
|
||||
)}
|
||||
style={{ contain: "layout paint size" }}
|
||||
role="complementary"
|
||||
aria-label="Inspector panel"
|
||||
>
|
||||
{/* Tab Header */}
|
||||
<div className="border-b px-2 py-1.5">
|
||||
@@ -175,41 +214,41 @@ export function InspectorPanel({
|
||||
onValueChange={handleTabChange}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid h-8 grid-cols-3">
|
||||
<TabsList className="flex h-9 w-full items-center gap-1 overflow-hidden">
|
||||
<TabsTrigger
|
||||
value="properties"
|
||||
className="flex items-center gap-1 text-[11px]"
|
||||
className="flex min-w-0 flex-1 items-center justify-center gap-1 truncate text-[11px]"
|
||||
title="Properties (Step / Action)"
|
||||
>
|
||||
<Settings className="h-3 w-3" />
|
||||
<Settings className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">Props</span>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="issues"
|
||||
className="flex items-center gap-1 text-[11px]"
|
||||
className="flex min-w-0 flex-1 items-center justify-center gap-1 truncate text-[11px]"
|
||||
title="Validation Issues"
|
||||
>
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
<AlertTriangle className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">
|
||||
Issues{issueCount > 0 ? ` (${issueCount})` : ""}
|
||||
</span>
|
||||
{issueCount > 0 && (
|
||||
<span className="text-amber-600 sm:hidden dark:text-amber-400">
|
||||
<span className="xs:hidden text-amber-600 dark:text-amber-400">
|
||||
{issueCount}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="dependencies"
|
||||
className="flex items-center gap-1 text-[11px]"
|
||||
className="flex min-w-0 flex-1 items-center justify-center gap-1 truncate text-[11px]"
|
||||
title="Dependencies / Drift"
|
||||
>
|
||||
<PackageSearch className="h-3 w-3" />
|
||||
<PackageSearch className="h-3 w-3 flex-shrink-0" />
|
||||
<span className="hidden sm:inline">
|
||||
Deps{driftCount > 0 ? ` (${driftCount})` : ""}
|
||||
</span>
|
||||
{driftCount > 0 && (
|
||||
<span className="text-purple-600 sm:hidden dark:text-purple-400">
|
||||
<span className="xs:hidden text-purple-600 dark:text-purple-400">
|
||||
{driftCount}
|
||||
</span>
|
||||
)}
|
||||
@@ -220,11 +259,15 @@ export function InspectorPanel({
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex min-h-0 flex-1 flex-col">
|
||||
{/*
|
||||
Force consistent width for tab bodies to prevent reflow when
|
||||
switching between content with different intrinsic widths.
|
||||
*/}
|
||||
<Tabs value={effectiveTab}>
|
||||
{/* Properties */}
|
||||
<TabsContent
|
||||
value="properties"
|
||||
className="m-0 flex h-full flex-col data-[state=inactive]:hidden"
|
||||
className="m-0 flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
|
||||
>
|
||||
{propertiesEmpty ? (
|
||||
<div className="text-muted-foreground flex h-full flex-col items-center justify-center gap-3 p-4 text-center">
|
||||
@@ -240,7 +283,7 @@ export function InspectorPanel({
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3">
|
||||
<div className="w-full px-3 py-3">
|
||||
<PropertiesPanel
|
||||
design={{
|
||||
id: "design",
|
||||
@@ -263,35 +306,46 @@ export function InspectorPanel({
|
||||
{/* Issues */}
|
||||
<TabsContent
|
||||
value="issues"
|
||||
className="m-0 flex h-full flex-col data-[state=inactive]:hidden"
|
||||
className="m-0 flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
|
||||
>
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3">
|
||||
<ValidationPanel
|
||||
issues={validationIssues}
|
||||
onIssueClick={(issue) => {
|
||||
if (issue.stepId) {
|
||||
selectStep(issue.stepId);
|
||||
if (issue.actionId) {
|
||||
selectAction(issue.stepId, issue.actionId);
|
||||
if (autoFocusOnSelection) {
|
||||
handleTabChange("properties");
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<ValidationPanel
|
||||
issues={validationIssues}
|
||||
entityLabelForId={(entityId) => {
|
||||
if (entityId.startsWith("action-")) {
|
||||
for (const s of steps) {
|
||||
const a = s.actions.find((x) => x.id === entityId);
|
||||
if (a) return `${a.name} • ${s.name}`;
|
||||
}
|
||||
}
|
||||
if (entityId.startsWith("step-")) {
|
||||
const st = steps.find((s) => s.id === entityId);
|
||||
if (st) return st.name;
|
||||
}
|
||||
return "Unknown";
|
||||
}}
|
||||
onIssueClick={(issue) => {
|
||||
if (issue.stepId) {
|
||||
selectStep(issue.stepId);
|
||||
if (issue.actionId) {
|
||||
selectAction(issue.stepId, issue.actionId);
|
||||
} else {
|
||||
selectAction(issue.stepId, undefined);
|
||||
}
|
||||
if (autoFocusOnSelection) {
|
||||
handleTabChange("properties");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Dependencies */}
|
||||
<TabsContent
|
||||
value="dependencies"
|
||||
className="m-0 flex h-full flex-col data-[state=inactive]:hidden"
|
||||
className="m-0 flex min-h-0 flex-1 flex-col data-[state=inactive]:hidden"
|
||||
>
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-3">
|
||||
<div className="w-full px-3 py-3">
|
||||
<DependencyInspector
|
||||
steps={steps}
|
||||
actionSignatureDrift={actionSignatureDrift}
|
||||
|
||||
Reference in New Issue
Block a user