import type { ComponentType, ReactNode } from "react"; /** Icon component signature AppMark renders with size+color — matches lucide-react's icon shape exactly. */ export type AppIconComponent = ComponentType<{ size?: number | string; color?: string; }>; /** A lucide-style component, a ready node, or nothing — then the mark falls back to initials. */ export type AppIcon = AppIconComponent | ReactNode; /** One app in the switcher's grid; missing/duplicate `id` or a missing `url` (without `locked`) warn once in dev only. */ export interface AppOption { /** Stable identity: matches `currentAppId`, keys the row, and seeds the mark's auto-assigned colour. */ id: string; /** Row label; also searched, matched by type-ahead, and the source of the initials when there is no icon. */ name: string; /** Destination opened in a new tab (noopener); can be omitted only when `locked`. */ url: string; /** Second line under the name, truncated to one line and included in search. */ description?: string; /** Mark glyph: a lucide-style component or a ready node; falls back to initials. Ignored when `iconUrl` is set. */ icon?: AppIcon; /** Image URL drawn as the mark instead of the tinted glyph tile; takes precedence over `icon` and `color`. */ iconUrl?: string; /** Mark hue; when omitted a distinct hue is auto-assigned from the chart palette. */ color?: string; /** Small info-coloured text beside the name (e.g. "New"); hidden on the current app. */ badge?: string | number; /** Listed but not openable — renders locked with `lockedReason` as its title. */ locked?: boolean; /** Tooltip for a locked row; defaults to " is not available on your plan". */ lockedReason?: string; /** Extra terms the search should match, e.g. "helpdesk" for Tickets. */ keywords?: string[]; } /** Fixed top-bar control: two columns, md density, bottom-end anchor, "Your apps", new tab. */ export interface AppSwitcherProps { /** Apps to list, in display order, laid out in two columns. */ apps: AppOption[]; /** id of the app this UI is running inside. Its row is marked and never navigates. */ currentAppId?: string; /** Called instead of following the link. Call `event.preventDefault()` to keep the panel's own navigation. */ onSelect?: (app: AppOption, event: React.MouseEvent | React.KeyboardEvent) => void; /** Fires with the new open state whenever the panel opens or closes (trigger, shortcut, selection, Escape, outside click). */ onOpenChange?: (isOpen: boolean) => void; /** The only thing that shows the search box — searchThreshold below is currently inert (kept for API compat). */ isSearchable?: boolean; /** Currently inert: the app count no longer opens search; use `isSearchable`. Kept for API compatibility. */ searchThreshold?: number; /** Keyboard shortcut that opens the panel, e.g. "mod+shift+a". `null` disables it. */ shortcut?: string | null; /** Shows the `shortcut` chord as a key hint in the panel header; default true, no effect without `shortcut`. */ showShortcutHint?: boolean; /** Renders nothing when there is no other app to switch to. */ hideWhenAlone?: boolean; /** Text shown when a search matches no app; default "No app matches that name." */ emptyMessage?: string; /** Content rendered in a bordered strip below the app grid. */ footer?: ReactNode; /** Accessible name of the trigger button; default "Switch app". */ ariaLabel?: string; /** CSS width of the panel; default "28rem", capped to the viewport width. */ panelWidth?: string; /** CSS max-height of the scrollable app grid; default "24rem". */ maxHeight?: string; /** Shows four skeleton rows instead of the apps and the empty message; default false. */ isLoading?: boolean; } /** Up to two initials, so a mark stays legible without an icon. */ export declare const appInitials: (name: string) => string; export declare const filterApps: (apps: AppOption[], query: string) => AppOption[]; /** Minimal keyboard-event shape `matchesShortcut` needs, so callers can test a shortcut without a real KeyboardEvent. */ export type ShortcutEvent = Pick; /** "mod" is Cmd on Apple platforms and Ctrl everywhere else. */ export declare const matchesShortcut: (event: ShortcutEvent, shortcut: string, isApple?: boolean) => boolean; /** One label per key in a chord, so a named key such as `enter` stays a single cap. */ export declare const shortcutKeys: (shortcut: string, isApple?: boolean) => string[]; export declare const formatShortcut: (shortcut: string, isApple?: boolean) => string; /** Input to `buildSuiteApps`: an `AppOption` keyed by its subdomain label, with `url` optional since it can be derived. */ export interface SuiteAppSource extends Omit { /** Hostname label substituted for `baseSubdomain` to derive this app's origin. */ subdomain: string; /** Explicit origin or URL for this app; wins over derivation and is trimmed. */ url?: string; } /** The consumer's own absolute origin, plus which hostname label identifies it — `buildSuiteApps` swaps that label per app. */ export interface SuiteUrlOptions { /** Absolute URL of the host the consumer is served on; throws if it is not absolute. */ baseUrl: string; /** Label in `baseUrl`'s hostname that identifies this app (e.g. "crm") and is swapped per app. */ baseSubdomain: string; /** Opt-in for hosts with no `baseSubdomain` label (plain `localhost`): other apps derive from this labelled URL instead of throwing, and the app owning `baseSubdomain` keeps `baseUrl`. */ devFallbackBaseUrl?: string; } /** * Derives every app's origin from the host the consumer is already served on, so one deployment * variable keeps dev, test and prod in step. An app may still carry its own `url`, which wins. */ export declare const buildSuiteApps: (apps: SuiteAppSource[], { baseUrl, baseSubdomain, devFallbackBaseUrl }: SuiteUrlOptions) => AppOption[];