-
+
c + c)
+ .join('')
+ }
+ return `#${cleaned}`
+}
interface ContextMenuProps {
/**
@@ -53,6 +82,14 @@ interface ContextMenuProps {
* Callback when delete is clicked
*/
onDelete: () => void
+ /**
+ * Callback when color is changed
+ */
+ onColorChange?: (color: string) => void
+ /**
+ * Current workflow color (for showing selected state)
+ */
+ currentColor?: string
/**
* Whether to show the open in new tab option (default: false)
* Set to true for items that can be opened in a new tab
@@ -83,11 +120,21 @@ interface ContextMenuProps {
* Set to true for items that can be exported (like workspaces)
*/
showExport?: boolean
+ /**
+ * Whether to show the change color option (default: false)
+ * Set to true for workflows to allow color customization
+ */
+ showColorChange?: boolean
/**
* Whether the export option is disabled (default: false)
* Set to true when user lacks permissions
*/
disableExport?: boolean
+ /**
+ * Whether the change color option is disabled (default: false)
+ * Set to true when user lacks permissions
+ */
+ disableColorChange?: boolean
/**
* Whether the rename option is disabled (default: false)
* Set to true when user lacks permissions
@@ -134,23 +181,74 @@ export function ContextMenu({
onDuplicate,
onExport,
onDelete,
+ onColorChange,
+ currentColor,
showOpenInNewTab = false,
showRename = true,
showCreate = false,
showCreateFolder = false,
showDuplicate = true,
showExport = false,
+ showColorChange = false,
disableExport = false,
+ disableColorChange = false,
disableRename = false,
disableDuplicate = false,
disableDelete = false,
disableCreate = false,
disableCreateFolder = false,
}: ContextMenuProps) {
- // Section visibility for divider logic
+ const [hexInput, setHexInput] = useState(currentColor || '#ffffff')
+
+ // Sync hexInput when currentColor changes (e.g., opening menu on different workflow)
+ useEffect(() => {
+ setHexInput(currentColor || '#ffffff')
+ }, [currentColor])
+
+ const canSubmitHex = useMemo(() => {
+ if (!isValidHex(hexInput)) return false
+ const normalized = normalizeHex(hexInput)
+ if (currentColor && normalized.toLowerCase() === currentColor.toLowerCase()) return false
+ return true
+ }, [hexInput, currentColor])
+
+ const handleHexSubmit = useCallback(() => {
+ if (!canSubmitHex || !onColorChange) return
+
+ const normalized = normalizeHex(hexInput)
+ onColorChange(normalized)
+ setHexInput(normalized)
+ }, [hexInput, canSubmitHex, onColorChange])
+
+ const handleHexKeyDown = useCallback(
+ (e: React.KeyboardEvent) => {
+ if (e.key === 'Enter') {
+ e.preventDefault()
+ handleHexSubmit()
+ }
+ },
+ [handleHexSubmit]
+ )
+
+ const handleHexChange = useCallback((e: React.ChangeEvent) => {
+ let value = e.target.value.trim()
+ if (value && !value.startsWith('#')) {
+ value = `#${value}`
+ }
+ value = value.slice(0, 1) + value.slice(1).replace(/[^0-9a-fA-F]/g, '')
+ setHexInput(value.slice(0, 7))
+ }, [])
+
+ const handleHexFocus = useCallback((e: React.FocusEvent) => {
+ e.target.select()
+ }, [])
+
const hasNavigationSection = showOpenInNewTab && onOpenInNewTab
const hasEditSection =
- (showRename && onRename) || (showCreate && onCreate) || (showCreateFolder && onCreateFolder)
+ (showRename && onRename) ||
+ (showCreate && onCreate) ||
+ (showCreateFolder && onCreateFolder) ||
+ (showColorChange && onColorChange)
const hasCopySection = (showDuplicate && onDuplicate) || (showExport && onExport)
return (
@@ -170,10 +268,21 @@ export function ContextMenu({
height: '1px',
}}
/>
-
+ e.preventDefault()}
+ onInteractOutside={(e) => e.preventDefault()}
+ >
+ {/* Back button - shown only when in a folder */}
+
+
{/* Navigation actions */}
{showOpenInNewTab && onOpenInNewTab && (
{
onOpenInNewTab()
onClose()
@@ -182,11 +291,12 @@ export function ContextMenu({
Open in new tab
)}
- {hasNavigationSection && (hasEditSection || hasCopySection) && }
+ {hasNavigationSection && (hasEditSection || hasCopySection) && }
{/* Edit and create actions */}
{showRename && onRename && (
{
onRename()
@@ -198,6 +308,7 @@ export function ContextMenu({
)}
{showCreate && onCreate && (
{
onCreate()
@@ -209,6 +320,7 @@ export function ContextMenu({
)}
{showCreateFolder && onCreateFolder && (
{
onCreateFolder()
@@ -218,11 +330,72 @@ export function ContextMenu({
Create folder
)}
+ {showColorChange && onColorChange && (
+
+
+ {/* Preset colors */}
+
+ {WORKFLOW_COLORS.map(({ color, name }) => (
+
+
+ {/* Hex input */}
+
+
+
e.stopPropagation()}
+ className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase focus:outline-none'
+ />
+
+
+
+
+ )}
{/* Copy and export actions */}
- {hasEditSection && hasCopySection && }
+ {hasEditSection && hasCopySection && }
{showDuplicate && onDuplicate && (
{
onDuplicate()
@@ -234,6 +407,7 @@ export function ContextMenu({
)}
{showExport && onExport && (
{
onExport()
@@ -245,8 +419,9 @@ export function ContextMenu({
)}
{/* Destructive action */}
- {(hasNavigationSection || hasEditSection || hasCopySection) && }
+ {(hasNavigationSection || hasEditSection || hasCopySection) && }
{
onDelete()
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
index ee65207b47..1dc249088d 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/folder-item/folder-item.tsx
@@ -3,8 +3,9 @@
import { useCallback, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import clsx from 'clsx'
-import { ChevronRight, Folder, FolderOpen } from 'lucide-react'
+import { ChevronRight, Folder, FolderOpen, MoreHorizontal } from 'lucide-react'
import { useParams, useRouter } from 'next/navigation'
+import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { ContextMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/context-menu/context-menu'
import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/delete-modal/delete-modal'
@@ -23,10 +24,7 @@ import {
import { useCreateFolder, useUpdateFolder } from '@/hooks/queries/folders'
import { useCreateWorkflow } from '@/hooks/queries/workflows'
import type { FolderTreeNode } from '@/stores/folders/types'
-import {
- generateCreativeWorkflowName,
- getNextWorkflowColor,
-} from '@/stores/workflows/registry/utils'
+import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
const logger = createLogger('FolderItem')
@@ -173,6 +171,7 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
menuRef,
handleContextMenu,
closeMenu,
+ preventDismiss,
} = useContextMenu()
// Rename hook
@@ -242,6 +241,40 @@ export function FolderItem({ folder, level, hoverHandlers }: FolderItemProps) {
[isEditing, handleRenameKeyDown, handleExpandKeyDown]
)
+ /**
+ * Handle more button pointerdown - prevents click-outside dismissal when toggling
+ */
+ const handleMorePointerDown = useCallback(() => {
+ if (isContextMenuOpen) {
+ preventDismiss()
+ }
+ }, [isContextMenuOpen, preventDismiss])
+
+ /**
+ * Handle more button click - toggles context menu at button position
+ */
+ const handleMoreClick = useCallback(
+ (e: React.MouseEvent) => {
+ e.preventDefault()
+ e.stopPropagation()
+
+ // Toggle: close if open, open if closed
+ if (isContextMenuOpen) {
+ closeMenu()
+ return
+ }
+
+ const rect = e.currentTarget.getBoundingClientRect()
+ handleContextMenu({
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ clientX: rect.right,
+ clientY: rect.top,
+ } as React.MouseEvent)
+ },
+ [isContextMenuOpen, closeMenu, handleContextMenu]
+ )
+
return (
<>
) : (
-
- {folder.name}
-
+ <>
+
+ {folder.name}
+
+
+ >
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars.tsx
index 685787bc92..506b9a6e24 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/avatars/avatars.tsx
@@ -1,15 +1,24 @@
'use client'
-import { type CSSProperties, useEffect, useMemo, useState } from 'react'
-import Image from 'next/image'
-import { Tooltip } from '@/components/emcn'
+import { type CSSProperties, useEffect, useMemo } from 'react'
+import { Avatar, AvatarFallback, AvatarImage, Tooltip } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { getUserColor } from '@/lib/workspaces/colors'
import { useSocket } from '@/app/workspace/providers/socket-provider'
+import { SIDEBAR_WIDTH } from '@/stores/constants'
+import { useSidebarStore } from '@/stores/sidebar/store'
+
+/**
+ * Avatar display configuration for responsive layout.
+ */
+const AVATAR_CONFIG = {
+ MIN_COUNT: 3,
+ MAX_COUNT: 12,
+ WIDTH_PER_AVATAR: 20,
+} as const
interface AvatarsProps {
workflowId: string
- maxVisible?: number
/**
* Callback fired when the presence visibility changes.
* Used by parent components to adjust layout (e.g., text truncation spacing).
@@ -30,45 +39,29 @@ interface UserAvatarProps {
}
/**
- * Individual user avatar with error handling for image loading.
+ * Individual user avatar using emcn Avatar component.
* Falls back to colored circle with initials if image fails to load.
*/
function UserAvatar({ user, index }: UserAvatarProps) {
- const [imageError, setImageError] = useState(false)
const color = getUserColor(user.userId)
const initials = user.userName ? user.userName.charAt(0).toUpperCase() : '?'
- const hasAvatar = Boolean(user.avatarUrl) && !imageError
-
- // Reset error state when avatar URL changes
- useEffect(() => {
- setImageError(false)
- }, [user.avatarUrl])
const avatarElement = (
-
- {hasAvatar && user.avatarUrl ? (
-
+ {user.avatarUrl && (
+ setImageError(true)}
/>
- ) : (
- initials
)}
-
+
+ {initials}
+
+
)
if (user.userName) {
@@ -92,14 +85,26 @@ function UserAvatar({ user, index }: UserAvatarProps) {
* @param props - Component props
* @returns Avatar stack for workflow presence
*/
-export function Avatars({ workflowId, maxVisible = 3, onPresenceChange }: AvatarsProps) {
+export function Avatars({ workflowId, onPresenceChange }: AvatarsProps) {
const { presenceUsers, currentWorkflowId } = useSocket()
const { data: session } = useSession()
const currentUserId = session?.user?.id
+ const sidebarWidth = useSidebarStore((state) => state.sidebarWidth)
/**
- * Only show presence for the currently active workflow
- * Filter out the current user from the list
+ * Calculate max visible avatars based on sidebar width.
+ * Scales between MIN_COUNT and MAX_COUNT as sidebar expands.
+ */
+ const maxVisible = useMemo(() => {
+ const widthDelta = sidebarWidth - SIDEBAR_WIDTH.MIN
+ const additionalAvatars = Math.floor(widthDelta / AVATAR_CONFIG.WIDTH_PER_AVATAR)
+ const calculated = AVATAR_CONFIG.MIN_COUNT + additionalAvatars
+ return Math.max(AVATAR_CONFIG.MIN_COUNT, Math.min(AVATAR_CONFIG.MAX_COUNT, calculated))
+ }, [sidebarWidth])
+
+ /**
+ * Only show presence for the currently active workflow.
+ * Filter out the current user from the list.
*/
const workflowUsers = useMemo(() => {
if (currentWorkflowId !== workflowId) {
@@ -122,7 +127,6 @@ export function Avatars({ workflowId, maxVisible = 3, onPresenceChange }: Avatar
return { visibleUsers: visible, overflowCount: overflow }
}, [workflowUsers, maxVisible])
- // Notify parent when avatars are present or not
useEffect(() => {
const hasAnyAvatars = visibleUsers.length > 0
if (typeof onPresenceChange === 'function') {
@@ -135,26 +139,25 @@ export function Avatars({ workflowId, maxVisible = 3, onPresenceChange }: Avatar
}
return (
-
- {visibleUsers.map((user, index) => (
-
- ))}
-
+
{overflowCount > 0 && (
-
- +{overflowCount}
-
+
+
+ +{overflowCount}
+
+
{overflowCount} more user{overflowCount > 1 ? 's' : ''}
)}
+
+ {visibleUsers.map((user, index) => (
+
0 ? index + 1 : index} />
+ ))}
)
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
index ffa481fa1b..2665ed3b24 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workflow-list/components/workflow-item/workflow-item.tsx
@@ -2,6 +2,7 @@
import { useCallback, useRef, useState } from 'react'
import clsx from 'clsx'
+import { MoreHorizontal } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
@@ -108,6 +109,16 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
window.open(`/workspace/${workspaceId}/w/${workflow.id}`, '_blank')
}, [workspaceId, workflow.id])
+ /**
+ * Changes the workflow color
+ */
+ const handleColorChange = useCallback(
+ (color: string) => {
+ updateWorkflow(workflow.id, { color })
+ },
+ [workflow.id, updateWorkflow]
+ )
+
/**
* Drag start handler - handles workflow dragging with multi-selection support
*
@@ -142,8 +153,38 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
menuRef,
handleContextMenu: handleContextMenuBase,
closeMenu,
+ preventDismiss,
} = useContextMenu()
+ /**
+ * Captures selection state for context menu operations
+ */
+ const captureSelectionState = useCallback(() => {
+ const { selectedWorkflows: currentSelection, selectOnly } = useFolderStore.getState()
+ const isCurrentlySelected = currentSelection.has(workflow.id)
+
+ if (!isCurrentlySelected) {
+ selectOnly(workflow.id)
+ }
+
+ const finalSelection = useFolderStore.getState().selectedWorkflows
+ const finalIsSelected = finalSelection.has(workflow.id)
+
+ const workflowIds =
+ finalIsSelected && finalSelection.size > 1 ? Array.from(finalSelection) : [workflow.id]
+
+ const workflowNames = workflowIds
+ .map((id) => workflows[id]?.name)
+ .filter((name): name is string => !!name)
+
+ capturedSelectionRef.current = {
+ workflowIds,
+ workflowNames: workflowNames.length > 1 ? workflowNames : workflowNames[0],
+ }
+
+ setCanDeleteCaptured(canDeleteWorkflows(workflowIds))
+ }, [workflow.id, workflows, canDeleteWorkflows])
+
/**
* Handle right-click - ensure proper selection behavior and capture selection state
* If right-clicking on an unselected workflow, select only that workflow
@@ -151,39 +192,46 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
*/
const handleContextMenu = useCallback(
(e: React.MouseEvent) => {
- // Check current selection state at time of right-click
- const { selectedWorkflows: currentSelection, selectOnly } = useFolderStore.getState()
- const isCurrentlySelected = currentSelection.has(workflow.id)
-
- // If this workflow is not in the current selection, select only this workflow
- if (!isCurrentlySelected) {
- selectOnly(workflow.id)
- }
-
- // Capture the selection state at right-click time
- const finalSelection = useFolderStore.getState().selectedWorkflows
- const finalIsSelected = finalSelection.has(workflow.id)
+ captureSelectionState()
+ handleContextMenuBase(e)
+ },
+ [captureSelectionState, handleContextMenuBase]
+ )
- const workflowIds =
- finalIsSelected && finalSelection.size > 1 ? Array.from(finalSelection) : [workflow.id]
+ /**
+ * Handle more button pointerdown - prevents click-outside dismissal when toggling
+ */
+ const handleMorePointerDown = useCallback(() => {
+ if (isContextMenuOpen) {
+ preventDismiss()
+ }
+ }, [isContextMenuOpen, preventDismiss])
- const workflowNames = workflowIds
- .map((id) => workflows[id]?.name)
- .filter((name): name is string => !!name)
+ /**
+ * Handle more button click - toggles context menu at button position
+ */
+ const handleMoreClick = useCallback(
+ (e: React.MouseEvent
) => {
+ e.preventDefault()
+ e.stopPropagation()
- // Store in ref so it persists even if selection changes
- capturedSelectionRef.current = {
- workflowIds,
- workflowNames: workflowNames.length > 1 ? workflowNames : workflowNames[0],
+ // Toggle: close if open, open if closed
+ if (isContextMenuOpen) {
+ closeMenu()
+ return
}
- // Check if the captured selection can be deleted
- setCanDeleteCaptured(canDeleteWorkflows(workflowIds))
-
- // If already selected with multiple selections, keep all selections
- handleContextMenuBase(e)
+ captureSelectionState()
+ // Open context menu aligned with the button
+ const rect = e.currentTarget.getBoundingClientRect()
+ handleContextMenuBase({
+ preventDefault: () => {},
+ stopPropagation: () => {},
+ clientX: rect.right,
+ clientY: rect.top,
+ } as React.MouseEvent)
},
- [workflow.id, workflows, handleContextMenuBase, canDeleteWorkflows]
+ [isContextMenuOpen, closeMenu, captureSelectionState, handleContextMenuBase]
)
// Rename hook
@@ -309,7 +357,17 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
)}
{!isEditing && (
-
+ <>
+
+
+ >
)}
@@ -324,13 +382,17 @@ export function WorkflowItem({ workflow, active, level, onWorkflowClick }: Workf
onDuplicate={handleDuplicateWorkflow}
onExport={handleExportWorkflow}
onDelete={handleOpenDeleteModal}
+ onColorChange={handleColorChange}
+ currentColor={workflow.color}
showOpenInNewTab={selectedWorkflows.size <= 1}
showRename={selectedWorkflows.size <= 1}
showDuplicate={true}
showExport={true}
+ showColorChange={selectedWorkflows.size <= 1}
disableRename={!userPermissions.canEdit}
disableDuplicate={!userPermissions.canEdit}
disableExport={!userPermissions.canEdit}
+ disableColorChange={!userPermissions.canEdit}
disableDelete={!userPermissions.canEdit || !canDeleteCaptured}
/>
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-context-menu.ts
index 13a6291e34..35b8546b2e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-context-menu.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-context-menu.ts
@@ -27,6 +27,8 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
const [isOpen, setIsOpen] = useState(false)
const [position, setPosition] = useState
({ x: 0, y: 0 })
const menuRef = useRef(null)
+ // Used to prevent click-outside dismissal when trigger is clicked
+ const dismissPreventedRef = useRef(false)
/**
* Handle right-click event
@@ -55,6 +57,14 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
setIsOpen(false)
}, [])
+ /**
+ * Prevent the next click-outside from dismissing the menu.
+ * Call this on pointerdown of a toggle trigger to allow proper toggle behavior.
+ */
+ const preventDismiss = useCallback(() => {
+ dismissPreventedRef.current = true
+ }, [])
+
/**
* Handle clicks outside the menu to close it
*/
@@ -62,6 +72,11 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
if (!isOpen) return
const handleClickOutside = (e: MouseEvent) => {
+ // Check if dismissal was prevented (e.g., by toggle trigger's pointerdown)
+ if (dismissPreventedRef.current) {
+ dismissPreventedRef.current = false
+ return
+ }
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
closeMenu()
}
@@ -84,5 +99,6 @@ export function useContextMenu({ onContextMenu }: UseContextMenuProps = {}) {
menuRef,
handleContextMenu,
closeMenu,
+ preventDismiss,
}
}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts
index f88b1cf118..6e9bed0441 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/hooks/use-workflow-operations.ts
@@ -1,13 +1,11 @@
import { useCallback } from 'react'
import { createLogger } from '@sim/logger'
import { useRouter } from 'next/navigation'
+import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { useCreateWorkflow, useWorkflows } from '@/hooks/queries/workflows'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
-import {
- generateCreativeWorkflowName,
- getNextWorkflowColor,
-} from '@/stores/workflows/registry/utils'
+import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
const logger = createLogger('useWorkflowOperations')
diff --git a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-workflow.ts b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-workflow.ts
index 6ead0955e0..5d39e8739e 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-workflow.ts
+++ b/apps/sim/app/workspace/[workspaceId]/w/hooks/use-duplicate-workflow.ts
@@ -1,10 +1,10 @@
import { useCallback } from 'react'
import { createLogger } from '@sim/logger'
import { useRouter } from 'next/navigation'
+import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { useDuplicateWorkflowMutation } from '@/hooks/queries/workflows'
import { useFolderStore } from '@/stores/folders/store'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
-import { getNextWorkflowColor } from '@/stores/workflows/registry/utils'
const logger = createLogger('useDuplicateWorkflow')
diff --git a/apps/sim/components/emcn/components/avatar/avatar.tsx b/apps/sim/components/emcn/components/avatar/avatar.tsx
index e41c40a9fa..9500f7ecbe 100644
--- a/apps/sim/components/emcn/components/avatar/avatar.tsx
+++ b/apps/sim/components/emcn/components/avatar/avatar.tsx
@@ -12,10 +12,10 @@ import { cn } from '@/lib/core/utils/cn'
const avatarVariants = cva('relative flex shrink-0 overflow-hidden rounded-full', {
variants: {
size: {
- xs: 'h-6 w-6',
- sm: 'h-8 w-8',
- md: 'h-10 w-10',
- lg: 'h-12 w-12',
+ xs: 'h-3.5 w-3.5',
+ sm: 'h-6 w-6',
+ md: 'h-8 w-8',
+ lg: 'h-10 w-10',
},
},
defaultVariants: {
@@ -37,10 +37,10 @@ const avatarStatusVariants = cva(
away: 'bg-[#f59e0b]',
},
size: {
- xs: 'h-2 w-2',
- sm: 'h-2.5 w-2.5',
- md: 'h-3 w-3',
- lg: 'h-3.5 w-3.5',
+ xs: 'h-1.5 w-1.5 border',
+ sm: 'h-2 w-2',
+ md: 'h-2.5 w-2.5',
+ lg: 'h-3 w-3',
},
},
defaultVariants: {
diff --git a/apps/sim/components/emcn/components/popover/popover.tsx b/apps/sim/components/emcn/components/popover/popover.tsx
index 0aa8237cee..d80841d677 100644
--- a/apps/sim/components/emcn/components/popover/popover.tsx
+++ b/apps/sim/components/emcn/components/popover/popover.tsx
@@ -52,6 +52,7 @@
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { Check, ChevronLeft, ChevronRight, Search } from 'lucide-react'
+import { createPortal } from 'react-dom'
import { cn } from '@/lib/core/utils/cn'
type PopoverSize = 'sm' | 'md'
@@ -166,6 +167,9 @@ interface PopoverContextValue {
colorScheme: PopoverColorScheme
searchQuery: string
setSearchQuery: (query: string) => void
+ /** ID of the last hovered item (for hover submenus) */
+ lastHoveredItem: string | null
+ setLastHoveredItem: (id: string | null) => void
}
const PopoverContext = React.createContext(null)
@@ -208,12 +212,24 @@ const Popover: React.FC = ({
variant = 'default',
size = 'md',
colorScheme = 'default',
+ open,
...props
}) => {
const [currentFolder, setCurrentFolder] = React.useState(null)
const [folderTitle, setFolderTitle] = React.useState(null)
const [onFolderSelect, setOnFolderSelect] = React.useState<(() => void) | null>(null)
const [searchQuery, setSearchQuery] = React.useState('')
+ const [lastHoveredItem, setLastHoveredItem] = React.useState(null)
+
+ React.useEffect(() => {
+ if (open === false) {
+ setCurrentFolder(null)
+ setFolderTitle(null)
+ setOnFolderSelect(null)
+ setSearchQuery('')
+ setLastHoveredItem(null)
+ }
+ }, [open])
const openFolder = React.useCallback(
(id: string, title: string, onLoad?: () => void | Promise, onSelect?: () => void) => {
@@ -246,6 +262,8 @@ const Popover: React.FC = ({
colorScheme,
searchQuery,
setSearchQuery,
+ lastHoveredItem,
+ setLastHoveredItem,
}),
[
openFolder,
@@ -257,12 +275,15 @@ const Popover: React.FC = ({
size,
colorScheme,
searchQuery,
+ lastHoveredItem,
]
)
return (
- {children}
+
+ {children}
+
)
}
@@ -496,7 +517,17 @@ export interface PopoverItemProps extends React.HTMLAttributes {
*/
const PopoverItem = React.forwardRef(
(
- { className, active, rootOnly, disabled, showCheck = false, children, onClick, ...props },
+ {
+ className,
+ active,
+ rootOnly,
+ disabled,
+ showCheck = false,
+ children,
+ onClick,
+ onMouseEnter,
+ ...props
+ },
ref
) => {
const context = React.useContext(PopoverContext)
@@ -514,6 +545,12 @@ const PopoverItem = React.forwardRef(
onClick?.(e)
}
+ const handleMouseEnter = (e: React.MouseEvent) => {
+ // Clear last hovered item to close any open hover submenus
+ context?.setLastHoveredItem(null)
+ onMouseEnter?.(e)
+ }
+
return (
(
aria-selected={active}
aria-disabled={disabled}
onClick={handleClick}
+ onMouseEnter={handleMouseEnter}
{...props}
>
{children}
@@ -589,44 +627,150 @@ export interface PopoverFolderProps extends Omit
(
- ({ className, id, title, icon, onOpen, onSelect, children, active, ...props }, ref) => {
- const { openFolder, currentFolder, isInFolder, variant, size, colorScheme } =
- usePopoverContext()
+ (
+ {
+ className,
+ id,
+ title,
+ icon,
+ onOpen,
+ onSelect,
+ children,
+ active,
+ expandOnHover = false,
+ ...props
+ },
+ ref
+ ) => {
+ const {
+ openFolder,
+ currentFolder,
+ isInFolder,
+ variant,
+ size,
+ colorScheme,
+ lastHoveredItem,
+ setLastHoveredItem,
+ } = usePopoverContext()
+ const [submenuPosition, setSubmenuPosition] = React.useState<{ top: number; left: number }>({
+ top: 0,
+ left: 0,
+ })
+ const triggerRef = React.useRef(null)
+
+ // Submenu is open when this folder is the last hovered item (for expandOnHover mode)
+ const isHoverOpen = expandOnHover && lastHoveredItem === id
+
+ // Merge refs
+ const mergedRef = React.useCallback(
+ (node: HTMLDivElement | null) => {
+ triggerRef.current = node
+ if (typeof ref === 'function') {
+ ref(node)
+ } else if (ref) {
+ ref.current = node
+ }
+ },
+ [ref]
+ )
+ // If we're in a folder and this isn't the current one, hide
if (isInFolder && currentFolder !== id) return null
+ // If this folder is open via click (inline mode), render children directly
if (currentFolder === id) return <>{children}>
+ const handleClickOpen = () => {
+ openFolder(id, title, onOpen, onSelect)
+ }
+
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation()
- openFolder(id, title, onOpen, onSelect)
+ if (expandOnHover) {
+ // In hover mode, clicking opens inline and clears hover state
+ setLastHoveredItem(null)
+ }
+ handleClickOpen()
+ }
+
+ const handleMouseEnter = () => {
+ if (!expandOnHover) return
+
+ // Calculate position for submenu
+ if (triggerRef.current) {
+ const rect = triggerRef.current.getBoundingClientRect()
+ const parentPopover = triggerRef.current.closest('[data-radix-popper-content-wrapper]')
+ const parentRect = parentPopover?.getBoundingClientRect()
+
+ // Position to the right of the parent popover with a small gap
+ setSubmenuPosition({
+ top: rect.top,
+ left: parentRect ? parentRect.right + 4 : rect.right + 4,
+ })
+ }
+
+ setLastHoveredItem(id)
+ onOpen?.()
}
return (
-
- {icon}
- {title}
-
-
+ <>
+
+ {icon}
+ {title}
+
+
+
+ {/* Hover submenu - rendered as a portal to escape overflow clipping */}
+ {isHoverOpen &&
+ typeof document !== 'undefined' &&
+ createPortal(
+
+ {children}
+
,
+ document.body
+ )}
+ >
)
}
)
@@ -665,7 +809,10 @@ const PopoverBackButton = React.forwardRef {
+ e.stopPropagation()
+ closeFolder()
+ }}
{...props}
>
diff --git a/apps/sim/hooks/queries/workflows.ts b/apps/sim/hooks/queries/workflows.ts
index 881a0a9939..eaabbab66e 100644
--- a/apps/sim/hooks/queries/workflows.ts
+++ b/apps/sim/hooks/queries/workflows.ts
@@ -1,6 +1,7 @@
import { useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
import {
createOptimisticMutationHandlers,
@@ -8,10 +9,7 @@ import {
} from '@/hooks/queries/utils/optimistic-mutation'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
-import {
- generateCreativeWorkflowName,
- getNextWorkflowColor,
-} from '@/stores/workflows/registry/utils'
+import { generateCreativeWorkflowName } from '@/stores/workflows/registry/utils'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
diff --git a/apps/sim/lib/workflows/colors.ts b/apps/sim/lib/workflows/colors.ts
new file mode 100644
index 0000000000..f4b34468ad
--- /dev/null
+++ b/apps/sim/lib/workflows/colors.ts
@@ -0,0 +1,75 @@
+/**
+ * Workflow color constants and utilities.
+ * Centralized location for all workflow color-related functionality.
+ *
+ * Colors are aligned with the brand color scheme:
+ * - Purple: brand-400 (#8e4cfb)
+ * - Blue: brand-secondary (#33b4ff)
+ * - Green: brand-tertiary (#22c55e)
+ * - Red: text-error (#ef4444)
+ * - Orange: warning (#f97316)
+ * - Pink: (#ec4899)
+ */
+
+/**
+ * Full list of available workflow colors with names.
+ * Used for color picker and random color assignment.
+ * Each base color has 6 vibrant shades optimized for both light and dark themes.
+ */
+export const WORKFLOW_COLORS = [
+ // Shade 1 - all base colors (brightest)
+ { color: '#c084fc', name: 'Purple 1' },
+ { color: '#5ed8ff', name: 'Blue 1' },
+ { color: '#4aea7f', name: 'Green 1' },
+ { color: '#ff6b6b', name: 'Red 1' },
+ { color: '#ff9642', name: 'Orange 1' },
+ { color: '#f472b6', name: 'Pink 1' },
+
+ // Shade 2 - all base colors
+ { color: '#a855f7', name: 'Purple 2' },
+ { color: '#38c8ff', name: 'Blue 2' },
+ { color: '#2ed96a', name: 'Green 2' },
+ { color: '#ff5555', name: 'Red 2' },
+ { color: '#ff8328', name: 'Orange 2' },
+ { color: '#ec4899', name: 'Pink 2' },
+
+ // Shade 3 - all base colors
+ { color: '#9333ea', name: 'Purple 3' },
+ { color: '#33b4ff', name: 'Blue 3' },
+ { color: '#22c55e', name: 'Green 3' },
+ { color: '#ef4444', name: 'Red 3' },
+ { color: '#f97316', name: 'Orange 3' },
+ { color: '#e11d89', name: 'Pink 3' },
+
+ // Shade 4 - all base colors
+ { color: '#8e4cfb', name: 'Purple 4' },
+ { color: '#1e9de8', name: 'Blue 4' },
+ { color: '#18b04c', name: 'Green 4' },
+ { color: '#dc3535', name: 'Red 4' },
+ { color: '#e56004', name: 'Orange 4' },
+ { color: '#d61c7a', name: 'Pink 4' },
+
+ // Shade 5 - all base colors
+ { color: '#7c3aed', name: 'Purple 5' },
+ { color: '#1486d1', name: 'Blue 5' },
+ { color: '#0e9b3a', name: 'Green 5' },
+ { color: '#c92626', name: 'Red 5' },
+ { color: '#d14d00', name: 'Orange 5' },
+ { color: '#be185d', name: 'Pink 5' },
+
+ // Shade 6 - all base colors (darkest)
+ { color: '#6322c9', name: 'Purple 6' },
+ { color: '#0a6fb8', name: 'Blue 6' },
+ { color: '#048628', name: 'Green 6' },
+ { color: '#b61717', name: 'Red 6' },
+ { color: '#bd3a00', name: 'Orange 6' },
+ { color: '#9d174d', name: 'Pink 6' },
+] as const
+
+/**
+ * Generates a random color for a new workflow
+ * @returns A hex color string from the available workflow colors
+ */
+export function getNextWorkflowColor(): string {
+ return WORKFLOW_COLORS[Math.floor(Math.random() * WORKFLOW_COLORS.length)].color
+}
diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts
index 6ba54ec1fa..33d2c0d9ef 100644
--- a/apps/sim/stores/workflows/registry/store.ts
+++ b/apps/sim/stores/workflows/registry/store.ts
@@ -3,6 +3,7 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { withOptimisticUpdate } from '@/lib/core/utils/optimistic-update'
import { DEFAULT_DUPLICATE_OFFSET } from '@/lib/workflows/autolayout/constants'
+import { getNextWorkflowColor } from '@/lib/workflows/colors'
import { buildDefaultWorkflowArtifacts } from '@/lib/workflows/defaults'
import { useVariablesStore } from '@/stores/panel/variables/store'
import type {
@@ -11,7 +12,6 @@ import type {
WorkflowMetadata,
WorkflowRegistry,
} from '@/stores/workflows/registry/types'
-import { getNextWorkflowColor } from '@/stores/workflows/registry/utils'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
import { getUniqueBlockName, regenerateBlockIds } from '@/stores/workflows/utils'
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
diff --git a/apps/sim/stores/workflows/registry/utils.ts b/apps/sim/stores/workflows/registry/utils.ts
index e3a91c1a97..8be102e4a6 100644
--- a/apps/sim/stores/workflows/registry/utils.ts
+++ b/apps/sim/stores/workflows/registry/utils.ts
@@ -1,321 +1,410 @@
-// Available workflow colors
-export const WORKFLOW_COLORS = [
- // Blues - vibrant blue tones
- '#3972F6', // Blue (original)
- '#2E5BF5', // Deeper Blue
- '#1E4BF4', // Royal Blue
- '#0D3BF3', // Deep Royal Blue
-
- // Pinks/Magentas - vibrant pink and magenta tones
- '#F639DD', // Pink/Magenta (original)
- '#F529CF', // Deep Magenta
- '#F749E7', // Light Magenta
- '#F419C1', // Hot Pink
-
- // Oranges/Yellows - vibrant orange and yellow tones
- '#F6B539', // Orange/Yellow (original)
- '#F5A529', // Deep Orange
- '#F49519', // Burnt Orange
- '#F38509', // Deep Burnt Orange
-
- // Purples - vibrant purple tones
- '#8139F6', // Purple (original)
- '#7129F5', // Deep Purple
- '#6119F4', // Royal Purple
- '#5109F3', // Deep Royal Purple
-
- // Greens - vibrant green tones
- '#39B54A', // Green (original)
- '#29A53A', // Deep Green
- '#19952A', // Forest Green
- '#09851A', // Deep Forest Green
-
- // Teals/Cyans - vibrant teal and cyan tones
- '#39B5AB', // Teal (original)
- '#29A59B', // Deep Teal
- '#19958B', // Dark Teal
- '#09857B', // Deep Dark Teal
-
- // Reds/Red-Oranges - vibrant red and red-orange tones
- '#F66839', // Red/Orange (original)
- '#F55829', // Deep Red-Orange
- '#F44819', // Burnt Red
- '#F33809', // Deep Burnt Red
-
- // Additional vibrant colors for variety
- // Corals - warm coral tones
- '#F6397A', // Coral
- '#F5296A', // Deep Coral
- '#F7498A', // Light Coral
-
- // Crimsons - deep red tones
- '#DC143C', // Crimson
- '#CC042C', // Deep Crimson
- '#EC243C', // Light Crimson
- '#BC003C', // Dark Crimson
- '#FC343C', // Bright Crimson
-
- // Mint - fresh green tones
- '#00FF7F', // Mint Green
- '#00EF6F', // Deep Mint
- '#00DF5F', // Dark Mint
-
- // Slate - blue-gray tones
- '#6A5ACD', // Slate Blue
- '#5A4ABD', // Deep Slate
- '#4A3AAD', // Dark Slate
-
- // Amber - warm orange-yellow tones
- '#FFBF00', // Amber
- '#EFAF00', // Deep Amber
- '#DF9F00', // Dark Amber
-]
-
-// Generates a random color for a new workflow
-export function getNextWorkflowColor(): string {
- // Simply return a random color from the available colors
- return WORKFLOW_COLORS[Math.floor(Math.random() * WORKFLOW_COLORS.length)]
-}
-
-// Adjectives and nouns for creative workflow names
+// Cosmos-themed adjectives and nouns for creative workflow names (max 9 chars each)
const ADJECTIVES = [
+ // Light & Luminosity
+ 'Radiant',
+ 'Luminous',
'Blazing',
- 'Crystal',
- 'Golden',
- 'Silver',
- 'Mystic',
- 'Cosmic',
- 'Electric',
- 'Frozen',
- 'Burning',
- 'Shining',
- 'Dancing',
- 'Flying',
- 'Roaring',
- 'Whispering',
'Glowing',
- 'Sparkling',
- 'Thunder',
- 'Lightning',
- 'Storm',
- 'Ocean',
- 'Mountain',
- 'Forest',
- 'Desert',
- 'Arctic',
- 'Tropical',
- 'Midnight',
- 'Dawn',
- 'Sunset',
- 'Rainbow',
- 'Diamond',
- 'Ruby',
- 'Emerald',
- 'Sapphire',
- 'Pearl',
- 'Jade',
- 'Amber',
- 'Coral',
- 'Ivory',
- 'Obsidian',
- 'Marble',
- 'Velvet',
- 'Silk',
- 'Satin',
- 'Linen',
- 'Cotton',
- 'Wool',
- 'Cashmere',
- 'Denim',
- 'Neon',
- 'Pastel',
- 'Vibrant',
- 'Muted',
- 'Bold',
- 'Subtle',
'Bright',
- 'Dark',
- 'Ancient',
- 'Modern',
- 'Eternal',
- 'Swift',
- 'Radiant',
- 'Quantum',
+ 'Gleaming',
+ 'Shining',
+ 'Lustrous',
+ 'Flaring',
+ 'Vivid',
+ 'Dazzling',
+ 'Beaming',
+ 'Brilliant',
+ 'Lit',
+ 'Ablaze',
+ // Celestial Descriptors
'Stellar',
+ 'Cosmic',
+ 'Astral',
+ 'Galactic',
+ 'Nebular',
+ 'Orbital',
'Lunar',
'Solar',
+ 'Starlit',
+ 'Heavenly',
'Celestial',
- 'Ethereal',
- 'Phantom',
- 'Shadow',
+ 'Sidereal',
+ 'Planetary',
+ 'Starry',
+ 'Spacial',
+ // Scale & Magnitude
+ 'Infinite',
+ 'Vast',
+ 'Boundless',
+ 'Immense',
+ 'Colossal',
+ 'Titanic',
+ 'Massive',
+ 'Grand',
+ 'Supreme',
+ 'Ultimate',
+ 'Epic',
+ 'Enormous',
+ 'Gigantic',
+ 'Limitless',
+ 'Total',
+ // Temporal
+ 'Eternal',
+ 'Ancient',
+ 'Timeless',
+ 'Enduring',
+ 'Ageless',
+ 'Immortal',
+ 'Primal',
+ 'Nascent',
+ 'First',
+ 'Elder',
+ 'Lasting',
+ 'Undying',
+ 'Perpetual',
+ 'Final',
+ 'Prime',
+ // Movement & Energy
+ 'Sidbuck',
+ 'Swift',
+ 'Drifting',
+ 'Spinning',
+ 'Surging',
+ 'Pulsing',
+ 'Soaring',
+ 'Racing',
+ 'Falling',
+ 'Rising',
+ 'Circling',
+ 'Streaking',
+ 'Hurtling',
+ 'Floating',
+ 'Orbiting',
+ 'Spiraling',
+ // Colors of Space
'Crimson',
'Azure',
'Violet',
- 'Scarlet',
- 'Magenta',
- 'Turquoise',
'Indigo',
- 'Jade',
- 'Noble',
- 'Regal',
- 'Imperial',
- 'Royal',
- 'Supreme',
- 'Prime',
- 'Elite',
- 'Ultra',
- 'Mega',
- 'Hyper',
- 'Super',
- 'Neo',
- 'Cyber',
- 'Digital',
- 'Virtual',
- 'Sonic',
+ 'Amber',
+ 'Sapphire',
+ 'Obsidian',
+ 'Silver',
+ 'Golden',
+ 'Scarlet',
+ 'Cobalt',
+ 'Emerald',
+ 'Ruby',
+ 'Onyx',
+ 'Ivory',
+ // Physical Properties
+ 'Magnetic',
+ 'Quantum',
+ 'Thermal',
+ 'Photonic',
+ 'Ionic',
+ 'Plasma',
+ 'Spectral',
+ 'Charged',
+ 'Polar',
+ 'Dense',
'Atomic',
'Nuclear',
- 'Laser',
- 'Plasma',
- 'Magnetic',
+ 'Electric',
+ 'Kinetic',
+ 'Static',
+ // Atmosphere & Mystery
+ 'Ethereal',
+ 'Mystic',
+ 'Phantom',
+ 'Shadow',
+ 'Silent',
+ 'Distant',
+ 'Hidden',
+ 'Veiled',
+ 'Fading',
+ 'Arcane',
+ 'Cryptic',
+ 'Obscure',
+ 'Dim',
+ 'Dusky',
+ 'Shrouded',
+ // Temperature & State
+ 'Frozen',
+ 'Burning',
+ 'Molten',
+ 'Volatile',
+ 'Icy',
+ 'Fiery',
+ 'Cool',
+ 'Warm',
+ 'Cold',
+ 'Hot',
+ 'Searing',
+ 'Frigid',
+ 'Scalding',
+ 'Chilled',
+ 'Heated',
+ // Power & Force
+ 'Mighty',
+ 'Fierce',
+ 'Raging',
+ 'Wild',
+ 'Serene',
+ 'Tranquil',
+ 'Harmonic',
+ 'Resonant',
+ 'Steady',
+ 'Bold',
+ 'Potent',
+ 'Violent',
+ 'Calm',
+ 'Furious',
+ 'Forceful',
+ // Texture & Form
+ 'Smooth',
+ 'Jagged',
+ 'Fractured',
+ 'Solid',
+ 'Hollow',
+ 'Curved',
+ 'Sharp',
+ 'Fluid',
+ 'Rigid',
+ 'Warped',
+ // Rare & Precious
+ 'Noble',
+ 'Pure',
+ 'Rare',
+ 'Pristine',
+ 'Flawless',
+ 'Unique',
+ 'Exotic',
+ 'Sacred',
+ 'Divine',
+ 'Hallowed',
]
const NOUNS = [
- 'Phoenix',
- 'Dragon',
- 'Eagle',
- 'Wolf',
- 'Lion',
- 'Tiger',
- 'Panther',
- 'Falcon',
- 'Hawk',
- 'Raven',
- 'Swan',
- 'Dove',
- 'Butterfly',
- 'Firefly',
- 'Dragonfly',
- 'Hummingbird',
+ // Stars & Stellar Objects
+ 'Star',
+ 'Sun',
+ 'Pulsar',
+ 'Quasar',
+ 'Magnetar',
+ 'Nova',
+ 'Supernova',
+ 'Hypernova',
+ 'Neutron',
+ 'Dwarf',
+ 'Giant',
+ 'Protostar',
+ 'Blazar',
+ 'Cepheid',
+ 'Binary',
+ // Galaxies & Clusters
'Galaxy',
'Nebula',
+ 'Cluster',
+ 'Void',
+ 'Filament',
+ 'Halo',
+ 'Bulge',
+ 'Spiral',
+ 'Ellipse',
+ 'Arm',
+ 'Disk',
+ 'Shell',
+ 'Remnant',
+ 'Cloud',
+ 'Dust',
+ // Planets & Moons
+ 'Planet',
+ 'Moon',
+ 'World',
+ 'Exoplanet',
+ 'Jovian',
+ 'Titan',
+ 'Europa',
+ 'Io',
+ 'Callisto',
+ 'Ganymede',
+ 'Triton',
+ 'Phobos',
+ 'Deimos',
+ 'Enceladus',
+ 'Charon',
+ // Small Bodies
'Comet',
'Meteor',
- 'Star',
- 'Moon',
- 'Sun',
- 'Planet',
'Asteroid',
- 'Constellation',
- 'Aurora',
+ 'Meteorite',
+ 'Bolide',
+ 'Fireball',
+ 'Iceball',
+ 'Plutino',
+ 'Centaur',
+ 'Trojan',
+ 'Shard',
+ 'Fragment',
+ 'Debris',
+ 'Rock',
+ 'Ice',
+ // Constellations & Myths
+ 'Orion',
+ 'Andromeda',
+ 'Perseus',
+ 'Pegasus',
+ 'Phoenix',
+ 'Draco',
+ 'Cygnus',
+ 'Aquila',
+ 'Lyra',
+ 'Vega',
+ 'Centaurus',
+ 'Hydra',
+ 'Sirius',
+ 'Polaris',
+ 'Altair',
+ // Celestial Phenomena
'Eclipse',
- 'Solstice',
- 'Equinox',
+ 'Aurora',
+ 'Corona',
+ 'Flare',
+ 'Storm',
+ 'Vortex',
+ 'Jet',
+ 'Burst',
+ 'Pulse',
+ 'Wave',
+ 'Ripple',
+ 'Shimmer',
+ 'Glow',
+ 'Flash',
+ 'Spark',
+ // Cosmic Structures
'Horizon',
'Zenith',
- 'Castle',
- 'Tower',
- 'Bridge',
- 'Garden',
- 'Fountain',
- 'Palace',
- 'Temple',
- 'Cathedral',
- 'Lighthouse',
- 'Windmill',
- 'Waterfall',
- 'Canyon',
- 'Valley',
- 'Peak',
- 'Ridge',
- 'Cliff',
- 'Ocean',
- 'River',
- 'Lake',
+ 'Nadir',
+ 'Apex',
+ 'Meridian',
+ 'Equinox',
+ 'Solstice',
+ 'Transit',
+ 'Aphelion',
+ 'Orbit',
+ 'Axis',
+ 'Pole',
+ 'Equator',
+ 'Limb',
+ 'Arc',
+ // Space & Dimensions
+ 'Cosmos',
+ 'Universe',
+ 'Dimension',
+ 'Realm',
+ 'Expanse',
+ 'Infinity',
+ 'Continuum',
+ 'Manifold',
+ 'Abyss',
+ 'Ether',
+ 'Vacuum',
+ 'Space',
+ 'Fabric',
+ 'Plane',
+ 'Domain',
+ // Energy & Particles
+ 'Photon',
+ 'Neutrino',
+ 'Proton',
+ 'Electron',
+ 'Positron',
+ 'Quark',
+ 'Boson',
+ 'Fermion',
+ 'Tachyon',
+ 'Graviton',
+ 'Meson',
+ 'Gluon',
+ 'Lepton',
+ 'Muon',
+ 'Pion',
+ // Regions & Zones
+ 'Sector',
+ 'Quadrant',
+ 'Zone',
+ 'Belt',
+ 'Ring',
+ 'Field',
'Stream',
- 'Pond',
- 'Bay',
- 'Cove',
- 'Harbor',
- 'Island',
- 'Peninsula',
- 'Archipelago',
- 'Atoll',
- 'Reef',
- 'Lagoon',
- 'Fjord',
- 'Delta',
- 'Cake',
- 'Cookie',
- 'Muffin',
- 'Cupcake',
- 'Pie',
- 'Tart',
- 'Brownie',
- 'Donut',
- 'Pancake',
- 'Waffle',
- 'Croissant',
- 'Bagel',
- 'Pretzel',
- 'Biscuit',
- 'Scone',
- 'Crumpet',
- 'Thunder',
- 'Blizzard',
- 'Tornado',
- 'Hurricane',
- 'Tsunami',
- 'Volcano',
- 'Glacier',
- 'Avalanche',
- 'Vortex',
- 'Tempest',
- 'Maelstrom',
- 'Whirlwind',
- 'Cyclone',
- 'Typhoon',
- 'Monsoon',
- 'Anvil',
- 'Hammer',
- 'Forge',
- 'Blade',
- 'Sword',
- 'Shield',
- 'Arrow',
- 'Spear',
- 'Crown',
- 'Throne',
- 'Scepter',
- 'Orb',
- 'Gem',
- 'Crystal',
- 'Prism',
- 'Spectrum',
+ 'Current',
+ 'Wake',
+ 'Region',
+ 'Frontier',
+ 'Border',
+ 'Edge',
+ 'Margin',
+ 'Rim',
+ // Navigation & Discovery
'Beacon',
'Signal',
- 'Pulse',
- 'Wave',
- 'Surge',
- 'Tide',
- 'Current',
- 'Flow',
- 'Circuit',
- 'Node',
+ 'Probe',
+ 'Voyager',
+ 'Pioneer',
+ 'Seeker',
+ 'Wanderer',
+ 'Nomad',
+ 'Drifter',
+ 'Scout',
+ 'Explorer',
+ 'Ranger',
+ 'Surveyor',
+ 'Sentinel',
+ 'Watcher',
+ // Portals & Passages
+ 'Gateway',
+ 'Portal',
+ 'Nexus',
+ 'Bridge',
+ 'Conduit',
+ 'Channel',
+ 'Passage',
+ 'Rift',
+ 'Warp',
+ 'Fold',
+ 'Tunnel',
+ 'Crossing',
+ 'Link',
+ 'Path',
+ 'Route',
+ // Core & Systems
'Core',
'Matrix',
+ 'Lattice',
'Network',
- 'System',
- 'Engine',
+ 'Circuit',
+ 'Array',
'Reactor',
- 'Generator',
- 'Dynamo',
- 'Catalyst',
- 'Nexus',
- 'Portal',
- 'Gateway',
- 'Passage',
- 'Conduit',
- 'Channel',
+ 'Engine',
+ 'Forge',
+ 'Crucible',
+ 'Hub',
+ 'Node',
+ 'Kernel',
+ 'Center',
+ 'Heart',
+ // Cosmic Objects
+ 'Crater',
+ 'Rift',
+ 'Chasm',
+ 'Canyon',
+ 'Peak',
+ 'Ridge',
+ 'Basin',
+ 'Plateau',
+ 'Valley',
+ 'Trench',
]
/**