Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 68 additions & 10 deletions src/routes/Dashboard/DashboardComponentsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ import { getComponentName } from "@/utils/getComponentName";

const PUBLISHED_COMPONENTS_URL = "/api/published_components/";

/**
* Sticky source column height when embedded in a maximized editor window.
* Uses dynamic viewport height minus approximate window header + padding (floating
* and docked maximized headers differ slightly; this is a single conservative value).
*/
const EDITOR_EMBED_SOURCE_STICKY_OFFSET_PX = 100;

interface DashboardComponentsViewProps {
/** When true, fills the parent window, uses controlled selection, no dashboard navigation. */
embedInEditorWindow?: boolean;
selectedComponentDigest?: string;
onSelectComponentDigest?: (digest: string) => void;
}

// ─── Collapsible section header ──────────────────────────────────────────────

const CollapsibleSection = ({
Expand Down Expand Up @@ -418,7 +432,13 @@ const CompactIO = ({

// ─── Detail Panel (Suspense) ────────────────────────────────────────────────

const ComponentDetailInner = ({ digest }: { digest: string }) => {
const ComponentDetailInner = ({
digest,
embedLayout = false,
}: {
digest: string;
embedLayout?: boolean;
}) => {
const { backendUrl } = useBackend();
const componentRef: ComponentReference = {
digest,
Expand Down Expand Up @@ -526,7 +546,11 @@ const ComponentDetailInner = ({ digest }: { digest: string }) => {
<div className="flex-3 min-w-0">
<div
className="sticky top-0 flex flex-col gap-1.5"
style={{ height: `calc(100vh - ${TOP_NAV_HEIGHT + 48}px)` }}
style={{
height: embedLayout
? `calc(100dvh - ${EDITOR_EMBED_SOURCE_STICKY_OFFSET_PX}px)`
: `calc(100vh - ${TOP_NAV_HEIGHT + 48}px)`,
}}
>
<Text
size="xs"
Expand All @@ -550,27 +574,53 @@ const ComponentDetailInner = ({ digest }: { digest: string }) => {

// ─── Main View ──────────────────────────────────────────────────────────────

export function DashboardComponentsView() {
export function DashboardComponentsView({
embedInEditorWindow = false,
selectedComponentDigest,
onSelectComponentDigest,
}: DashboardComponentsViewProps = {}) {
const [query, setQuery] = useState("");
const navigate = useNavigate();
// useSearch strict:false is required here — this route has no validateSearch defined
const { component: selectedDigest } = useSearch({ strict: false }) as {
const { component: routeSelectedDigest } = useSearch({ strict: false }) as {
component?: string;
};
const selectedDigest = embedInEditorWindow
? selectedComponentDigest
: routeSelectedDigest;

const handleSelect = (component: ComponentReference) => {
const digest = component.digest;
if (!digest) return;
if (embedInEditorWindow) {
onSelectComponentDigest?.(digest);
return;
}
navigate({
to: APP_ROUTES.DASHBOARD_COMPONENTS,
search: { component: component.digest },
search: { component: digest },
});
};

return (
<div
className="flex -mt-4 -mb-6 -mx-8 overflow-hidden border-t border-border"
style={{ height: `calc(100vh - ${TOP_NAV_HEIGHT}px)` }}
className={cn(
"flex overflow-hidden border-t border-border",
embedInEditorWindow ? "h-full min-h-0" : "-mt-4 -mb-6 -mx-8",
)}
style={
embedInEditorWindow
? undefined
: { height: `calc(100vh - ${TOP_NAV_HEIGHT}px)` }
}
>
{/* Left: component list */}
<div className="w-64 shrink-0 border-r border-border flex flex-col overflow-hidden">
<div
className={cn(
"w-64 shrink-0 border-r border-border flex flex-col overflow-hidden",
embedInEditorWindow && "min-h-0",
)}
>
<div className="p-3 border-b border-border shrink-0">
<Input
placeholder="Search components..."
Expand All @@ -588,7 +638,12 @@ export function DashboardComponentsView() {
</div>

{/* Right: detail panel — single scroll, source sticky on right */}
<div className="flex-1 min-w-0 overflow-y-auto p-6">
<div
className={cn(
"flex-1 min-w-0 overflow-y-auto p-6",
embedInEditorWindow && "min-h-0",
)}
>
{selectedDigest ? (
<SuspenseWrapper
fallback={
Expand All @@ -603,7 +658,10 @@ export function DashboardComponentsView() {
</InlineStack>
}
>
<ComponentDetailInner digest={selectedDigest} />
<ComponentDetailInner
digest={selectedDigest}
embedLayout={embedInEditorWindow}
/>
</SuspenseWrapper>
) : (
<InlineStack fill>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import { observer } from "mobx-react-lite";
import { useState } from "react";

import GraphComponents from "@/components/shared/ReactFlow/FlowSidebar/sections/GraphComponents";
import { BlockStack } from "@/components/ui/layout";
import { DashboardComponentsView } from "@/routes/Dashboard/DashboardComponentsView";
import { useWindowContext } from "@/routes/v2/shared/windows/ContentWindowStateContext";

/**
* Wrapper component that renders the component library inside a Window.
* GraphComponents is designed for the sidebar but works well in a window context
* when rendered in "open" (expanded) mode.
* GraphComponents is used when the window is not maximized; when maximized,
* the full dashboard-style browser is shown instead.
*/
export function ComponentLibraryContent() {
return (
<BlockStack className="h-full overflow-y-auto">
<GraphComponents />
</BlockStack>
);
}
export const ComponentLibraryContent = observer(
function ComponentLibraryContent() {
const { model } = useWindowContext();
const [selectedComponentDigest, setSelectedComponentDigest] = useState<
string | undefined
>();

if (model.isMaximized) {
return (
<BlockStack className="h-full min-h-0 overflow-hidden">
<DashboardComponentsView
embedInEditorWindow
selectedComponentDigest={selectedComponentDigest}
onSelectComponentDigest={setSelectedComponentDigest}
/>
</BlockStack>
);
}

return (
<BlockStack className="h-full min-h-0 overflow-y-auto">
<GraphComponents />
</BlockStack>
);
},
);
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { useNavigate } from "@tanstack/react-router";
import { useRef } from "react";

import { ImportComponent } from "@/components/shared/ReactFlow/FlowSidebar/components";
Expand All @@ -10,11 +9,11 @@ import {
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Icon } from "@/components/ui/icon";
import { APP_ROUTES } from "@/routes/router";
import { MenuTriggerButton } from "@/routes/v2/shared/components/MenuTriggerButton";
import { useSharedStores } from "@/routes/v2/shared/store/SharedStoreContext";

export function ComponentsLibraryMenu() {
const navigate = useNavigate();
const { windows } = useSharedStores();
const importTriggerRef = useRef<HTMLButtonElement>(null);

return (
Expand All @@ -25,9 +24,7 @@ export function ComponentsLibraryMenu() {
</DropdownMenuTrigger>
<DropdownMenuContent align="start" sideOffset={2}>
<DropdownMenuItem
onClick={() =>
void navigate({ to: APP_ROUTES.DASHBOARD_COMPONENTS })
}
onClick={() => windows.maximizeWindow("component-library")}
>
<Icon name="Library" size="sm" />
Explore library
Expand Down
4 changes: 4 additions & 0 deletions src/routes/v2/shared/windows/windowStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ export class WindowStoreImpl implements WindowStoreRef {
this.windows[id]?.restore();
}

maximizeWindow(id: string): void {
this.windows[id]?.maximize();
}

/** Get a window model by ID */
getWindowById(id: string): WindowModel | undefined {
return this.windows[id];
Expand Down
Loading