Procházet zdrojové kódy

feat(dashboard): Search command palette first shot

David Höck před 5 měsíci
rodič
revize
62df2468bd

+ 19 - 16
GLOBAL_SEARCH_IMPLEMENTATION_PLAN.md

@@ -846,6 +846,11 @@ export class WebsiteContentService {
 
 ## Frontend Implementation
 
+## Data Fetching
+
+NEVER use Apollo Client or any other fetching library. We expose our own API client via `import { api } from '@/vdb/graphql/api.js';`
+The API Client must be combined with Tanstack Query, mostly `useQuery` or `useMutation`.
+
 ### Integration with App Layout
 
 Based on the current dashboard structure, the command palette should be integrated into the main app layout to be available globally. Here's how to modify the existing `app-layout.tsx`:
@@ -859,7 +864,7 @@ import { useKeyboardShortcuts } from '@/vdb/hooks/use-keyboard-shortcuts.js';
 export function AppLayout() {
     const { settings } = useUserSettings();
     const { isCommandPaletteOpen, setIsCommandPaletteOpen } = useKeyboardShortcuts();
-    
+
     return (
         <SearchProvider>
             <SidebarProvider>
@@ -883,9 +888,9 @@ export function AppLayout() {
                     </div>
                 </SidebarInset>
                 <PrereleasePopup />
-                
+
                 {/* Global Command Palette */}
-                <CommandPalette 
+                <CommandPalette
                     isOpen={isCommandPaletteOpen}
                     onOpenChange={setIsCommandPaletteOpen}
                 />
@@ -1013,15 +1018,10 @@ export const useQuickActions = () => {
         const currentContextActions = getContextActions(
             location.pathname,
             // Extract entity type from route
-            location.pathname.split('/')[1]
+            location.pathname.split('/')[1],
         );
 
-        return [
-            ...builtInGlobalActions,
-            ...globalActions,
-            ...currentContextActions,
-            ...contextActions,
-        ];
+        return [...builtInGlobalActions, ...globalActions, ...currentContextActions, ...contextActions];
     }, [location.pathname, globalActions, contextActions]);
 
     return {
@@ -1057,7 +1057,7 @@ export const SearchProvider = ({ children }: { children: ReactNode }) => {
     const [isSearching, setIsSearching] = useState(false);
 
     return (
-        <SearchContext.Provider 
+        <SearchContext.Provider
             value={{
                 isCommandPaletteOpen,
                 setIsCommandPaletteOpen,
@@ -1100,17 +1100,17 @@ export const useGlobalSearch = () => {
                 query: debouncedQuery,
                 types: [], // All types by default
                 limit: 20,
-            }
+            },
         },
         skip: !debouncedQuery || debouncedQuery.length < 2,
-        onCompleted: (data) => {
+        onCompleted: data => {
             setSearchResults(data.globalSearch.items);
             setIsSearching(false);
         },
         onError: () => {
             setSearchResults([]);
             setIsSearching(false);
-        }
+        },
     });
 
     // Update loading state
@@ -1126,6 +1126,7 @@ export const useGlobalSearch = () => {
     };
 };
 ```
+
     };
 
     // Register custom actions from extensions
@@ -1142,7 +1143,9 @@ export const useGlobalSearch = () => {
         contextActions: [...getContextActions(location.pathname), ...contextActions],
         registerCustomAction,
     };
+
 };
+
 ```
 
 ### Integration Points
@@ -1391,7 +1394,7 @@ packages/dashboard/src/lib/components/global-search/
 ├── use-search-shortcuts.ts
 └── use-quick-actions.ts
 
-```
+````
 
 ### Files to Modify
 
@@ -1446,7 +1449,7 @@ $$ LANGUAGE plpgsql;
 CREATE TRIGGER search_vector_update
     BEFORE INSERT OR UPDATE ON search_index
     FOR EACH ROW EXECUTE FUNCTION update_search_vector();
-```
+````
 
 ## Configuration Examples
 

+ 4 - 1
packages/dashboard/src/app/app-providers.tsx

@@ -1,6 +1,7 @@
 import { AuthProvider } from '@/vdb/providers/auth.js';
 import { ChannelProvider } from '@/vdb/providers/channel-provider.js';
 import { I18nProvider } from '@/vdb/providers/i18n-provider.js';
+import { SearchProvider } from '@/vdb/providers/search-provider.js';
 import { ServerConfigProvider } from '@/vdb/providers/server-config.js';
 import { ThemeProvider } from '@/vdb/providers/theme-provider.js';
 import { UserSettingsProvider } from '@/vdb/providers/user-settings.js';
@@ -18,7 +19,9 @@ export function AppProviders({ children }: { children: React.ReactNode }) {
                     <ThemeProvider defaultTheme="system">
                         <AuthProvider>
                             <ServerConfigProvider>
-                                <ChannelProvider>{children}</ChannelProvider>
+                                <ChannelProvider>
+                                    <SearchProvider>{children}</SearchProvider>
+                                </ChannelProvider>
                             </ServerConfigProvider>
                         </AuthProvider>
                     </ThemeProvider>

+ 111 - 0
packages/dashboard/src/lib/components/global-search/README.md

@@ -0,0 +1,111 @@
+# Global Search Frontend Implementation
+
+This directory contains the React components and hooks for the Vendure dashboard global search feature.
+
+## Components
+
+### Core Components
+- **`CommandPalette`** - Main search dialog using cmdk
+- **`SearchTrigger`** - Search button in the header with keyboard shortcut display
+
+### Search Results
+- **`SearchResultsList`** - Container for search results grouped by type
+- **`SearchResultItem`** - Individual search result with icon, title, subtitle, and actions
+
+### Quick Actions
+- **`QuickActionsList`** - Container for quick actions filtered by search query
+- **`QuickActionItem`** - Individual quick action with keyboard shortcuts
+
+### Utility Components
+- **`RecentSearches`** - Shows recent search queries when no search is active
+
+## Hooks
+
+### Search Hooks
+- **`useGlobalSearch`** - Handles search API integration with debouncing
+- **`useQuickActions`** - Manages built-in and custom quick actions
+- **`useKeyboardShortcuts`** - Global keyboard shortcut handling
+
+## Context
+
+### SearchProvider
+- **`SearchProvider`** - Global state management for search functionality
+- **`useSearchContext`** - Access to search state and actions
+
+## Features Implemented
+
+### 🔍 Global Search
+- Command palette interface (⌘K to open)
+- Real-time search with 300ms debouncing
+- Grouped results by entity type
+- External link handling
+- Recent search history
+
+### ⚡ Quick Actions
+- Built-in global actions (create, navigate, etc.)
+- Context-aware actions (based on current page)
+- Custom keyboard shortcuts
+- Action categorization
+
+### ⌨️ Keyboard Navigation
+- ⌘K / Ctrl+K to open command palette
+- Escape to close
+- All quick action shortcuts work globally
+- Full keyboard navigation within palette
+
+### 🎨 UI/UX
+- Modern command palette design using existing cmdk components
+- Icons for different entity types
+- Badges for result types and context actions
+- Loading states and empty states
+- Responsive design
+
+## Usage
+
+The global search is automatically integrated into the `AppLayout` and will be available on all authenticated pages. Users can:
+
+1. Click the search button in the header
+2. Press ⌘K (Mac) or Ctrl+K (Windows/Linux)
+3. Start typing to search entities
+4. Use quick actions for common tasks
+5. Navigate using keyboard or mouse
+
+## Extension Points
+
+### Custom Quick Actions
+Developers can register custom quick actions through the extension API (to be implemented in backend):
+
+```typescript
+// Example of how custom actions would be registered
+registerQuickAction({
+  id: 'my-custom-action',
+  label: 'My Custom Action',
+  shortcut: 'ctrl+shift+m',
+  handler: (context) => {
+    // Custom action logic
+  }
+});
+```
+
+## Backend Integration
+
+This frontend implementation expects the following GraphQL API:
+
+```graphql
+query GlobalSearch($input: GlobalSearchInput!) {
+  globalSearch(input: $input) {
+    id
+    type
+    title
+    subtitle
+    description
+    url
+    thumbnailUrl
+    metadata
+    relevanceScore
+    lastModified
+  }
+}
+```
+
+The backend implementation is defined in the `GLOBAL_SEARCH_IMPLEMENTATION_PLAN.md` and should be implemented separately.

+ 84 - 0
packages/dashboard/src/lib/components/global-search/command-palette.tsx

@@ -0,0 +1,84 @@
+import {
+    CommandDialog,
+    CommandInput,
+    CommandList,
+    CommandSeparator,
+} from '@/vdb/components/ui/command.js';
+import { useSearchContext } from '@/vdb/providers/search-provider.js';
+import { useGlobalSearch } from './hooks/use-global-search.js';
+import { QuickActionsList } from './quick-actions-list.js';
+import { SearchResultsList } from './search-results-list.js';
+import { RecentSearches } from './recent-searches.js';
+import { useEffect } from 'react';
+
+interface CommandPaletteProps {
+    isOpen: boolean;
+    onOpenChange: (open: boolean) => void;
+}
+
+export function CommandPalette({ isOpen, onOpenChange }: CommandPaletteProps) {
+    const { 
+        searchQuery, 
+        setSearchQuery,
+        setSearchResults,
+        setIsSearching
+    } = useSearchContext();
+    
+    const { hasQuery } = useGlobalSearch();
+
+    // Clear search state when dialog closes
+    useEffect(() => {
+        if (!isOpen) {
+            setSearchQuery('');
+            setSearchResults([]);
+            setIsSearching(false);
+        }
+    }, [isOpen, setSearchQuery, setSearchResults, setIsSearching]);
+
+    const handleClose = () => {
+        onOpenChange(false);
+    };
+
+    const handleResultSelect = () => {
+        handleClose();
+    };
+
+    const handleActionSelect = () => {
+        handleClose();
+    };
+
+    return (
+        <CommandDialog
+            open={isOpen}
+            onOpenChange={onOpenChange}
+            title="Command Palette"
+            description="Search for entities, quick actions, and more..."
+        >
+            <CommandInput
+                placeholder="Search products, customers, orders, or type a command..."
+                value={searchQuery}
+                onValueChange={setSearchQuery}
+                className="border-0"
+            />
+            
+            <CommandList className="max-h-[400px]">
+                {/* Quick Actions - Always shown first, filtered by search query */}
+                <QuickActionsList 
+                    searchQuery={searchQuery}
+                    onActionSelect={handleActionSelect}
+                />
+
+                {/* Separator between actions and search results */}
+                {hasQuery && <CommandSeparator />}
+
+                {/* Search Results - Only shown when there's a query */}
+                {hasQuery && (
+                    <SearchResultsList onResultSelect={handleResultSelect} />
+                )}
+
+                {/* Recent Searches - Only shown when there's no query */}
+                {!hasQuery && <RecentSearches onSearchSelect={setSearchQuery} />}
+            </CommandList>
+        </CommandDialog>
+    );
+}

+ 53 - 0
packages/dashboard/src/lib/components/global-search/hooks/use-global-search.ts

@@ -0,0 +1,53 @@
+import { api } from '@/vdb/graphql/api.js';
+import { GLOBAL_SEARCH_QUERY } from '@/vdb/graphql/global-search.js';
+import { useDebounce } from '@/vdb/hooks/use-debounce.js';
+import { useSearchContext } from '@/vdb/providers/search-provider.js';
+import { useQuery } from '@tanstack/react-query';
+import { useEffect } from 'react';
+
+export const useGlobalSearch = () => {
+    const { searchQuery, setSearchResults, setIsSearching, selectedTypes, addRecentSearch } =
+        useSearchContext();
+
+    const debouncedQuery = useDebounce(searchQuery, 300);
+
+    const { data, isLoading, error } = useQuery({
+        queryKey: ['globalSearch', debouncedQuery, selectedTypes],
+        queryFn: () => {
+            return api.query(GLOBAL_SEARCH_QUERY, {
+                input: {
+                    query: debouncedQuery,
+                    types: selectedTypes.length > 0 ? selectedTypes : undefined,
+                    limit: 20,
+                    skip: 0,
+                },
+            });
+        },
+        enabled: !debouncedQuery || debouncedQuery.length < 2,
+    });
+
+    useEffect(() => {
+        if (data?.globalSearch) {
+            setSearchResults(data.globalSearch);
+            setIsSearching(false);
+        }
+    }, [data, setSearchResults, setIsSearching]);
+
+    // Update loading state
+    useEffect(() => {
+        if (debouncedQuery && debouncedQuery.length >= 2) {
+            setIsSearching(isLoading);
+        } else {
+            setIsSearching(false);
+            setSearchResults([]);
+        }
+    }, [isLoading, debouncedQuery, setIsSearching, setSearchResults]);
+
+    return {
+        results: data?.globalSearch || [],
+        loading: isLoading,
+        error,
+        totalCount: data?.globalSearch?.length || 0,
+        hasQuery: debouncedQuery.length >= 2,
+    };
+};

+ 76 - 0
packages/dashboard/src/lib/components/global-search/hooks/use-keyboard-shortcuts.ts

@@ -0,0 +1,76 @@
+import { useSearchContext } from '@/vdb/providers/search-provider.js';
+import { useCallback, useEffect } from 'react';
+
+import { useQuickActions } from './use-quick-actions.js';
+
+export const useKeyboardShortcuts = () => {
+    const { isCommandPaletteOpen, setIsCommandPaletteOpen } = useSearchContext();
+    const { actions, executeAction } = useQuickActions();
+
+    const handleKeyDown = useCallback(
+        (event: KeyboardEvent) => {
+            // Check if user is typing in an input field
+            const isTyping =
+                document.activeElement instanceof HTMLInputElement ||
+                document.activeElement instanceof HTMLTextAreaElement ||
+                (document.activeElement as HTMLElement)?.contentEditable === 'true';
+
+            // Global shortcuts that work even when typing (with Cmd/Ctrl)
+            if (event.metaKey || event.ctrlKey) {
+                // Cmd/Ctrl + K to open command palette
+                if (event.key === 'k') {
+                    event.preventDefault();
+                    setIsCommandPaletteOpen(true);
+                    return;
+                }
+
+                // Check for quick action shortcuts
+                const shortcutKey = event.shiftKey
+                    ? `${event.metaKey ? 'cmd' : 'ctrl'}+shift+${event.key.toLowerCase()}`
+                    : `${event.metaKey ? 'cmd' : 'ctrl'}+${event.key.toLowerCase()}`;
+
+                const matchingAction = actions.find(
+                    action =>
+                        action.shortcut === shortcutKey ||
+                        action.shortcut === shortcutKey.replace('cmd', 'ctrl') ||
+                        action.shortcut === shortcutKey.replace('ctrl', 'cmd'),
+                );
+
+                if (matchingAction) {
+                    event.preventDefault();
+
+                    // Don't execute if command palette is open, let it handle the shortcut
+                    if (!isCommandPaletteOpen) {
+                        void executeAction(matchingAction.id);
+                    }
+                    return;
+                }
+            }
+
+            // Escape to close command palette
+            if (event.key === 'Escape' && isCommandPaletteOpen) {
+                event.preventDefault();
+                setIsCommandPaletteOpen(false);
+                return;
+            }
+
+            // Don't handle other shortcuts if user is typing
+            if (isTyping) {
+                return;
+            }
+        },
+        [isCommandPaletteOpen, setIsCommandPaletteOpen, actions, executeAction],
+    );
+
+    useEffect(() => {
+        document.addEventListener('keydown', handleKeyDown);
+        return () => {
+            document.removeEventListener('keydown', handleKeyDown);
+        };
+    }, [handleKeyDown]);
+
+    return {
+        isCommandPaletteOpen,
+        setIsCommandPaletteOpen,
+    };
+};

+ 275 - 0
packages/dashboard/src/lib/components/global-search/hooks/use-quick-actions.ts

@@ -0,0 +1,275 @@
+import { QuickAction, QuickActionContext, useSearchContext } from '@/vdb/providers/search-provider.js';
+import { useLocation, useNavigate } from '@tanstack/react-router';
+import { useCallback, useEffect, useMemo } from 'react';
+import { toast } from 'sonner';
+
+export const useQuickActions = () => {
+    const { quickActions, registerQuickAction } = useSearchContext();
+    const location = useLocation();
+    const navigate = useNavigate();
+
+    const createActionContext = useCallback((): QuickActionContext => {
+        // Extract entity info from current path
+        const pathSegments = location.pathname.split('/').filter(Boolean);
+        const currentEntityType = pathSegments[0];
+        const currentEntityId = pathSegments.find(
+            segment => segment.match(/^[a-f0-9-]{36}$/i) || segment.match(/^\d+$/),
+        );
+
+        return {
+            currentRoute: location.pathname,
+            currentEntityType,
+            currentEntityId,
+            navigate: (path: string) => navigate({ to: path }),
+            showNotification: (message: string, type: 'success' | 'error' | 'warning' = 'success') => {
+                toast[type](message);
+            },
+            confirm: async (message: string) => {
+                return window.confirm(message);
+            },
+            executeGraphQL: async (query: string, variables?: any) => {
+                // perform API operation
+            },
+        };
+    }, [location, navigate]);
+
+    // Built-in global actions
+    const builtInGlobalActions: QuickAction[] = useMemo(
+        () => [
+            {
+                id: 'create-product',
+                label: 'Create New Product',
+                description: 'Create a new product',
+                icon: 'plus',
+                shortcut: 'ctrl+shift+p',
+                isContextAware: false,
+                handler: context => context.navigate('/products/create'),
+            },
+            {
+                id: 'create-customer',
+                label: 'Create New Customer',
+                description: 'Create a new customer',
+                icon: 'user-plus',
+                shortcut: 'ctrl+shift+c',
+                isContextAware: false,
+                handler: context => context.navigate('/customers/create'),
+            },
+            {
+                id: 'create-order',
+                label: 'Create New Order',
+                description: 'Create a new order',
+                icon: 'shopping-cart',
+                shortcut: 'ctrl+shift+o',
+                isContextAware: false,
+                handler: context => context.navigate('/orders/create'),
+            },
+            {
+                id: 'go-to-products',
+                label: 'Go to Products',
+                description: 'Navigate to products list',
+                icon: 'package',
+                isContextAware: false,
+                handler: context => context.navigate('/products'),
+            },
+            {
+                id: 'go-to-orders',
+                label: 'Go to Orders',
+                description: 'Navigate to orders list',
+                icon: 'shopping-cart',
+                isContextAware: false,
+                handler: context => context.navigate('/orders'),
+            },
+            {
+                id: 'go-to-customers',
+                label: 'Go to Customers',
+                description: 'Navigate to customers list',
+                icon: 'users',
+                isContextAware: false,
+                handler: context => context.navigate('/customers'),
+            },
+            {
+                id: 'go-to-profile',
+                label: 'Go to Profile',
+                description: 'Navigate to user profile',
+                icon: 'user',
+                isContextAware: false,
+                handler: context => context.navigate('/profile'),
+            },
+        ],
+        [],
+    );
+
+    // Context-aware actions based on current route
+    const getContextActions = useCallback(
+        (route: string, entityType?: string, entityId?: string): QuickAction[] => {
+            const actions: QuickAction[] = [];
+
+            // Product detail page actions
+            if (route.includes('/products/') && entityId) {
+                actions.push({
+                    id: 'duplicate-product',
+                    label: 'Duplicate Product',
+                    description: 'Duplicate current product',
+                    icon: 'copy',
+                    shortcut: 'ctrl+d',
+                    isContextAware: true,
+                    handler: async context => {
+                        const confirmed = await context.confirm('Duplicate this product?');
+                        if (confirmed) {
+                            // Implementation would go here
+                            context.showNotification('Product duplicated');
+                            context.navigate(`/products/${entityId}/duplicate`);
+                        }
+                    },
+                });
+
+                actions.push({
+                    id: 'add-variant',
+                    label: 'Add Product Variant',
+                    description: 'Add new variant to current product',
+                    icon: 'plus-square',
+                    shortcut: 'ctrl+shift+v',
+                    isContextAware: true,
+                    handler: context => {
+                        context.navigate(`/products/${entityId}/variants/create`);
+                    },
+                });
+            }
+
+            // Order detail page actions
+            if (route.includes('/orders/') && entityId) {
+                actions.push({
+                    id: 'fulfill-order',
+                    label: 'Fulfill Order',
+                    description: 'Fulfill current order',
+                    icon: 'truck',
+                    shortcut: 'ctrl+f',
+                    isContextAware: true,
+                    handler: context => {
+                        context.navigate(`/orders/${entityId}/fulfill`);
+                    },
+                });
+
+                actions.push({
+                    id: 'cancel-order',
+                    label: 'Cancel Order',
+                    description: 'Cancel current order',
+                    icon: 'x-circle',
+                    shortcut: 'ctrl+shift+x',
+                    isContextAware: true,
+                    handler: async context => {
+                        const confirmed = await context.confirm(
+                            'Are you sure you want to cancel this order?',
+                        );
+                        if (confirmed) {
+                            // Implementation would go here
+                            context.showNotification('Order cancelled');
+                        }
+                    },
+                });
+            }
+
+            // Customer detail page actions
+            if (route.includes('/customers/') && entityId) {
+                actions.push({
+                    id: 'view-customer-orders',
+                    label: 'View Customer Orders',
+                    description: 'View orders for current customer',
+                    icon: 'list',
+                    isContextAware: true,
+                    handler: context => {
+                        context.navigate(`/customers/${entityId}/orders`);
+                    },
+                });
+            }
+
+            // Any list page actions
+            if (route.includes('/list') || route.match(/\/(products|orders|customers|collections)$/)) {
+                actions.push({
+                    id: 'export-data',
+                    label: 'Export Data',
+                    description: 'Export current filtered data',
+                    icon: 'download',
+                    shortcut: 'ctrl+e',
+                    isContextAware: true,
+                    handler: async context => {
+                        context.showNotification('Export started', 'success');
+                        // Implementation would go here
+                    },
+                });
+
+                if (entityType) {
+                    actions.push({
+                        id: 'create-new-item',
+                        label: `Create New ${entityType.charAt(0).toUpperCase() + entityType.slice(1)}`,
+                        description: `Create new ${entityType}`,
+                        icon: 'plus',
+                        shortcut: 'ctrl+n',
+                        isContextAware: true,
+                        handler: context => {
+                            context.navigate(`/${entityType}/new`);
+                        },
+                    });
+                }
+            }
+
+            return actions;
+        },
+        [],
+    );
+
+    // Register built-in actions
+    useEffect(() => {
+        builtInGlobalActions.forEach(action => {
+            registerQuickAction(action);
+        });
+    }, [builtInGlobalActions, registerQuickAction]);
+
+    // Get all available actions for current context
+    const availableActions = useMemo(() => {
+        const pathSegments = location.pathname.split('/').filter(Boolean);
+        const entityType = pathSegments[0];
+        const entityId = pathSegments.find(
+            segment => segment.match(/^[a-f0-9-]{36}$/i) || segment.match(/^\d+$/),
+        );
+
+        const contextActions = getContextActions(location.pathname, entityType, entityId);
+
+        // Combine built-in global actions, context actions, and registered custom actions
+        const allActions = [
+            ...builtInGlobalActions,
+            ...contextActions,
+            ...quickActions.filter(
+                action =>
+                    !builtInGlobalActions.some(builtin => builtin.id === action.id) &&
+                    !contextActions.some(context => context.id === action.id),
+            ),
+        ];
+
+        return allActions;
+    }, [location.pathname, builtInGlobalActions, getContextActions, quickActions]);
+
+    const executeAction = useCallback(
+        async (actionId: string) => {
+            const action = availableActions.find(a => a.id === actionId);
+            if (!action) {
+                return;
+            }
+
+            try {
+                const context = createActionContext();
+                await action.handler(context);
+            } catch (error) {
+                toast.error(`Failed to execute action: ${action.label}`);
+            }
+        },
+        [availableActions, createActionContext],
+    );
+
+    return {
+        actions: availableActions,
+        executeAction,
+        registerAction: registerQuickAction,
+        createContext: createActionContext,
+    };
+};

+ 19 - 0
packages/dashboard/src/lib/components/global-search/index.ts

@@ -0,0 +1,19 @@
+// Main components
+export { CommandPalette } from './command-palette.js';
+export { SearchTrigger } from './search-trigger.js';
+
+// Search result components
+export { SearchResultItem } from './search-result-item.js';
+export { SearchResultsList } from './search-results-list.js';
+
+// Quick action components
+export { QuickActionItem } from './quick-action-item.js';
+export { QuickActionsList } from './quick-actions-list.js';
+
+// Utility components
+export { RecentSearches } from './recent-searches.js';
+
+// Hooks
+export { useGlobalSearch } from './hooks/use-global-search.js';
+export { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts.js';
+export { useQuickActions } from './hooks/use-quick-actions.js';

+ 66 - 0
packages/dashboard/src/lib/components/global-search/quick-action-item.tsx

@@ -0,0 +1,66 @@
+import { CommandItem, CommandShortcut } from '@/vdb/components/ui/command.js';
+import { Badge } from '@/vdb/components/ui/badge.js';
+import { QuickAction } from '@/vdb/providers/search-provider.js';
+import * as Icons from 'lucide-react';
+
+interface QuickActionItemProps {
+    action: QuickAction;
+    onSelect: (actionId: string) => void;
+}
+
+export function QuickActionItem({ action, onSelect }: QuickActionItemProps) {
+    // Get the icon component dynamically
+    const IconComponent = action.icon 
+        ? (Icons as any)[action.icon] || (Icons as any)[toPascalCase(action.icon)] || Icons.Zap
+        : Icons.Zap;
+
+    return (
+        <CommandItem 
+            key={action.id}
+            value={`${action.id}-${action.label}`}
+            onSelect={() => onSelect(action.id)}
+            className="flex items-center gap-3 p-3"
+        >
+            <div className="flex h-8 w-8 items-center justify-center rounded-md bg-accent">
+                <IconComponent className="h-4 w-4" />
+            </div>
+            
+            <div className="flex-1 space-y-1">
+                <div className="flex items-center gap-2">
+                    <span className="font-medium">{action.label}</span>
+                    {action.isContextAware && (
+                        <Badge variant="secondary" className="text-xs">
+                            Context
+                        </Badge>
+                    )}
+                </div>
+                {action.description && (
+                    <p className="text-xs text-muted-foreground">{action.description}</p>
+                )}
+            </div>
+
+            {action.shortcut && (
+                <CommandShortcut>
+                    {formatShortcut(action.shortcut)}
+                </CommandShortcut>
+            )}
+        </CommandItem>
+    );
+}
+
+function toPascalCase(str: string): string {
+    return str
+        .split('-')
+        .map(word => word.charAt(0).toUpperCase() + word.slice(1))
+        .join('');
+}
+
+function formatShortcut(shortcut: string): string {
+    return shortcut
+        .replace('ctrl', '⌘')
+        .replace('cmd', '⌘')
+        .replace('shift', '⇧')
+        .replace('alt', '⌥')
+        .replace('+', ' + ')
+        .toUpperCase();
+}

+ 69 - 0
packages/dashboard/src/lib/components/global-search/quick-actions-list.tsx

@@ -0,0 +1,69 @@
+import { CommandGroup } from '@/vdb/components/ui/command.js';
+import { QuickActionItem } from './quick-action-item.js';
+import { useQuickActions } from './hooks/use-quick-actions.js';
+import { useMemo } from 'react';
+
+interface QuickActionsListProps {
+    searchQuery: string;
+    onActionSelect: (actionId: string) => void;
+}
+
+export function QuickActionsList({ searchQuery, onActionSelect }: QuickActionsListProps) {
+    const { actions, executeAction } = useQuickActions();
+
+    // Filter actions based on search query
+    const filteredActions = useMemo(() => {
+        if (!searchQuery.trim()) {
+            // Show all actions when no search query
+            return actions;
+        }
+
+        const query = searchQuery.toLowerCase();
+        return actions.filter(action => 
+            action.label.toLowerCase().includes(query) ||
+            action.description?.toLowerCase().includes(query) ||
+            action.id.toLowerCase().includes(query)
+        );
+    }, [actions, searchQuery]);
+
+    const handleActionSelect = async (actionId: string) => {
+        onActionSelect(actionId);
+        await executeAction(actionId);
+    };
+
+    if (filteredActions.length === 0) {
+        return null;
+    }
+
+    // Group actions by type
+    const globalActions = filteredActions.filter(action => !action.isContextAware);
+    const contextActions = filteredActions.filter(action => action.isContextAware);
+
+    return (
+        <>
+            {globalActions.length > 0 && (
+                <CommandGroup heading="Quick Actions">
+                    {globalActions.map(action => (
+                        <QuickActionItem
+                            key={action.id}
+                            action={action}
+                            onSelect={handleActionSelect}
+                        />
+                    ))}
+                </CommandGroup>
+            )}
+
+            {contextActions.length > 0 && (
+                <CommandGroup heading="Context Actions">
+                    {contextActions.map(action => (
+                        <QuickActionItem
+                            key={action.id}
+                            action={action}
+                            onSelect={handleActionSelect}
+                        />
+                    ))}
+                </CommandGroup>
+            )}
+        </>
+    );
+}

+ 59 - 0
packages/dashboard/src/lib/components/global-search/recent-searches.tsx

@@ -0,0 +1,59 @@
+import { CommandGroup, CommandItem, CommandEmpty } from '@/vdb/components/ui/command.js';
+import { Button } from '@/vdb/components/ui/button.js';
+import { useSearchContext } from '@/vdb/providers/search-provider.js';
+import { Clock, X } from 'lucide-react';
+
+interface RecentSearchesProps {
+    onSearchSelect: (query: string) => void;
+}
+
+export function RecentSearches({ onSearchSelect }: RecentSearchesProps) {
+    const { recentSearches, clearRecentSearches } = useSearchContext();
+
+    if (recentSearches.length === 0) {
+        return (
+            <CommandGroup heading="Getting Started">
+                <CommandEmpty className="py-6 text-center">
+                    <div className="space-y-2">
+                        <p className="text-sm text-muted-foreground">
+                            Start typing to search products, customers, orders, and more...
+                        </p>
+                        <p className="text-xs text-muted-foreground">
+                            Use <kbd className="px-2 py-1 text-xs bg-muted rounded">⌘K</kbd> to open this anytime
+                        </p>
+                    </div>
+                </CommandEmpty>
+            </CommandGroup>
+        );
+    }
+
+    return (
+        <CommandGroup 
+            heading={
+                <div className="flex items-center justify-between">
+                    <span>Recent Searches</span>
+                    <Button
+                        variant="ghost"
+                        size="sm"
+                        className="h-auto p-1 text-xs"
+                        onClick={clearRecentSearches}
+                    >
+                        Clear
+                    </Button>
+                </div>
+            }
+        >
+            {recentSearches.map((query, index) => (
+                <CommandItem
+                    key={`${query}-${index}`}
+                    value={query}
+                    onSelect={() => onSearchSelect(query)}
+                    className="flex items-center gap-2 p-3"
+                >
+                    <Clock className="h-4 w-4 text-muted-foreground" />
+                    <span className="flex-1 text-sm">{query}</span>
+                </CommandItem>
+            ))}
+        </CommandGroup>
+    );
+}

+ 117 - 0
packages/dashboard/src/lib/components/global-search/search-result-item.tsx

@@ -0,0 +1,117 @@
+import { CommandItem } from '@/vdb/components/ui/command.js';
+import { Badge } from '@/vdb/components/ui/badge.js';
+import { SearchResult, SearchResultType } from '@/vdb/providers/search-provider.js';
+import * as Icons from 'lucide-react';
+import { useNavigate } from '@tanstack/react-router';
+
+interface SearchResultItemProps {
+    result: SearchResult;
+    onSelect: () => void;
+}
+
+export function SearchResultItem({ result, onSelect }: SearchResultItemProps) {
+    const navigate = useNavigate();
+    const IconComponent = getIconForResultType(result.type);
+
+    const handleSelect = () => {
+        onSelect();
+        
+        // Handle different URL types
+        if (result.url.startsWith('http')) {
+            // External URL - open in new tab
+            window.open(result.url, '_blank');
+        } else {
+            // Internal URL - navigate within the app
+            navigate({ to: result.url });
+        }
+    };
+
+    return (
+        <CommandItem 
+            key={result.id}
+            value={`${result.id}-${result.title}-${result.subtitle || ''}`}
+            onSelect={handleSelect}
+            className="flex items-center gap-3 p-3"
+        >
+            <div className="flex h-8 w-8 items-center justify-center rounded-md bg-secondary/50">
+                {result.thumbnailUrl ? (
+                    <img 
+                        src={result.thumbnailUrl} 
+                        alt={result.title}
+                        className="h-8 w-8 rounded-md object-cover"
+                    />
+                ) : (
+                    <IconComponent className="h-4 w-4 text-muted-foreground" />
+                )}
+            </div>
+            
+            <div className="flex-1 space-y-1 min-w-0">
+                <div className="flex items-center gap-2">
+                    <span className="font-medium truncate">{result.title}</span>
+                    <Badge variant="outline" className="text-xs shrink-0">
+                        {formatResultType(result.type)}
+                    </Badge>
+                </div>
+                
+                {result.subtitle && (
+                    <p className="text-xs text-muted-foreground truncate">
+                        {result.subtitle}
+                    </p>
+                )}
+                
+                {result.description && (
+                    <p className="text-xs text-muted-foreground line-clamp-2">
+                        {result.description}
+                    </p>
+                )}
+            </div>
+
+            {result.url.startsWith('http') && (
+                <Icons.ExternalLink className="h-3 w-3 text-muted-foreground shrink-0" />
+            )}
+        </CommandItem>
+    );
+}
+
+function getIconForResultType(type: SearchResultType): React.ComponentType<any> {
+    const iconMap: Record<SearchResultType, React.ComponentType<any>> = {
+        [SearchResultType.PRODUCT]: Icons.Package,
+        [SearchResultType.PRODUCT_VARIANT]: Icons.Package2,
+        [SearchResultType.CUSTOMER]: Icons.User,
+        [SearchResultType.ORDER]: Icons.ShoppingCart,
+        [SearchResultType.COLLECTION]: Icons.FolderOpen,
+        [SearchResultType.ADMINISTRATOR]: Icons.UserCog,
+        [SearchResultType.CHANNEL]: Icons.Globe,
+        [SearchResultType.ASSET]: Icons.Image,
+        [SearchResultType.FACET]: Icons.Tag,
+        [SearchResultType.FACET_VALUE]: Icons.Tags,
+        [SearchResultType.PROMOTION]: Icons.Percent,
+        [SearchResultType.PAYMENT_METHOD]: Icons.CreditCard,
+        [SearchResultType.SHIPPING_METHOD]: Icons.Truck,
+        [SearchResultType.TAX_CATEGORY]: Icons.Receipt,
+        [SearchResultType.TAX_RATE]: Icons.Calculator,
+        [SearchResultType.COUNTRY]: Icons.Flag,
+        [SearchResultType.ZONE]: Icons.MapPin,
+        [SearchResultType.ROLE]: Icons.Shield,
+        [SearchResultType.CUSTOMER_GROUP]: Icons.Users,
+        [SearchResultType.STOCK_LOCATION]: Icons.Warehouse,
+        [SearchResultType.TAG]: Icons.Hash,
+        [SearchResultType.CUSTOM_ENTITY]: Icons.Box,
+        [SearchResultType.NAVIGATION]: Icons.Navigation,
+        [SearchResultType.SETTINGS]: Icons.Settings,
+        [SearchResultType.QUICK_ACTION]: Icons.Zap,
+        [SearchResultType.DOCUMENTATION]: Icons.BookOpen,
+        [SearchResultType.BLOG_POST]: Icons.FileText,
+        [SearchResultType.PLUGIN]: Icons.Puzzle,
+        [SearchResultType.WEBSITE_CONTENT]: Icons.Globe2,
+    };
+
+    return iconMap[type] || Icons.FileText;
+}
+
+function formatResultType(type: SearchResultType): string {
+    return type
+        .split('_')
+        .map(word => word.charAt(0) + word.slice(1).toLowerCase())
+        .join(' ');
+}

+ 145 - 0
packages/dashboard/src/lib/components/global-search/search-results-list.tsx

@@ -0,0 +1,145 @@
+import { CommandGroup, CommandEmpty } from '@/vdb/components/ui/command.js';
+import { SearchResultItem } from './search-result-item.js';
+import { useSearchContext } from '@/vdb/providers/search-provider.js';
+import { SearchResultType } from '@/vdb/providers/search-provider.js';
+import { useMemo } from 'react';
+
+interface SearchResultsListProps {
+    onResultSelect: () => void;
+}
+
+export function SearchResultsList({ onResultSelect }: SearchResultsListProps) {
+    const { searchResults, isSearching, searchQuery } = useSearchContext();
+
+    // Group results by type
+    const groupedResults = useMemo(() => {
+        const groups = new Map<SearchResultType, typeof searchResults>();
+        
+        searchResults.forEach(result => {
+            if (!groups.has(result.type)) {
+                groups.set(result.type, []);
+            }
+            groups.get(result.type)!.push(result);
+        });
+
+        // Sort groups by priority and results by relevance
+        const sortedGroups = Array.from(groups.entries()).sort(([a], [b]) => {
+            const priority = getTypePriority(a) - getTypePriority(b);
+            return priority;
+        });
+
+        return sortedGroups.map(([type, results]) => [
+            type,
+            results.sort((a, b) => (b.relevanceScore || 0) - (a.relevanceScore || 0))
+        ] as const);
+    }, [searchResults]);
+
+    if (isSearching) {
+        return (
+            <CommandGroup heading="Searching...">
+                <CommandEmpty>Searching...</CommandEmpty>
+            </CommandGroup>
+        );
+    }
+
+    if (!searchQuery || searchQuery.length < 2) {
+        return null;
+    }
+
+    if (searchResults.length === 0) {
+        return <CommandEmpty>No results found.</CommandEmpty>;
+    }
+
+    return (
+        <>
+            {groupedResults.map(([type, results]) => (
+                <CommandGroup 
+                    key={type} 
+                    heading={formatGroupHeading(type, results.length)}
+                >
+                    {results.map(result => (
+                        <SearchResultItem
+                            key={result.id}
+                            result={result}
+                            onSelect={onResultSelect}
+                        />
+                    ))}
+                </CommandGroup>
+            ))}
+        </>
+    );
+}
+
+function getTypePriority(type: SearchResultType): number {
+    // Define priority order for result types
+    const priorities: Record<SearchResultType, number> = {
+        [SearchResultType.QUICK_ACTION]: 0,
+        [SearchResultType.PRODUCT]: 1,
+        [SearchResultType.CUSTOMER]: 2,
+        [SearchResultType.ORDER]: 3,
+        [SearchResultType.COLLECTION]: 4,
+        [SearchResultType.PRODUCT_VARIANT]: 5,
+        [SearchResultType.ASSET]: 6,
+        [SearchResultType.ADMINISTRATOR]: 7,
+        [SearchResultType.NAVIGATION]: 8,
+        [SearchResultType.SETTINGS]: 9,
+        [SearchResultType.FACET]: 10,
+        [SearchResultType.FACET_VALUE]: 11,
+        [SearchResultType.PROMOTION]: 12,
+        [SearchResultType.PAYMENT_METHOD]: 13,
+        [SearchResultType.SHIPPING_METHOD]: 14,
+        [SearchResultType.TAX_CATEGORY]: 15,
+        [SearchResultType.TAX_RATE]: 16,
+        [SearchResultType.COUNTRY]: 17,
+        [SearchResultType.ZONE]: 18,
+        [SearchResultType.ROLE]: 19,
+        [SearchResultType.CUSTOMER_GROUP]: 20,
+        [SearchResultType.STOCK_LOCATION]: 21,
+        [SearchResultType.TAG]: 22,
+        [SearchResultType.CHANNEL]: 23,
+        [SearchResultType.CUSTOM_ENTITY]: 24,
+        [SearchResultType.DOCUMENTATION]: 25,
+        [SearchResultType.BLOG_POST]: 26,
+        [SearchResultType.PLUGIN]: 27,
+        [SearchResultType.WEBSITE_CONTENT]: 28,
+    };
+
+    return priorities[type] ?? 999;
+}
+
+function formatGroupHeading(type: SearchResultType, count: number): string {
+    const typeNames: Record<SearchResultType, string> = {
+        [SearchResultType.PRODUCT]: 'Products',
+        [SearchResultType.PRODUCT_VARIANT]: 'Product Variants',
+        [SearchResultType.CUSTOMER]: 'Customers',
+        [SearchResultType.ORDER]: 'Orders',
+        [SearchResultType.COLLECTION]: 'Collections',
+        [SearchResultType.ADMINISTRATOR]: 'Administrators',
+        [SearchResultType.CHANNEL]: 'Channels',
+        [SearchResultType.ASSET]: 'Assets',
+        [SearchResultType.FACET]: 'Facets',
+        [SearchResultType.FACET_VALUE]: 'Facet Values',
+        [SearchResultType.PROMOTION]: 'Promotions',
+        [SearchResultType.PAYMENT_METHOD]: 'Payment Methods',
+        [SearchResultType.SHIPPING_METHOD]: 'Shipping Methods',
+        [SearchResultType.TAX_CATEGORY]: 'Tax Categories',
+        [SearchResultType.TAX_RATE]: 'Tax Rates',
+        [SearchResultType.COUNTRY]: 'Countries',
+        [SearchResultType.ZONE]: 'Zones',
+        [SearchResultType.ROLE]: 'Roles',
+        [SearchResultType.CUSTOMER_GROUP]: 'Customer Groups',
+        [SearchResultType.STOCK_LOCATION]: 'Stock Locations',
+        [SearchResultType.TAG]: 'Tags',
+        [SearchResultType.CUSTOM_ENTITY]: 'Custom Entities',
+        [SearchResultType.NAVIGATION]: 'Navigation',
+        [SearchResultType.SETTINGS]: 'Settings',
+        [SearchResultType.QUICK_ACTION]: 'Actions',
+        [SearchResultType.DOCUMENTATION]: 'Documentation',
+        [SearchResultType.BLOG_POST]: 'Blog Posts',
+        [SearchResultType.PLUGIN]: 'Plugins',
+        [SearchResultType.WEBSITE_CONTENT]: 'Website Content',
+    };
+
+    const name = typeNames[type] || type;
+    return `${name} (${count})`;
+}

+ 22 - 0
packages/dashboard/src/lib/components/global-search/search-trigger.tsx

@@ -0,0 +1,22 @@
+import { Button } from '@/vdb/components/ui/button.js';
+import { useSearchContext } from '@/vdb/providers/search-provider.js';
+import { Search } from 'lucide-react';
+
+export function SearchTrigger() {
+    const { setIsCommandPaletteOpen } = useSearchContext();
+
+    return (
+        <Button
+            variant="outline"
+            size="sm"
+            className="h-9 px-3 justify-start text-muted-foreground"
+            onClick={() => setIsCommandPaletteOpen(true)}
+        >
+            <Search className="h-4 w-4 mr-2" />
+            <span className="hidden sm:inline-flex">Search...</span>
+            <kbd className="pointer-events-none hidden h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex ml-auto">
+                <span className="text-xs">⌘</span>K
+            </kbd>
+        </Button>
+    );
+}

+ 12 - 0
packages/dashboard/src/lib/components/layout/app-layout.tsx

@@ -2,6 +2,9 @@ import { AppSidebar } from '@/vdb/components/layout/app-sidebar.js';
 import { DevModeIndicator } from '@/vdb/components/layout/dev-mode-indicator.js';
 import { GeneratedBreadcrumbs } from '@/vdb/components/layout/generated-breadcrumbs.js';
 import { PrereleasePopup } from '@/vdb/components/layout/prerelease-popup.js';
+import { CommandPalette } from '@/vdb/components/global-search/command-palette.js';
+import { SearchTrigger } from '@/vdb/components/global-search/search-trigger.js';
+import { useKeyboardShortcuts } from '@/vdb/components/global-search/hooks/use-keyboard-shortcuts.js';
 import { Separator } from '@/vdb/components/ui/separator.js';
 import { SidebarInset, SidebarProvider, SidebarTrigger } from '@/vdb/components/ui/sidebar.js';
 import { useUserSettings } from '@/vdb/hooks/use-user-settings.js';
@@ -10,6 +13,8 @@ import { Alerts } from '../shared/alerts.js';
 
 export function AppLayout() {
     const { settings } = useUserSettings();
+    const { isCommandPaletteOpen, setIsCommandPaletteOpen } = useKeyboardShortcuts();
+    
     return (
         <SidebarProvider>
             <AppSidebar />
@@ -23,6 +28,7 @@ export function AppLayout() {
                                 <GeneratedBreadcrumbs />
                             </div>
                             <div className="flex items-center justify-end gap-2">
+                                <SearchTrigger />
                                 {settings.devMode && <DevModeIndicator />}
                                 <Alerts />
                             </div>
@@ -32,6 +38,12 @@ export function AppLayout() {
                 </div>
             </SidebarInset>
             <PrereleasePopup />
+            
+            {/* Global Command Palette */}
+            <CommandPalette 
+                isOpen={isCommandPaletteOpen}
+                onOpenChange={setIsCommandPaletteOpen}
+            />
         </SidebarProvider>
     );
 }

+ 84 - 0
packages/dashboard/src/lib/graphql/global-search.ts

@@ -0,0 +1,84 @@
+import { gql } from 'graphql-tag';
+
+export const GLOBAL_SEARCH_QUERY = gql`
+    query GlobalSearch($input: GlobalSearchInput!) {
+        globalSearch(input: $input) {
+            id
+            type
+            title
+            subtitle
+            description
+            url
+            thumbnailUrl
+            metadata
+            relevanceScore
+            lastModified
+        }
+    }
+`;
+
+export const GLOBAL_SEARCH_INPUT_FRAGMENT = gql`
+    input GlobalSearchInput {
+        query: String!
+        types: [SearchResultType!]
+        limit: Int = 20
+        skip: Int = 0
+        channelId: String
+    }
+`;
+
+export const SEARCH_RESULT_TYPE_ENUM = gql`
+    enum SearchResultType {
+        # Core Entities
+        PRODUCT
+        PRODUCT_VARIANT
+        CUSTOMER
+        ORDER
+        COLLECTION
+        ADMINISTRATOR
+        CHANNEL
+        ASSET
+        FACET
+        FACET_VALUE
+        PROMOTION
+        PAYMENT_METHOD
+        SHIPPING_METHOD
+        TAX_CATEGORY
+        TAX_RATE
+        COUNTRY
+        ZONE
+        ROLE
+        CUSTOMER_GROUP
+        STOCK_LOCATION
+        TAG
+
+        # Custom/Plugin Entities
+        CUSTOM_ENTITY
+
+        # Dashboard Content
+        NAVIGATION
+        SETTINGS
+        QUICK_ACTION
+
+        # External Content
+        DOCUMENTATION
+        BLOG_POST
+        PLUGIN
+        WEBSITE_CONTENT
+    }
+`;
+
+export const GLOBAL_SEARCH_RESULT_FRAGMENT = gql`
+    fragment GlobalSearchResult on GlobalSearchResult {
+        id
+        type
+        title
+        subtitle
+        description
+        url
+        thumbnailUrl
+        metadata
+        relevanceScore
+        lastModified
+    }
+`;

+ 17 - 0
packages/dashboard/src/lib/hooks/use-debounce.ts

@@ -0,0 +1,17 @@
+import { useEffect, useState } from 'react';
+
+export function useDebounce<T>(value: T, delay: number): T {
+    const [debouncedValue, setDebouncedValue] = useState<T>(value);
+
+    useEffect(() => {
+        const handler = setTimeout(() => {
+            setDebouncedValue(value);
+        }, delay);
+
+        return () => {
+            clearTimeout(handler);
+        };
+    }, [value, delay]);
+
+    return debouncedValue;
+}

+ 195 - 0
packages/dashboard/src/lib/providers/search-provider.tsx

@@ -0,0 +1,195 @@
+import { createContext, useContext, useState, ReactNode, useCallback } from 'react';
+
+export interface SearchResult {
+    id: string;
+    type: SearchResultType;
+    title: string;
+    subtitle?: string;
+    description?: string;
+    url: string;
+    thumbnailUrl?: string;
+    metadata?: Record<string, any>;
+    relevanceScore?: number;
+    lastModified?: string;
+}
+
+export enum SearchResultType {
+    // Core Entities
+    PRODUCT = 'PRODUCT',
+    PRODUCT_VARIANT = 'PRODUCT_VARIANT',
+    CUSTOMER = 'CUSTOMER',
+    ORDER = 'ORDER',
+    COLLECTION = 'COLLECTION',
+    ADMINISTRATOR = 'ADMINISTRATOR',
+    CHANNEL = 'CHANNEL',
+    ASSET = 'ASSET',
+    FACET = 'FACET',
+    FACET_VALUE = 'FACET_VALUE',
+    PROMOTION = 'PROMOTION',
+    PAYMENT_METHOD = 'PAYMENT_METHOD',
+    SHIPPING_METHOD = 'SHIPPING_METHOD',
+    TAX_CATEGORY = 'TAX_CATEGORY',
+    TAX_RATE = 'TAX_RATE',
+    COUNTRY = 'COUNTRY',
+    ZONE = 'ZONE',
+    ROLE = 'ROLE',
+    CUSTOMER_GROUP = 'CUSTOMER_GROUP',
+    STOCK_LOCATION = 'STOCK_LOCATION',
+    TAG = 'TAG',
+
+    // Custom/Plugin Entities
+    CUSTOM_ENTITY = 'CUSTOM_ENTITY',
+
+    // Dashboard Content
+    NAVIGATION = 'NAVIGATION',
+    SETTINGS = 'SETTINGS',
+    QUICK_ACTION = 'QUICK_ACTION',
+
+    // External Content
+    DOCUMENTATION = 'DOCUMENTATION',
+    BLOG_POST = 'BLOG_POST',
+    PLUGIN = 'PLUGIN',
+    WEBSITE_CONTENT = 'WEBSITE_CONTENT',
+}
+
+export interface QuickAction {
+    id: string;
+    label: string;
+    description?: string;
+    icon?: string;
+    shortcut?: string;
+    isContextAware: boolean;
+    requiredPermissions?: string[];
+    handler: QuickActionHandler;
+    params?: Record<string, any>;
+}
+
+export type QuickActionHandler = (context: QuickActionContext) => void | Promise<void>;
+
+export interface QuickActionContext {
+    currentRoute: string;
+    currentEntityType?: string;
+    currentEntityId?: string;
+    navigate: (path: string) => void;
+    showNotification: (message: string, type?: 'success' | 'error' | 'warning') => void;
+    confirm: (message: string) => Promise<boolean>;
+    executeGraphQL: (query: string, variables?: any) => Promise<any>;
+}
+
+interface SearchContextType {
+    // Command palette state
+    isCommandPaletteOpen: boolean;
+    setIsCommandPaletteOpen: (open: boolean) => void;
+    
+    // Search state
+    searchQuery: string;
+    setSearchQuery: (query: string) => void;
+    searchResults: SearchResult[];
+    setSearchResults: (results: SearchResult[]) => void;
+    isSearching: boolean;
+    setIsSearching: (searching: boolean) => void;
+    
+    // Search filters
+    selectedTypes: SearchResultType[];
+    setSelectedTypes: (types: SearchResultType[]) => void;
+    
+    // Quick actions
+    quickActions: QuickAction[];
+    registerQuickAction: (action: QuickAction) => void;
+    unregisterQuickAction: (actionId: string) => void;
+    
+    // Recent searches
+    recentSearches: string[];
+    addRecentSearch: (query: string) => void;
+    clearRecentSearches: () => void;
+}
+
+const SearchContext = createContext<SearchContextType | undefined>(undefined);
+
+export const SearchProvider = ({ children }: { children: ReactNode }) => {
+    // Command palette state
+    const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
+    
+    // Search state
+    const [searchQuery, setSearchQuery] = useState('');
+    const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
+    const [isSearching, setIsSearching] = useState(false);
+    const [selectedTypes, setSelectedTypes] = useState<SearchResultType[]>([]);
+    
+    // Quick actions
+    const [quickActions, setQuickActions] = useState<QuickAction[]>([]);
+    
+    // Recent searches (stored in localStorage)
+    const [recentSearches, setRecentSearches] = useState<string[]>(() => {
+        try {
+            const stored = localStorage.getItem('vendure-search-history');
+            return stored ? JSON.parse(stored) : [];
+        } catch {
+            return [];
+        }
+    });
+
+    const registerQuickAction = useCallback((action: QuickAction) => {
+        setQuickActions(prev => {
+            const existing = prev.find(a => a.id === action.id);
+            if (existing) {
+                // Replace existing action
+                return prev.map(a => a.id === action.id ? action : a);
+            }
+            return [...prev, action];
+        });
+    }, []);
+
+    const unregisterQuickAction = useCallback((actionId: string) => {
+        setQuickActions(prev => prev.filter(a => a.id !== actionId));
+    }, []);
+
+    const addRecentSearch = useCallback((query: string) => {
+        if (!query.trim() || query.length < 2) return;
+        
+        setRecentSearches(prev => {
+            const filtered = prev.filter(q => q !== query);
+            const updated = [query, ...filtered].slice(0, 10); // Keep last 10 searches
+            localStorage.setItem('vendure-search-history', JSON.stringify(updated));
+            return updated;
+        });
+    }, []);
+
+    const clearRecentSearches = useCallback(() => {
+        setRecentSearches([]);
+        localStorage.removeItem('vendure-search-history');
+    }, []);
+
+    return (
+        <SearchContext.Provider 
+            value={{
+                isCommandPaletteOpen,
+                setIsCommandPaletteOpen,
+                searchQuery,
+                setSearchQuery,
+                searchResults,
+                setSearchResults,
+                isSearching,
+                setIsSearching,
+                selectedTypes,
+                setSelectedTypes,
+                quickActions,
+                registerQuickAction,
+                unregisterQuickAction,
+                recentSearches,
+                addRecentSearch,
+                clearRecentSearches,
+            }}
+        >
+            {children}
+        </SearchContext.Provider>
+    );
+};
+
+export const useSearchContext = () => {
+    const context = useContext(SearchContext);
+    if (!context) {
+        throw new Error('useSearchContext must be used within SearchProvider');
+    }
+    return context;
+};