From 36945deaa56f309683cf9cb90dde4f179b104f3e Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 26 Jan 2026 14:56:06 -0800 Subject: [PATCH 01/19] improvement(preview): consolidate block rendering and fix handle configurations (#3013) * improvement(preview): consolidate block rendering and fix handle configurations * refactor(preview): extract SubflowContainerProps interface --- .../templates/components/template-card.tsx | 1 + .../templates/components/template-card.tsx | 1 + .../components/template/template.tsx | 1 + .../panel/components/editor/editor.tsx | 1 + .../w/components/preview/components/block.tsx | 317 +++++++++++++++--- .../components/preview/components/subflow.tsx | 113 ------- .../w/components/preview/preview.tsx | 140 ++++---- 7 files changed, 339 insertions(+), 235 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx diff --git a/apps/sim/app/templates/components/template-card.tsx b/apps/sim/app/templates/components/template-card.tsx index 0be5079e0..3c1c32a41 100644 --- a/apps/sim/app/templates/components/template-card.tsx +++ b/apps/sim/app/templates/components/template-card.tsx @@ -207,6 +207,7 @@ function TemplateCardInner({ isPannable={false} defaultZoom={0.8} fitPadding={0.2} + lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx index b32db3ff9..5bb5886d1 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/components/template-card.tsx @@ -214,6 +214,7 @@ function TemplateCardInner({ defaultZoom={0.8} fitPadding={0.2} cursorStyle='pointer' + lightweight /> ) : (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx index 415c830f0..6ff75f6f6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/template/template.tsx @@ -446,6 +446,7 @@ const OGCaptureContainer = forwardRef((_, ref) => { isPannable={false} defaultZoom={0.8} fitPadding={0.2} + lightweight />
) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx index 762ad1f2d..cc124bdd6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/editor.tsx @@ -466,6 +466,7 @@ export function Editor() { defaultZoom={0.6} fitPadding={0.15} cursorStyle='grab' + lightweight />
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx index a56e881de..84f53b415 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/block.tsx @@ -1,6 +1,7 @@ 'use client' import { memo, useMemo } from 'react' +import { RepeatIcon, SplitIcon } from 'lucide-react' import { Handle, type NodeProps, Position } from 'reactflow' import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' import { @@ -10,14 +11,12 @@ import { isSubBlockVisibleForMode, } from '@/lib/workflows/subblocks/visibility' import { getDisplayValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block' +import type { ExecutionStatus } from '@/app/workspace/[workspaceId]/w/components/preview/preview' import { getBlock } from '@/blocks' import { SELECTOR_TYPES_HYDRATION_REQUIRED, type SubBlockConfig } from '@/blocks/types' import { useVariablesStore } from '@/stores/panel/variables/store' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' -/** Execution status for blocks in preview mode */ -type ExecutionStatus = 'success' | 'error' | 'not-executed' - /** Subblock value structure matching workflow state */ interface SubBlockValueEntry { value: unknown @@ -35,6 +34,16 @@ interface WorkflowPreviewBlockData { executionStatus?: ExecutionStatus /** Subblock values from the workflow state */ subBlockValues?: Record + /** Skips expensive subblock computations for thumbnails */ + lightweight?: boolean + /** Whether this is a subflow container (loop/parallel) */ + isSubflow?: boolean + /** Type of subflow container */ + subflowKind?: 'loop' | 'parallel' + /** Width of subflow container */ + width?: number + /** Height of subflow container */ + height?: number } /** @@ -204,21 +213,16 @@ function resolveToolsDisplay( * - Shows '-' for other selector types that need hydration */ function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) { - // Mask password fields const isPasswordField = subBlock?.password === true const maskedValue = isPasswordField && value && value !== '-' ? '•••' : null - // Resolve various display names (synchronous access, matching WorkflowBlock priority) const dropdownLabel = resolveDropdownLabel(subBlock, rawValue) const variablesDisplay = resolveVariablesDisplay(subBlock, rawValue) const toolsDisplay = resolveToolsDisplay(subBlock, rawValue) const workflowName = resolveWorkflowName(subBlock, rawValue) - // Check if this is a selector type that needs hydration (show '-' for raw IDs) const isSelectorType = subBlock?.type && SELECTOR_TYPES_HYDRATION_REQUIRED.includes(subBlock.type) - // Compute final display value matching WorkflowBlock logic - // Priority order matches WorkflowBlock: masked > hydrated names > selector fallback > raw value const hydratedName = dropdownLabel || variablesDisplay || toolsDisplay || workflowName const displayValue = maskedValue || hydratedName || (isSelectorType && value ? '-' : value) @@ -242,11 +246,115 @@ function SubBlockRow({ title, value, subBlock, rawValue }: SubBlockRowProps) { ) } +interface SubflowContainerProps { + name: string + width?: number + height?: number + kind: 'loop' | 'parallel' + isPreviewSelected?: boolean +} + +/** + * Renders a subflow container (loop/parallel) for preview mode. + */ +function SubflowContainer({ + name, + width = 500, + height = 300, + kind, + isPreviewSelected = false, +}: SubflowContainerProps) { + const isLoop = kind === 'loop' + const BlockIcon = isLoop ? RepeatIcon : SplitIcon + const blockIconBg = isLoop ? '#2FB3FF' : '#FEE12B' + const blockName = name || (isLoop ? 'Loop' : 'Parallel') + + const startHandleId = isLoop ? 'loop-start-source' : 'parallel-start-source' + const endHandleId = isLoop ? 'loop-end-source' : 'parallel-end-source' + + const leftHandleClass = + '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none' + const rightHandleClass = + '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none' + + return ( +
+ {/* Selection ring overlay */} + {isPreviewSelected && ( +
+ )} + + {/* Target handle on left (input to the subflow) */} + + + {/* Header - matches actual subflow header structure */} +
+
+
+ +
+ + {blockName} + +
+
+ + {/* Content area - matches workflow structure */} +
+ {/* Subflow Start - connects to first block in subflow */} +
+ Start + +
+
+ + {/* End source handle on right (output from the subflow) */} + +
+ ) +} + /** * Preview block component for workflow visualization. * Renders block header, subblock values, and handles without * hooks, store subscriptions, or interactive features. * Matches the visual structure of WorkflowBlock exactly. + * Also handles subflow containers (loop/parallel) when isSubflow is true. */ function WorkflowPreviewBlockInner({ data }: NodeProps) { const { @@ -258,53 +366,84 @@ function WorkflowPreviewBlockInner({ data }: NodeProps isPreviewSelected = false, executionStatus, subBlockValues, + lightweight = false, + isSubflow = false, + subflowKind, + width, + height, } = data + if (isSubflow && subflowKind) { + return ( + + ) + } + const blockConfig = getBlock(type) const canonicalIndex = useMemo( - () => buildCanonicalIndex(blockConfig?.subBlocks || []), - [blockConfig?.subBlocks] + () => + lightweight + ? { groupsById: {}, canonicalIdBySubBlockId: {} } + : buildCanonicalIndex(blockConfig?.subBlocks || []), + [blockConfig?.subBlocks, lightweight] ) const rawValues = useMemo(() => { - if (!subBlockValues) return {} + if (lightweight || !subBlockValues) return {} return Object.entries(subBlockValues).reduce>((acc, [key, entry]) => { acc[key] = extractValue(entry) return acc }, {}) - }, [subBlockValues]) + }, [subBlockValues, lightweight]) const visibleSubBlocks = useMemo(() => { - if (!blockConfig?.subBlocks) return [] + if (lightweight || !blockConfig?.subBlocks) return [] - const isStarterOrTrigger = - blockConfig.category === 'triggers' || type === 'starter' || isTrigger + const isPureTriggerBlock = blockConfig.triggers?.enabled && blockConfig.category === 'triggers' + + const effectiveTrigger = isTrigger || type === 'starter' return blockConfig.subBlocks.filter((subBlock) => { if (subBlock.hidden) return false if (subBlock.hideFromPreview) return false if (!isSubBlockFeatureEnabled(subBlock)) return false - // Handle trigger mode visibility - if (subBlock.mode === 'trigger' && !isStarterOrTrigger) return false + if (effectiveTrigger) { + const isValidTriggerSubblock = isPureTriggerBlock + ? subBlock.mode === 'trigger' || !subBlock.mode + : subBlock.mode === 'trigger' + if (!isValidTriggerSubblock) return false + } else { + if (subBlock.mode === 'trigger') return false + } - // Check advanced mode visibility if (!isSubBlockVisibleForMode(subBlock, false, canonicalIndex, rawValues, undefined)) { return false } - // Check condition visibility if (!subBlock.condition) return true return evaluateSubBlockCondition(subBlock.condition, rawValues) }) - }, [blockConfig?.subBlocks, blockConfig?.category, type, isTrigger, canonicalIndex, rawValues]) + }, [ + lightweight, + blockConfig?.subBlocks, + blockConfig?.category, + blockConfig?.triggers?.enabled, + type, + isTrigger, + canonicalIndex, + rawValues, + ]) - /** - * Compute condition rows for condition blocks - */ const conditionRows = useMemo(() => { - if (type !== 'condition') return [] + if (lightweight || type !== 'condition') return [] const conditionsValue = rawValues.conditions const raw = typeof conditionsValue === 'string' ? conditionsValue : undefined @@ -332,13 +471,10 @@ function WorkflowPreviewBlockInner({ data }: NodeProps { id: 'if', title: 'if', value: '' }, { id: 'else', title: 'else', value: '' }, ] - }, [type, rawValues]) + }, [lightweight, type, rawValues]) - /** - * Compute router rows for router_v2 blocks - */ const routerRows = useMemo(() => { - if (type !== 'router_v2') return [] + if (lightweight || type !== 'router_v2') return [] const routesValue = rawValues.routes const raw = typeof routesValue === 'string' ? routesValue : undefined @@ -361,7 +497,7 @@ function WorkflowPreviewBlockInner({ data }: NodeProps } return [{ id: 'route1', value: '' }] - }, [type, rawValues]) + }, [lightweight, type, rawValues]) if (!blockConfig) { return null @@ -439,12 +575,10 @@ function WorkflowPreviewBlockInner({ data }: NodeProps {hasContentBelowHeader && (
{type === 'condition' ? ( - // Condition block: render condition rows conditionRows.map((cond) => ( )) ) : type === 'router_v2' ? ( - // Router block: render context + route rows <> ))} ) : ( - // Standard blocks: render visible subblocks visibleSubBlocks.map((subBlock) => { const rawValue = rawValues[subBlock.id] return ( @@ -479,18 +612,102 @@ function WorkflowPreviewBlockInner({ data }: NodeProps
)} - {/* Source handle */} - + {/* Condition block handles - one per condition branch + error */} + {type === 'condition' && ( + <> + {conditionRows.map((cond, condIndex) => { + const topOffset = + HANDLE_POSITIONS.CONDITION_START_Y + condIndex * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT + return ( + + ) + })} + + + )} + + {/* Router block handles - one per route + error */} + {type === 'router_v2' && ( + <> + {routerRows.map((route, routeIndex) => { + // +1 row offset for context row at the top + const topOffset = + HANDLE_POSITIONS.CONDITION_START_Y + + (routeIndex + 1) * HANDLE_POSITIONS.CONDITION_ROW_HEIGHT + return ( + + ) + })} + + + )} + + {/* Standard block handles - source + error (not for condition, router, or response) */} + {type !== 'condition' && type !== 'router_v2' && type !== 'response' && ( + <> + + {shouldShowDefaultHandles && ( + + )} + + )}
) } @@ -499,7 +716,6 @@ function shouldSkipPreviewBlockRender( prevProps: NodeProps, nextProps: NodeProps ): boolean { - // Check primitive props first (fast path) if ( prevProps.id !== nextProps.id || prevProps.data.type !== nextProps.data.type || @@ -508,12 +724,16 @@ function shouldSkipPreviewBlockRender( prevProps.data.horizontalHandles !== nextProps.data.horizontalHandles || prevProps.data.enabled !== nextProps.data.enabled || prevProps.data.isPreviewSelected !== nextProps.data.isPreviewSelected || - prevProps.data.executionStatus !== nextProps.data.executionStatus + prevProps.data.executionStatus !== nextProps.data.executionStatus || + prevProps.data.lightweight !== nextProps.data.lightweight || + prevProps.data.isSubflow !== nextProps.data.isSubflow || + prevProps.data.subflowKind !== nextProps.data.subflowKind || + prevProps.data.width !== nextProps.data.width || + prevProps.data.height !== nextProps.data.height ) { return false } - // Compare subBlockValues by reference first const prevValues = prevProps.data.subBlockValues const nextValues = nextProps.data.subBlockValues @@ -525,7 +745,6 @@ function shouldSkipPreviewBlockRender( return false } - // Shallow compare keys and values const prevKeys = Object.keys(prevValues) const nextKeys = Object.keys(nextValues) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx deleted file mode 100644 index a9aa913a2..000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/components/subflow.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client' - -import { memo } from 'react' -import { RepeatIcon, SplitIcon } from 'lucide-react' -import { Handle, type NodeProps, Position } from 'reactflow' -import { HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions' - -interface WorkflowPreviewSubflowData { - name: string - width?: number - height?: number - kind: 'loop' | 'parallel' - /** Whether this subflow is selected in preview mode */ - isPreviewSelected?: boolean -} - -/** - * Preview subflow component for workflow visualization. - * Renders loop/parallel containers without hooks, store subscriptions, - * or interactive features. - */ -function WorkflowPreviewSubflowInner({ data }: NodeProps) { - const { name, width = 500, height = 300, kind, isPreviewSelected = false } = data - - const isLoop = kind === 'loop' - const BlockIcon = isLoop ? RepeatIcon : SplitIcon - const blockIconBg = isLoop ? '#2FB3FF' : '#FEE12B' - const blockName = name || (isLoop ? 'Loop' : 'Parallel') - - const startHandleId = isLoop ? 'loop-start-source' : 'parallel-start-source' - const endHandleId = isLoop ? 'loop-end-source' : 'parallel-end-source' - - const leftHandleClass = - '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none' - const rightHandleClass = - '!z-[10] !border-none !bg-[var(--workflow-edge)] !h-5 !w-[7px] !rounded-r-[2px] !rounded-l-none' - - return ( -
- {/* Selection ring overlay */} - {isPreviewSelected && ( -
- )} - - {/* Target handle on left (input to the subflow) */} - - - {/* Header - matches actual subflow header structure */} -
-
-
- -
- - {blockName} - -
-
- - {/* Content area - matches workflow structure */} -
- {/* Subflow Start - connects to first block in subflow */} -
- Start - -
-
- - {/* End source handle on right (output from the subflow) */} - -
- ) -} - -export const WorkflowPreviewSubflow = memo(WorkflowPreviewSubflowInner) diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx index a9d56c794..ec00e2c38 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/preview/preview.tsx @@ -18,7 +18,6 @@ import { BLOCK_DIMENSIONS, CONTAINER_DIMENSIONS } from '@/lib/workflows/blocks/b import { WorkflowEdge } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-edge/workflow-edge' import { estimateBlockDimensions } from '@/app/workspace/[workspaceId]/w/[workflowId]/utils' import { WorkflowPreviewBlock } from '@/app/workspace/[workspaceId]/w/components/preview/components/block' -import { WorkflowPreviewSubflow } from '@/app/workspace/[workspaceId]/w/components/preview/components/subflow' import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types' const logger = createLogger('WorkflowPreview') @@ -53,13 +52,12 @@ function getPreviewBlockDimensions(block: BlockState): { width: number; height: /** * Calculates container dimensions based on child block positions and sizes. * Mirrors the logic from useNodeUtilities.calculateLoopDimensions. + * Accepts pre-filtered childBlocks for O(1) lookup instead of filtering all blocks. */ -function calculateContainerDimensions( - containerId: string, - blocks: Record -): { width: number; height: number } { - const childBlocks = Object.values(blocks).filter((block) => block?.data?.parentId === containerId) - +function calculateContainerDimensions(childBlocks: BlockState[]): { + width: number + height: number +} { if (childBlocks.length === 0) { return { width: CONTAINER_DIMENSIONS.DEFAULT_WIDTH, @@ -113,8 +111,34 @@ export function getLeftmostBlockId(workflowState: WorkflowState | null | undefin return leftmostId } +/** + * Recursively calculates the absolute position of a block, + * accounting for parent container offsets. + */ +function calculateAbsolutePosition( + block: BlockState, + blocks: Record +): { x: number; y: number } { + if (!block.data?.parentId) { + return block.position + } + + const parentBlock = blocks[block.data.parentId] + if (!parentBlock) { + logger.warn(`Parent block not found for child block: ${block.id}`) + return block.position + } + + const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks) + + return { + x: parentAbsolutePosition.x + block.position.x, + y: parentAbsolutePosition.y + block.position.y, + } +} + /** Execution status for edges/nodes in the preview */ -type ExecutionStatus = 'success' | 'error' | 'not-executed' +export type ExecutionStatus = 'success' | 'error' | 'not-executed' interface WorkflowPreviewProps { workflowState: WorkflowState @@ -136,6 +160,8 @@ interface WorkflowPreviewProps { executedBlocks?: Record /** Currently selected block ID for highlighting */ selectedBlockId?: string | null + /** Skips expensive computations for thumbnails/template previews */ + lightweight?: boolean } /** @@ -144,8 +170,6 @@ interface WorkflowPreviewProps { */ const previewNodeTypes: NodeTypes = { workflowBlock: WorkflowPreviewBlock, - noteBlock: WorkflowPreviewBlock, - subflowNode: WorkflowPreviewSubflow, } // Define edge types @@ -221,64 +245,30 @@ export function WorkflowPreview({ cursorStyle = 'grab', executedBlocks, selectedBlockId, + lightweight = false, }: WorkflowPreviewProps) { const containerRef = useRef(null) - const nodeTypes = previewNodeTypes const isValidWorkflowState = workflowState?.blocks && workflowState.edges - const blocksStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: Object.keys(workflowState.blocks || {}).length, - ids: Object.keys(workflowState.blocks || {}).join(','), - } - }, [workflowState.blocks, isValidWorkflowState]) + const workflowStructureIds = useMemo(() => { + if (!isValidWorkflowState) return '' + const blockIds = Object.keys(workflowState.blocks || {}) + const edgeIds = (workflowState.edges || []).map((e) => e.id) + return [...blockIds, ...edgeIds].join(',') + }, [workflowState.blocks, workflowState.edges, isValidWorkflowState]) - const loopsStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: Object.keys(workflowState.loops || {}).length, - ids: Object.keys(workflowState.loops || {}).join(','), + // Pre-compute parent-child relationships for O(1) lookups in container dimension calculations + const containerChildIndex = useMemo(() => { + const index: Record = {} + for (const block of Object.values(workflowState.blocks || {})) { + if (block?.data?.parentId) { + const parentId = block.data.parentId + if (!index[parentId]) index[parentId] = [] + index[parentId].push(block) + } } - }, [workflowState.loops, isValidWorkflowState]) - - const parallelsStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: Object.keys(workflowState.parallels || {}).length, - ids: Object.keys(workflowState.parallels || {}).join(','), - } - }, [workflowState.parallels, isValidWorkflowState]) - - const edgesStructure = useMemo(() => { - if (!isValidWorkflowState) return { count: 0, ids: '' } - return { - count: workflowState.edges?.length || 0, - ids: workflowState.edges?.map((e) => e.id).join(',') || '', - } - }, [workflowState.edges, isValidWorkflowState]) - - const calculateAbsolutePosition = ( - block: any, - blocks: Record - ): { x: number; y: number } => { - if (!block.data?.parentId) { - return block.position - } - - const parentBlock = blocks[block.data.parentId] - if (!parentBlock) { - logger.warn(`Parent block not found for child block: ${block.id}`) - return block.position - } - - const parentAbsolutePosition = calculateAbsolutePosition(parentBlock, blocks) - - return { - x: parentAbsolutePosition.x + block.position.x, - y: parentAbsolutePosition.y + block.position.y, - } - } + return index + }, [workflowState.blocks]) const nodes: Node[] = useMemo(() => { if (!isValidWorkflowState) return [] @@ -293,21 +283,24 @@ export function WorkflowPreview({ const absolutePosition = calculateAbsolutePosition(block, workflowState.blocks) - // Handle loop/parallel containers + // Handle loop/parallel containers - use unified workflowBlock with isSubflow flag if (block.type === 'loop' || block.type === 'parallel') { const isSelected = selectedBlockId === blockId - const dimensions = calculateContainerDimensions(blockId, workflowState.blocks) + const childBlocks = containerChildIndex[blockId] || [] + const dimensions = calculateContainerDimensions(childBlocks) nodeArray.push({ id: blockId, - type: 'subflowNode', + type: 'workflowBlock', position: absolutePosition, draggable: false, data: { + type: block.type, name: block.name, + isPreviewSelected: isSelected, + isSubflow: true, + subflowKind: block.type as 'loop' | 'parallel', width: dimensions.width, height: dimensions.height, - kind: block.type as 'loop' | 'parallel', - isPreviewSelected: isSelected, }, }) return @@ -348,19 +341,20 @@ export function WorkflowPreview({ isPreviewSelected: isSelected, executionStatus, subBlockValues: block.subBlocks, + lightweight, }, }) }) return nodeArray }, [ - blocksStructure, - loopsStructure, - parallelsStructure, + workflowStructureIds, workflowState.blocks, + containerChildIndex, isValidWorkflowState, executedBlocks, selectedBlockId, + lightweight, ]) const edges: Edge[] = useMemo(() => { @@ -395,7 +389,7 @@ export function WorkflowPreview({ zIndex: executionStatus === 'success' ? 10 : 0, } }) - }, [edgesStructure, workflowState.edges, isValidWorkflowState, executedBlocks]) + }, [workflowStructureIds, workflowState.edges, isValidWorkflowState, executedBlocks]) if (!isValidWorkflowState) { return ( @@ -449,7 +443,7 @@ export function WorkflowPreview({ From 3ccbee187d859cb3ed21552272ea5f64fd67254c Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 26 Jan 2026 15:49:23 -0800 Subject: [PATCH 02/19] improvement(docs): updated logo, added lightbox to action media, fixed minor styling inconsistencies between themes (#3014) * improvement(docs): updated logo, added lightbox to action media, fixed minor styling inconsistencies between themes * updated og image * ack comments --- apps/docs/app/[lang]/[[...slug]]/page.tsx | 5 - apps/docs/app/[lang]/layout.tsx | 13 +- apps/docs/app/api/og/route.tsx | 121 ++++++++---------- apps/docs/app/global.css | 74 +++++++---- .../docs-layout/sidebar-components.tsx | 6 +- .../components/docs-layout/toc-footer.tsx | 2 +- apps/docs/components/navbar/navbar.tsx | 10 +- apps/docs/components/ui/action-media.tsx | 81 +++++++++--- apps/docs/components/ui/language-dropdown.tsx | 69 ++++++---- apps/docs/components/ui/sim-logo.tsx | 108 ++++++++++++++++ 10 files changed, 325 insertions(+), 164 deletions(-) create mode 100644 apps/docs/components/ui/sim-logo.tsx diff --git a/apps/docs/app/[lang]/[[...slug]]/page.tsx b/apps/docs/app/[lang]/[[...slug]]/page.tsx index 228bf6da1..025df8093 100644 --- a/apps/docs/app/[lang]/[[...slug]]/page.tsx +++ b/apps/docs/app/[lang]/[[...slug]]/page.tsx @@ -185,11 +185,6 @@ export default async function Page(props: { params: Promise<{ slug?: string[]; l tableOfContent={{ style: 'clerk', enabled: true, - header: ( -
- On this page -
- ), footer: , single: false, }} diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index 4dbd3d5a4..2563b9d4b 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -3,13 +3,13 @@ import { defineI18nUI } from 'fumadocs-ui/i18n' import { DocsLayout } from 'fumadocs-ui/layouts/docs' import { RootProvider } from 'fumadocs-ui/provider/next' import { Geist_Mono, Inter } from 'next/font/google' -import Image from 'next/image' import { SidebarFolder, SidebarItem, SidebarSeparator, } from '@/components/docs-layout/sidebar-components' import { Navbar } from '@/components/navbar/navbar' +import { SimLogoFull } from '@/components/ui/sim-logo' import { i18n } from '@/lib/i18n' import { source } from '@/lib/source' import '../global.css' @@ -102,16 +102,7 @@ export default async function Layout({ children, params }: LayoutProps) { - ), + title: , }} sidebar={{ defaultOpenLevel: 0, diff --git a/apps/docs/app/api/og/route.tsx b/apps/docs/app/api/og/route.tsx index 9fec7a4b5..7481224d6 100644 --- a/apps/docs/app/api/og/route.tsx +++ b/apps/docs/app/api/og/route.tsx @@ -33,15 +33,41 @@ async function loadGoogleFont(font: string, weights: string, text: string): Prom throw new Error('Failed to load font data') } +/** + * Sim logo with icon and "Sim" text for OG image. + */ +function SimLogoFull() { + return ( + + {/* Green icon - top left shape with cutout */} + + {/* Green icon - bottom right square */} + + {/* "Sim" text - white for dark background */} + + + ) +} + /** * Generates dynamic Open Graph images for documentation pages. + * Style matches Cursor docs: dark background, title at top, logo bottom-left, domain bottom-right. */ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) const title = searchParams.get('title') || 'Documentation' - const baseUrl = new URL(request.url).origin - const allText = `${title}docs.sim.ai` const fontData = await loadGoogleFont('Geist', '400;500;600', allText) @@ -52,84 +78,39 @@ export async function GET(request: NextRequest) { width: '100%', display: 'flex', flexDirection: 'column', - background: '#0c0c0c', - position: 'relative', + justifyContent: 'space-between', + padding: '56px 64px', + background: '#121212', // Dark mode background matching docs (hsla 0, 0%, 7%) fontFamily: 'Geist', }} > - {/* Base gradient layer - subtle purple tint across the entire image */} -
- - {/* Secondary glow - adds depth without harsh edges */} -
- - {/* Top darkening - creates natural vignette */} -
- - {/* Content */} -
- {/* Logo */} - sim + {title} + - {/* Title */} - - {title} - - - {/* Footer */} + {/* Footer: icon left, domain right */} +
+ diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 53bc8db7f..656b946cd 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -9,11 +9,20 @@ body { } @theme { - --color-fd-primary: #802fff; /* Purple from control-bar component */ + --color-fd-primary: #33c482; /* Green from Sim logo */ --font-geist-sans: var(--font-geist-sans); --font-geist-mono: var(--font-geist-mono); } +/* Ensure primary color is set in both light and dark modes */ +:root { + --color-fd-primary: #33c482; +} + +.dark { + --color-fd-primary: #33c482; +} + /* Font family utilities */ .font-sans { font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, @@ -34,7 +43,7 @@ body { :root { --fd-border: transparent !important; --fd-border-sidebar: transparent !important; - --fd-nav-height: 64px; /* Custom navbar height (h-16 = 4rem = 64px) */ + --fd-nav-height: 65px; /* Custom navbar height (h-16 = 64px + 1px border) */ /* Content container width used to center main content */ --spacing-fd-container: 1400px; /* Edge gutter = leftover space on each side of centered container */ @@ -136,11 +145,11 @@ aside#nd-sidebar { /* On mobile, let fumadocs handle the layout natively */ @media (min-width: 1024px) { :root { - --fd-banner-height: 64px !important; + --fd-banner-height: 65px !important; /* 64px navbar + 1px border */ } #nd-docs-layout { - --fd-docs-height: calc(100dvh - 64px) !important; + --fd-docs-height: calc(100dvh - 65px) !important; /* 64px navbar + 1px border */ --fd-sidebar-width: 300px !important; margin-left: var(--sidebar-offset) !important; margin-right: var(--toc-offset) !important; @@ -227,19 +236,19 @@ html:not(.dark) #nd-sidebar button:not([aria-label*="ollapse"]):not([aria-label* letter-spacing: 0.05em !important; } -/* Override active state (NO PURPLE) */ +/* Override active state */ #nd-sidebar a[data-active="true"], #nd-sidebar button[data-active="true"], #nd-sidebar a.bg-fd-primary\/10, #nd-sidebar a.text-fd-primary, #nd-sidebar a[class*="bg-fd-primary"], #nd-sidebar a[class*="text-fd-primary"], -/* Override custom sidebar purple classes */ +/* Override custom sidebar green classes */ #nd-sidebar - a.bg-purple-50\/80, -#nd-sidebar a.text-purple-600, -#nd-sidebar a[class*="bg-purple"], -#nd-sidebar a[class*="text-purple"] { + a.bg-emerald-50\/80, +#nd-sidebar a.text-emerald-600, +#nd-sidebar a[class*="bg-emerald"], +#nd-sidebar a[class*="text-emerald"] { background-image: none !important; } @@ -250,10 +259,10 @@ html.dark #nd-sidebar a.bg-fd-primary\/10, html.dark #nd-sidebar a.text-fd-primary, html.dark #nd-sidebar a[class*="bg-fd-primary"], html.dark #nd-sidebar a[class*="text-fd-primary"], -html.dark #nd-sidebar a.bg-purple-50\/80, -html.dark #nd-sidebar a.text-purple-600, -html.dark #nd-sidebar a[class*="bg-purple"], -html.dark #nd-sidebar a[class*="text-purple"] { +html.dark #nd-sidebar a.bg-emerald-50\/80, +html.dark #nd-sidebar a.text-emerald-600, +html.dark #nd-sidebar a[class*="bg-emerald"], +html.dark #nd-sidebar a[class*="text-emerald"] { background-color: rgba(255, 255, 255, 0.15) !important; color: rgba(255, 255, 255, 1) !important; } @@ -265,10 +274,10 @@ html:not(.dark) #nd-sidebar a.bg-fd-primary\/10, html:not(.dark) #nd-sidebar a.text-fd-primary, html:not(.dark) #nd-sidebar a[class*="bg-fd-primary"], html:not(.dark) #nd-sidebar a[class*="text-fd-primary"], -html:not(.dark) #nd-sidebar a.bg-purple-50\/80, -html:not(.dark) #nd-sidebar a.text-purple-600, -html:not(.dark) #nd-sidebar a[class*="bg-purple"], -html:not(.dark) #nd-sidebar a[class*="text-purple"] { +html:not(.dark) #nd-sidebar a.bg-emerald-50\/80, +html:not(.dark) #nd-sidebar a.text-emerald-600, +html:not(.dark) #nd-sidebar a[class*="bg-emerald"], +html:not(.dark) #nd-sidebar a[class*="text-emerald"] { background-color: rgba(0, 0, 0, 0.07) !important; color: rgba(0, 0, 0, 0.9) !important; } @@ -286,8 +295,8 @@ html:not(.dark) #nd-sidebar button:hover:not([data-active="true"]) { } /* Dark mode - ensure active/selected items don't change on hover */ -html.dark #nd-sidebar a.bg-purple-50\/80:hover, -html.dark #nd-sidebar a[class*="bg-purple"]:hover, +html.dark #nd-sidebar a.bg-emerald-50\/80:hover, +html.dark #nd-sidebar a[class*="bg-emerald"]:hover, html.dark #nd-sidebar a[data-active="true"]:hover, html.dark #nd-sidebar button[data-active="true"]:hover { background-color: rgba(255, 255, 255, 0.15) !important; @@ -295,8 +304,8 @@ html.dark #nd-sidebar button[data-active="true"]:hover { } /* Light mode - ensure active/selected items don't change on hover */ -html:not(.dark) #nd-sidebar a.bg-purple-50\/80:hover, -html:not(.dark) #nd-sidebar a[class*="bg-purple"]:hover, +html:not(.dark) #nd-sidebar a.bg-emerald-50\/80:hover, +html:not(.dark) #nd-sidebar a[class*="bg-emerald"]:hover, html:not(.dark) #nd-sidebar a[data-active="true"]:hover, html:not(.dark) #nd-sidebar button[data-active="true"]:hover { background-color: rgba(0, 0, 0, 0.07) !important; @@ -368,16 +377,22 @@ aside[data-sidebar] > *:not([data-sidebar-viewport]) { button[aria-label="Toggle Sidebar"], button[aria-label="Collapse Sidebar"], /* Hide nav title/logo in sidebar on desktop - target all possible locations */ + #nd-sidebar + a[href="/"], + #nd-sidebar a[href="/"] img, + #nd-sidebar a[href="/"] svg, + #nd-sidebar > a:first-child, + #nd-sidebar > div:first-child > a:first-child, + #nd-sidebar img[alt="Sim"], + #nd-sidebar svg[aria-label="Sim"], aside[data-sidebar] a[href="/"], aside[data-sidebar] a[href="/"] img, aside[data-sidebar] > a:first-child, aside[data-sidebar] > div > a:first-child, aside[data-sidebar] img[alt="Sim"], + aside[data-sidebar] svg[aria-label="Sim"], [data-sidebar-header], [data-sidebar] [data-title], - #nd-sidebar > a:first-child, - #nd-sidebar > div:first-child > a:first-child, - #nd-sidebar img[alt="Sim"], /* Hide theme toggle at bottom of sidebar on desktop */ #nd-sidebar > footer, @@ -515,6 +530,15 @@ pre code .line { color: var(--color-fd-primary); } +/* ============================================ + TOC (Table of Contents) Styling + ============================================ */ + +/* Remove the thin border-left on nested TOC items (keeps main indicator only) */ +#nd-toc a[style*="padding-inline-start"] { + border-left: none !important; +} + /* Add bottom spacing to prevent abrupt page endings */ [data-content] { padding-top: 1.5rem !important; diff --git a/apps/docs/components/docs-layout/sidebar-components.tsx b/apps/docs/components/docs-layout/sidebar-components.tsx index e5fb882fb..e6fbe18cd 100644 --- a/apps/docs/components/docs-layout/sidebar-components.tsx +++ b/apps/docs/components/docs-layout/sidebar-components.tsx @@ -44,7 +44,7 @@ export function SidebarItem({ item }: { item: Item }) { 'lg:text-gray-600 lg:dark:text-gray-400', !active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40', active && - 'lg:bg-purple-50/80 lg:font-normal lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400' + 'lg:bg-emerald-50/80 lg:font-normal lg:text-emerald-600 lg:dark:bg-emerald-900/15 lg:dark:text-emerald-400' )} > {item.name} @@ -79,7 +79,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac 'lg:text-gray-600 lg:dark:text-gray-400', !active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40', active && - 'lg:bg-purple-50/80 lg:font-normal lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400' + 'lg:bg-emerald-50/80 lg:font-normal lg:text-emerald-600 lg:dark:bg-emerald-900/15 lg:dark:text-emerald-400' )} > {item.name} @@ -104,7 +104,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac 'lg:text-gray-800 lg:dark:text-gray-200', !active && 'lg:hover:bg-gray-100/60 lg:dark:hover:bg-gray-800/40', active && - 'lg:bg-purple-50/80 lg:text-purple-600 lg:dark:bg-purple-900/15 lg:dark:text-purple-400' + 'lg:bg-emerald-50/80 lg:text-emerald-600 lg:dark:bg-emerald-900/15 lg:dark:text-emerald-400' )} > {item.name} diff --git a/apps/docs/components/docs-layout/toc-footer.tsx b/apps/docs/components/docs-layout/toc-footer.tsx index cd62bd6c7..01eb5d81a 100644 --- a/apps/docs/components/docs-layout/toc-footer.tsx +++ b/apps/docs/components/docs-layout/toc-footer.tsx @@ -23,7 +23,7 @@ export function TOCFooter() { rel='noopener noreferrer' onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} - className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#9B77FF] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50' + className='group mt-2 inline-flex h-8 w-fit items-center justify-center gap-1 whitespace-nowrap rounded-[10px] border border-[#2AAD6C] bg-gradient-to-b from-[#3ED990] to-[#2AAD6C] px-3 pr-[10px] pl-[12px] font-medium text-sm text-white shadow-[inset_0_2px_4px_0_#5EE8A8] outline-none transition-all hover:shadow-lg focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50' aria-label='Get started with Sim - Sign up for free' > Get started diff --git a/apps/docs/components/navbar/navbar.tsx b/apps/docs/components/navbar/navbar.tsx index bbddf45e6..12bf9f7be 100644 --- a/apps/docs/components/navbar/navbar.tsx +++ b/apps/docs/components/navbar/navbar.tsx @@ -1,9 +1,9 @@ 'use client' -import Image from 'next/image' import Link from 'next/link' import { LanguageDropdown } from '@/components/ui/language-dropdown' import { SearchTrigger } from '@/components/ui/search-trigger' +import { SimLogoFull } from '@/components/ui/sim-logo' import { ThemeToggle } from '@/components/ui/theme-toggle' export function Navbar() { @@ -27,13 +27,7 @@ export function Navbar() { {/* Left cluster: logo */}
- Sim +
diff --git a/apps/docs/components/ui/action-media.tsx b/apps/docs/components/ui/action-media.tsx index 1f187fb90..629716b66 100644 --- a/apps/docs/components/ui/action-media.tsx +++ b/apps/docs/components/ui/action-media.tsx @@ -1,38 +1,87 @@ 'use client' -import { getAssetUrl } from '@/lib/utils' +import { useState } from 'react' +import { cn, getAssetUrl } from '@/lib/utils' +import { Lightbox } from './lightbox' interface ActionImageProps { src: string alt: string + enableLightbox?: boolean } interface ActionVideoProps { src: string alt: string + enableLightbox?: boolean } -export function ActionImage({ src, alt }: ActionImageProps) { +export function ActionImage({ src, alt, enableLightbox = true }: ActionImageProps) { + const [isLightboxOpen, setIsLightboxOpen] = useState(false) + + const handleClick = () => { + if (enableLightbox) { + setIsLightboxOpen(true) + } + } + return ( - {alt} + <> + {alt} + {enableLightbox && ( + setIsLightboxOpen(false)} + src={src} + alt={alt} + type='image' + /> + )} + ) } -export function ActionVideo({ src, alt }: ActionVideoProps) { +export function ActionVideo({ src, alt, enableLightbox = true }: ActionVideoProps) { + const [isLightboxOpen, setIsLightboxOpen] = useState(false) const resolvedSrc = getAssetUrl(src) + const handleClick = () => { + if (enableLightbox) { + setIsLightboxOpen(true) + } + } + return ( -