or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

index.mdmulti-select.mdomnibar.mdquery-list.mdselect.mdsuggest.mdutilities.md
tile.json

omnibar.mddocs/

Spotlight Search

The Omnibar component provides a macOS Spotlight-style search overlay that appears over the entire application.

Capabilities

Omnibar Component

React component providing a full-screen search overlay similar to macOS Spotlight.

/**
 * Omnibar component for full-screen search overlay
 * @template T - Type of items in the search results
 */
class Omnibar<T> extends React.PureComponent<OmnibarProps<T>> {
  /** Generic factory method for type inference */
  static ofType<U>(): new (props: OmnibarProps<U>) => Omnibar<U>;
}

interface OmnibarProps<T> extends ListItemsProps<T> {
  /** Whether the omnibar overlay is open */
  isOpen: boolean;
  /** Callback when omnibar should be closed */
  onClose?: (event?: React.SyntheticEvent<HTMLElement>) => void;
  /** Props for the search input */
  inputProps?: InputGroupProps;
  /** Props for the overlay container */
  overlayProps?: Partial<OverlayProps>;
}

Usage Examples:

import React, { useState } from "react";
import { Omnibar, ItemRenderer } from "@blueprintjs/select";
import { MenuItem, Button } from "@blueprintjs/core";

interface SearchItem {
  title: string;
  description: string;
  category: string;
  action: () => void;
}

const searchItems: SearchItem[] = [
  {
    title: "Create New Project",
    description: "Start a new project from scratch",
    category: "Actions",
    action: () => console.log("Creating new project..."),
  },
  {
    title: "Open Settings",
    description: "Configure application preferences",
    category: "Navigation",
    action: () => console.log("Opening settings..."),
  },
  {
    title: "View Documentation",
    description: "Browse help articles and guides",
    category: "Help",
    action: () => console.log("Opening documentation..."),
  },
  {
    title: "Export Data",
    description: "Download your data in various formats",
    category: "Actions",
    action: () => console.log("Exporting data..."),
  },
];

const renderSearchItem: ItemRenderer<SearchItem> = (item, { handleClick, modifiers, query }) => (
  <MenuItem
    key={item.title}
    text={item.title}
    label={item.category}
    onClick={handleClick}
    active={modifiers.active}
    disabled={modifiers.disabled}
    shouldDismissPopover={false}
  >
    <div style={{ fontSize: "12px", color: "#666", marginTop: "4px" }}>
      {item.description}
    </div>
  </MenuItem>
);

const AppOmnibar = () => {
  const [isOpen, setIsOpen] = useState(false);

  const handleItemSelect = (item: SearchItem) => {
    item.action();
    setIsOpen(false);
  };

  const searchPredicate = (query: string, item: SearchItem) => {
    const searchText = `${item.title} ${item.description} ${item.category}`.toLowerCase();
    return searchText.includes(query.toLowerCase());
  };

  return (
    <>
      <Button
        text="Open Search (Cmd+K)"
        onClick={() => setIsOpen(true)}
        icon="search"
      />
      
      <Omnibar<SearchItem>
        isOpen={isOpen}
        items={searchItems}
        itemRenderer={renderSearchItem}
        itemPredicate={searchPredicate}
        onItemSelect={handleItemSelect}
        onClose={() => setIsOpen(false)}
        noResults={<MenuItem disabled text="No results found." />}
        inputProps={{
          placeholder: "Search actions, navigate, or get help...",
          leftIcon: "search",
        }}
      />
    </>
  );
};

// Global keyboard shortcut integration
const GlobalOmnibar = () => {
  const [isOpen, setIsOpen] = useState(false);

  React.useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "k") {
        e.preventDefault();
        setIsOpen(true);
      }
      if (e.key === "Escape" && isOpen) {
        setIsOpen(false);
      }
    };

    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [isOpen]);

  return (
    <Omnibar<SearchItem>
      isOpen={isOpen}
      items={searchItems}
      itemRenderer={renderSearchItem}
      onItemSelect={(item) => {
        item.action();
        setIsOpen(false);
      }}
      onClose={() => setIsOpen(false)}
      inputProps={{
        placeholder: "Search everything... (Esc to close)",
        autoFocus: true,
      }}
    />
  );
};

Overlay Customization

Omnibar supports extensive overlay customization through Blueprint's Overlay component props.

// Subset of OverlayProps available to Omnibar
interface OverlayProps {
  /** Additional CSS classes for the overlay */
  className?: string;
  /** Whether overlay should focus trap */
  enforceFocus?: boolean;
  /** Whether to restore focus on close */
  shouldReturnFocusOnClose?: boolean;
  /** Whether clicking outside should close */
  canOutsideClickClose?: boolean;
  /** Whether pressing Escape should close */
  canEscapeKeyClose?: boolean;
  /** Callback when overlay attempts to close */
  onClose?: (event?: React.SyntheticEvent<HTMLElement>) => void;
  /** Custom backdrop props */
  backdropProps?: React.HTMLProps<HTMLDivElement>;
  /** Whether to use portal for rendering */
  usePortal?: boolean;
  /** Portal container element */
  portalContainer?: HTMLElement;
}

Usage Examples:

import { Omnibar } from "@blueprintjs/select";

const CustomOverlayOmnibar = () => (
  <Omnibar<SearchItem>
    isOpen={true}
    items={searchItems}
    itemRenderer={renderSearchItem}
    onItemSelect={(item) => item.action()}
    onClose={() => console.log("Closing omnibar")}
    overlayProps={{
      className: "custom-omnibar-overlay",
      enforceFocus: true,
      shouldReturnFocusOnClose: true,
      canOutsideClickClose: true,
      canEscapeKeyClose: true,
      backdropProps: {
        style: { backgroundColor: "rgba(0, 0, 0, 0.8)" },
      },
      usePortal: true,
    }}
    inputProps={{
      placeholder: "Custom overlay omnibar...",
      large: true,
    }}
  />
);

Advanced Search Features

Omnibar can implement sophisticated search behaviors like fuzzy matching, categorization, and recent items.

Usage Examples:

interface CommandItem {
  id: string;
  title: string;
  description?: string;
  category: string;
  keywords: string[];
  icon?: string;
  action: () => void;
  lastUsed?: Date;
}

const commands: CommandItem[] = [
  {
    id: "new-file",
    title: "New File",
    description: "Create a new file in the current directory",
    category: "File",
    keywords: ["create", "file", "new", "document"],
    icon: "document",
    action: () => console.log("Creating new file"),
  },
  // ... more commands
];

const FuzzySearchOmnibar = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [recentCommands, setRecentCommands] = useState<string[]>([]);

  // Fuzzy search with scoring
  const fuzzySearch = (query: string, commands: CommandItem[]): CommandItem[] => {
    if (!query) {
      // Show recent items first when no query
      const recentItems = commands.filter(cmd => recentCommands.includes(cmd.id));
      const otherItems = commands.filter(cmd => !recentCommands.includes(cmd.id));
      return [...recentItems, ...otherItems];
    }

    const scored = commands.map(command => {
      let score = 0;
      const q = query.toLowerCase();
      
      // Title matching
      if (command.title.toLowerCase().includes(q)) {
        score += command.title.toLowerCase().startsWith(q) ? 100 : 50;
      }
      
      // Keywords matching
      const keywordMatch = command.keywords.some(keyword => 
        keyword.toLowerCase().includes(q)
      );
      if (keywordMatch) score += 30;
      
      // Description matching
      if (command.description?.toLowerCase().includes(q)) score += 20;
      
      // Recent usage bonus
      if (recentCommands.includes(command.id)) score += 10;
      
      return { command, score };
    })
    .filter(({ score }) => score > 0)
    .sort((a, b) => b.score - a.score)
    .map(({ command }) => command);

    return scored;
  };

  const handleItemSelect = (command: CommandItem) => {
    command.action();
    
    // Update recent commands
    const updated = [command.id, ...recentCommands.filter(id => id !== command.id)].slice(0, 5);
    setRecentCommands(updated);
    
    setIsOpen(false);
  };

  const renderCommand: ItemRenderer<CommandItem> = (command, { handleClick, modifiers, query }) => {
    const isRecent = recentCommands.includes(command.id);
    
    return (
      <MenuItem
        key={command.id}
        icon={command.icon}
        text={
          <div>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
              <span>{command.title}</span>
              <span style={{ fontSize: "11px", color: "#666" }}>
                {command.category} {isRecent && "• Recent"}
              </span>
            </div>
            {command.description && (
              <div style={{ fontSize: "12px", color: "#666", marginTop: "2px" }}>
                {command.description}
              </div>
            )}
          </div>
        }
        onClick={handleClick}
        active={modifiers.active}
        shouldDismissPopover={false}
      />
    );
  };

  return (
    <Omnibar<CommandItem>
      isOpen={isOpen}
      items={commands}
      itemRenderer={renderCommand}
      itemListPredicate={fuzzySearch}
      onItemSelect={handleItemSelect}
      onClose={() => setIsOpen(false)}
      resetOnClose
      inputProps={{
        placeholder: "Type a command or search...",
        leftIcon: "command",
        autoFocus: true,
      }}
    />
  );
};

Integration Patterns

Common patterns for integrating Omnibar into applications.

Usage Examples:

// With React Router navigation
import { useNavigate } from "react-router-dom";

const NaviagtionOmnibar = () => {
  const navigate = useNavigate();
  const [isOpen, setIsOpen] = useState(false);

  const navigationItems = [
    {
      title: "Dashboard",
      path: "/dashboard",
      description: "View your main dashboard",
    },
    {
      title: "Profile",
      path: "/profile",
      description: "Manage your account settings",
    },
    {
      title: "Projects",
      path: "/projects",
      description: "Browse all your projects",
    },
  ];

  return (
    <Omnibar
      isOpen={isOpen}
      items={navigationItems}
      itemRenderer={(item, { handleClick, modifiers }) => (
        <MenuItem
          text={item.title}
          onClick={handleClick}
          active={modifiers.active}
        >
          <div style={{ fontSize: "12px", color: "#666" }}>
            {item.description}
          </div>
        </MenuItem>
      )}
      onItemSelect={(item) => {
        navigate(item.path);
        setIsOpen(false);
      }}
      onClose={() => setIsOpen(false)}
    />
  );
};

// With async data loading
const AsyncDataOmnibar = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [loading, setLoading] = useState(false);
  const [searchResults, setSearchResults] = useState<SearchItem[]>([]);

  const performSearch = async (query: string) => {
    if (!query) {
      setSearchResults([]);
      return;
    }

    setLoading(true);
    try {
      // Simulate API call
      const results = await fetch(`/api/search?q=${encodeURIComponent(query)}`);
      const data = await results.json();
      setSearchResults(data);
    } catch (error) {
      console.error("Search failed:", error);
      setSearchResults([]);
    } finally {
      setLoading(false);
    }
  };

  // Debounced search
  React.useEffect(() => {
    const timeoutId = setTimeout(() => performSearch(query), 300);
    return () => clearTimeout(timeoutId);
  }, [query]);

  return (
    <Omnibar<SearchItem>
      isOpen={isOpen}
      items={searchResults}
      itemRenderer={renderSearchItem}
      onItemSelect={(item) => {
        item.action();
        setIsOpen(false);
      }}
      onClose={() => setIsOpen(false)}
      onQueryChange={(query) => setQuery(query)}
      noResults={
        loading ? (
          <MenuItem disabled text="Searching..." icon="search" />
        ) : (
          <MenuItem disabled text="No results found." />
        )
      }
    />
  );
};