+
+ {assetToolsEnabled && (
+
-
Active mode
-
- {(['draw', 'text', 'shape'] as const).map(mode => (
-
- ))}
+ {reviewToolsEnabled && (
+
+ {reviewTools}
+
+ )}
+
+
+ {[
+ { label: 'Asset tools', enabled: assetToolsEnabled, toggle: () => setAssetToolsEnabled(v => !v) },
+ { label: 'Review tools', enabled: reviewToolsEnabled, toggle: () => setReviewToolsEnabled(v => !v) },
+ ].map(({ label, enabled, toggle }) => (
+
+ {label}
+
+ ))}
+
+
+
+ );
+ };
+
+ return
;
+ },
+};
+
+/**
+ * Demonstrates editor-specific toolbar layouts (Canvas, Text, Schema) with a
+ * smooth transition when switching editor type.
+ *
+ * The transition is driven by `ToolbarLayout` itself, so the story only swaps
+ * slot content and the toolbar handles fade/resize animation internally.
+ */
+export const LayoutWithSmoothEditorTransitions: Story = {
+ render: () => {
+ const LayoutWithSmoothEditorTransitionsDemo = () => {
+ const [activeEditor, setActiveEditor] = useState<'canvas' | 'text' | 'schema'>('canvas');
+
+ const canvasTools = useMemo(
+ () => (
+
+
+
+
+
+ ),
+ []
+ );
+
+ const textTools = useMemo(
+ () => (
+
+
+
+
+
+ ),
+ []
+ );
+
+ const schemaTools = useMemo(
+ () => (
+ <>
+
+
+
+
+
+
+
+
+ >
+ ),
+ []
+ );
+
+ const editorTools = {
+ canvas: canvasTools,
+ text: textTools,
+ schema: schemaTools,
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {editorTools[activeEditor]}
+
+
+
+
Active editor
+
+ {(['canvas', 'text', 'schema'] as const).map(editor => (
+
+ ))}
-
- );
+
+
+ );
+ };
+
+ return
;
+ },
+};
+
+/**
+ * Shows app-level layout regions where one `ToolbarLayout` stays stable for
+ * global actions while another layout swaps by editor type.
+ */
+export const LayoutWithGlobalAndEditorRegions: Story = {
+ render: () => {
+ const LayoutWithGlobalAndEditorRegionsDemo = () => {
+ const [activeEditor, setActiveEditor] = useState<'page' | 'workflow'>('page');
+
+ const globalActions = useMemo(
+ () => (
+
+
+
+
+ ),
+ []
+ );
+
+ const pageEditorTools = useMemo(
+ () => (
+
+
+
+
+ ),
+ []
+ );
+
+ const workflowEditorTools = useMemo(
+ () => (
+
+
+
+
+
+ ),
+ []
+ );
+
+ const editorRegionTools = {
+ page: pageEditorTools,
+ workflow: workflowEditorTools,
};
- return
;
- },
- };
+ return (
+
+
+
+
+
+
+
+
+
+ {globalActions}
+
+
+
+ {editorRegionTools[activeEditor]}
+
+
+
+ {(['page', 'workflow'] as const).map(editor => (
+
+ ))}
+
+
+
+ );
+ };
+
+ return
;
+ },
+};
diff --git a/Source/Toolbar/ToolbarLayout.tsx b/Source/Toolbar/ToolbarLayout.tsx
index 25aaf75..0878f61 100644
--- a/Source/Toolbar/ToolbarLayout.tsx
+++ b/Source/Toolbar/ToolbarLayout.tsx
@@ -1,9 +1,95 @@
// Copyright (c) Cratis. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
-import { ReactNode } from 'react';
+import { Children, ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useToolbarSlot } from './ToolbarSlot';
+/** How long the fade-out animation runs (ms). React unmounts exiting content after this. */
+const LAYOUT_TRANSITION_MS = 220;
+
+/**
+ * Renders toolbar layout content with a cross-fade and size-morph transition.
+ *
+ * - The container resizes smoothly to fit incoming layout content.
+ * - Outgoing content fades out while incoming content fades in.
+ */
+const LayoutTransition = ({ items, flexClass }: { items: ReactNode[]; flexClass: string }) => {
+ const [current, setCurrent] = useState
(items);
+ const [exiting, setExiting] = useState([]);
+ const [exitRevision, setExitRevision] = useState(0);
+
+ const currentRef = useRef(current);
+ currentRef.current = current;
+
+ const incomingRef = useRef(null);
+ const [size, setSize] = useState<{ width: number; height: number } | null>(null);
+ const timerRef = useRef | undefined>(undefined);
+ const hasMountedRef = useRef(false);
+
+ const measureIncoming = useCallback(() => {
+ if (incomingRef.current) {
+ setSize({
+ width: incomingRef.current.offsetWidth,
+ height: incomingRef.current.offsetHeight,
+ });
+ }
+ }, []);
+
+ useLayoutEffect(() => {
+ measureIncoming();
+ }, []); // mount only
+
+ useEffect(() => {
+ if (items === currentRef.current) return;
+ const old = currentRef.current;
+
+ if (timerRef.current !== undefined) clearTimeout(timerRef.current);
+ setCurrent(items);
+
+ if (old.length > 0) {
+ setExiting(old);
+ setExitRevision(revision => revision + 1);
+ timerRef.current = setTimeout(() => setExiting([]), LAYOUT_TRANSITION_MS);
+ }
+ }, [items]);
+
+ useEffect(() => {
+ if (!hasMountedRef.current) {
+ hasMountedRef.current = true;
+ return;
+ }
+ measureIncoming();
+ }, [current, measureIncoming]);
+
+ useEffect(() => () => {
+ if (timerRef.current !== undefined) clearTimeout(timerRef.current);
+ }, []);
+
+ if (current.length === 0 && exiting.length === 0) return null;
+
+ return (
+
+
+ {current}
+
+ {exiting.length > 0 && (
+
+ {exiting}
+
+ )}
+
+ );
+};
+
/** Props for the {@link ToolbarLayout} component. */
export interface ToolbarLayoutProps {
/**
@@ -71,13 +157,14 @@ export interface ToolbarLayoutProps {
export const ToolbarLayout = ({ name, children, orientation = 'vertical' }: ToolbarLayoutProps) => {
const slotItems = useToolbarSlot(name);
const flexClass = orientation === 'horizontal' ? 'flex-row' : 'flex-col';
+ const fallbackItems = useMemo(() => Children.toArray(children), [children]);
+ const items = slotItems.length > 0 ? slotItems : fallbackItems;
- const content = slotItems.length > 0 ? slotItems : children;
- if (content == null) return null;
+ if (items.length === 0) return null;
return (
- {content}
+
);
};