UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

241 lines (240 loc) 8.19 kB
import type { LucideIcon } from 'lucide-react'; import type { AnchorHTMLAttributes, ReactNode } from 'react'; export interface AppDefinition { id: string; label: string; /** * Either a Lucide-style icon component or a rendered React node such as a * custom brand SVG. */ icon: LucideIcon | ReactNode; /** Optional route target for consumers that render app tabs as links. */ to?: string; } export interface AppTabRenderLinkArgs { app: AppDefinition; isActive: boolean; className: string; children: ReactNode; linkProps: AnchorHTMLAttributes<HTMLAnchorElement>; } export interface AppShellAppTabsConfig { renderLink?: (args: AppTabRenderLinkArgs) => ReactNode; } export interface WorkspaceDefinition { id: string; name: string; icon: ReactNode; } export interface ContextModule { id: string; label: string; description?: string; items?: ContextItem[]; render?: (ctx: ShellRenderContext) => ReactNode; } export interface ContextItem { id: string; label: string; icon: LucideIcon; to: string; description?: string; shortcut?: string; } export interface PanelView { id: string; label: string; icon: LucideIcon; render: (ctx: ShellRenderContext) => ReactNode; } export type PanelMode = 'closed' | 'panel' | 'expanded' | 'fullscreen'; export type ShellMainLayout = 'workspace' | 'centered' | 'fullbleed'; export type ShellViewport = 'mobile' | 'tablet' | 'desktop' | 'wide' | 'ultrawide'; export type ContextRailHeader = 'auto' | 'visible' | 'hidden'; export type ContextRailScroll = 'auto' | 'none'; export interface MobileBottomNavItem { id: string; label: string | (() => string); icon: LucideIcon; opens: 'menu-drawer' | 'module-drawer' | 'panels-drawer' | 'ai-drawer' | ((close: () => void) => ReactNode); } /** A pinned slot in the floating dock — a route link or a shell action. */ export interface MobileDockItem { id: string; label: string | (() => string); icon: LucideIcon | ReactNode; /** Route target — rendered as a nav link via `renderLink`. */ to?: string; /** Show an unread/notification dot on the slot. */ badge?: boolean; /** Perform a shell action instead of navigating. */ action?: 'open-sheet' | 'open-ai' | 'open-command-palette'; /** `brand` renders the standalone emphasized circle (e.g. Pilot). */ emphasis?: 'brand'; } /** A preset view under a submodule (e.g. Deals → Won) — expands inline. */ export interface MobileNavView { id: string; label: string; to: string; count?: string | number; } /** A submodule row in the workspace sheet (e.g. CRM → Deals). */ export interface MobileNavItem { id: string; label: string; icon?: LucideIcon | ReactNode; to: string; badge?: string | number; /** Preset views that expand inline under this row (accordion). */ views?: MobileNavView[]; } /** A module group in the workspace sheet (e.g. CRM). */ export interface MobileNavGroup { id: string; label: string; icon?: LucideIcon | ReactNode; items: MobileNavItem[]; } /** The full nav tree shown in the workspace sheet. */ export interface MobileNavTree { groups: MobileNavGroup[]; /** Rows below the groups (e.g. Settings). */ footerItems?: MobileNavItem[]; } export interface MobileNavLinkArgs { to: string; isActive: boolean; className: string; children: ReactNode; onNavigate: () => void; linkProps: AnchorHTMLAttributes<HTMLAnchorElement>; } /** Opt-in mobile dock + workspace sheet configuration. */ export interface AppShellMobileNavConfig { dock: MobileDockItem[]; tree: MobileNavTree; /** The `to` of the currently-active row, for highlighting. */ activeTo?: string; /** Render dock/sheet targets as router links (else plain `<a>`). */ renderLink?: (args: MobileNavLinkArgs) => ReactNode; /** * Dock presentation: * - `pill` (default) — a floating, centered pill of icon-only slots with any * `emphasis: 'brand'` item as a standalone circle beside it. * - `bar` — a flat, full-width bottom bar of evenly-spread slots, each with a * larger icon + label and a tinted active state (mirrors the native app). */ variant?: 'pill' | 'bar'; } export interface ShellRenderContext { workspaceId: string; appId: string; } export interface ShellLinkRenderArgs { item: ContextItem; isActive: boolean; className: string; children: ReactNode; linkProps: AnchorHTMLAttributes<HTMLAnchorElement>; } export interface CommandDefinition { id: string; label: string; icon?: LucideIcon; group: string; shortcut?: string; /** Display-only alias for `shortcut`; `shortcut` wins when both are supplied. */ hotkey?: string; run: () => void | Promise<void>; } export interface ThemeAdapter { theme: 'light' | 'dark' | 'system'; setTheme: (t: 'light' | 'dark' | 'system') => void; resolvedTheme: 'light' | 'dark'; } /** * AppShell renders one of three shell modes: * - `auth` — split-screen marketing + form (signed-out) * - `workspace` — header + rails + main + panel (signed-in work surface; * collapses to mobile header + bottom nav + drawers on mobile) * - `chat` — transcript-dominant layout (conversational AI) */ export type AppShellVariant = 'auth' | 'workspace' | 'chat'; /** IconRail configuration for `<AppShell variant="workspace">`. */ export interface AppShellIconRailConfig { mainItems: import('./icon-rail.js').IconRailItem[]; utilityItems?: import('./icon-rail.js').IconRailItem[]; footer?: ReactNode; activeId?: string; renderLink?: import('./icon-rail.js').IconRailProps['renderLink']; labelVisibility?: import('./icon-rail.js').IconRailProps['labelVisibility']; } /** ContextRail configuration for `<AppShell variant="workspace">`. */ export interface AppShellContextRailConfig { appId: string; module: ContextModule; activeItemId?: string; renderLink?: import('./context-rail.js').ContextRailProps['renderLink']; header?: ContextRailHeader; scroll?: ContextRailScroll; } /** RightPanel configuration for `<AppShell variant="workspace">`. */ export interface AppShellRightPanelConfig { panelViews: PanelView[]; defaultView?: string; } /** * A single configurable item in the WorkspaceSwitcher dropdown. * Either `href` (renders as a link) or `onClick` (renders as a button) MUST * be provided; supplying both makes the item a button that fires `onClick` * AND navigates to `href` afterwards. */ interface WorkspaceMenuItemBase { id: string; label: ReactNode; icon?: LucideIcon | ReactNode; /** Insert a `<DropdownMenuSeparator />` immediately after this item. */ separatorAfter?: boolean; /** Visual intent — `destructive` paints with the destructive token. */ variant?: 'default' | 'destructive'; } export type WorkspaceMenuItem = WorkspaceMenuItemBase & ({ href: string; onClick?: () => void; } | { onClick: () => void; href?: string; }); /** * Workspace dropdown configuration for `<AppShell variant="workspace">`. * * When `menuItems` is provided, it REPLACES the package-default menu * ("Manage workspaces", "Settings", "Profile", "Sign out"). The theme toggle * is still inserted automatically by meda — between the items and the footer * — so consumers don't have to reimplement theme cycling. * * When `menuItems` is omitted, the package defaults render unchanged (the * existing 1.x behavior). This is fully additive and non-breaking. */ export interface AppShellWorkspaceConfig { menuItems?: WorkspaceMenuItem[]; /** Extra slot rendered after all items and the theme toggle. */ menuFooter?: ReactNode; } export interface AppShellAuthConfig { title: ReactNode; description?: ReactNode; brandName?: ReactNode; brandMark?: ReactNode; eyebrow?: ReactNode; preview?: ReactNode; actions?: ReactNode; } export interface AppShellAuthBranding { brandName?: ReactNode; brandMark?: ReactNode; appName?: ReactNode; tagline?: ReactNode; } export {};