UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

152 lines (108 loc) 9.32 kB
--- name: shell description: Use when scaffolding or modifying shell regions (icon rail, context rail, header, panel, command palette) in any app consuming `@medalsocial/meda` — Picasso, pilot-talk, NextMedal, apps/web. Required reading before adding a rail item, picking an AppShell variant (auth/workspace/chat), wiring command palette actions, configuring `MedaShellProvider`, or any `IconRail` change. --- # Meda Shell ## When to load this skill - Scaffolding a new shell — picking `AppShell` variant and configuring `MedaShellProvider`. - Adding/removing/reordering icon rail items. - Registering command palette actions. - Wiring the workspace switcher or theme toggle in the header. - Building or modifying the right panel. - Touching anything under `src/shell/` in `@medalsocial/meda`. ## App shell variants The package exports a single `<AppShell>` component with a discriminated `variant` prop. Pick exactly one variant per route group: | Variant | When to use | Key config | |---|---|---| | `'auth'` | Sign-in / sign-up / password reset / OAuth callbacks. Lets the form scroll past viewport (dense forms, high zoom). | `auth`, `branding`, optional `preview` (right-side art) + `actions` (top-right) | | `'workspace'` | Logged-in product shell. Has icon rail + context rail + header + main + optional right panel. | `iconRail`, `contextRail`, `rightPanel`, `workspace` (menu items override), `appTabs` (router integration), `headerCenter`, `banners`, `mainLayout`, `globalActions` | | `'chat'` | Chat-first surfaces (full-bleed messaging UI; no rails). | `globalActions` | `AppShellWorkspace.workspace.menuItems` REPLACES the default workspace dropdown ("Manage workspaces / Settings / Profile / Sign out") when provided. **The theme toggle is preserved automatically** — consumers do not have to re-implement theme cycling. ## MedaShellProvider — the runtime root Wrap your app once with `<MedaShellProvider>` (typically in the root layout). Props: ```ts interface MedaShellProviderProps { workspace: WorkspaceDefinition; // required workspaces?: WorkspaceDefinition[]; // for the switcher apps: AppDefinition[]; // required, non-empty (throws if empty) defaultActiveApp?: string; // app id; defaults to apps[0] storage?: ShellStorageAdapter; // defaults to localStorage adapter mobileBottomNav?: MobileBottomNavItem[]; // mobile nav config commandPaletteHotkey?: string; // e.g. 'mod+k' (default) themeAdapter?: 'default' | 'next-themes' | ThemeAdapter; // pick per-framework children: ReactNode; } ``` - **`themeAdapter: 'next-themes'`** for Next.js apps using `next-themes`. The adapter is lazy-loaded so default-adapter consumers don't pay for the bridge. - **`themeAdapter: 'default'`** uses the built-in adapter (no external dep). - Pass a custom `ThemeAdapter` object to integrate with any other theme system. - `apps` MUST have at least one entry — the provider throws on empty array. ## Regions and component map | Region | Component | Source | |---|---|---| | Icon rail | `IconRail` | `src/shell/icon-rail.tsx` | | Context rail | `ContextRail` | `src/shell/context-rail.tsx` | | Header | `ShellHeader` + `WorkspaceSwitcher` | `src/shell/shell-header.tsx` | | Main | `ShellMain` | `src/shell/shell-main.tsx` | | Right panel | `RightPanel` | `src/shell/right-panel.tsx` | | Command palette | `CommandPalette` + `CommandRegistryContext` | `src/shell/command-palette.tsx` | ## The flat-rail rule (critical) **The `IconRail` is a flat list of equal-weight icons. No section headers, no per-group labels, no in-line dividers between groups.** `IconRail` accepts `mainItems`, `utilityItems`, and a `footer`. Use those three slots — do NOT inject section labels or dividers via `renderLink`, custom items, or a wrapping component. **Why:** A "Testing" divider above Journeys was specced and built three different ways in 2026-05 (renderLink-injected, a real `IconRailDivider` API in meda, and a consumer `pnpm patch`). All three were abandoned — meda PR #161 and labs PR #162 were closed unmerged; labs PR #163 removed the work. The icon-button slot is 44×44; anything wider overflows and overlaps the next icon, and an icon-only ~60px rail cannot host a text label legibly. **Do not re-propose dividers, group headers, or "mark this surface as testing/ops" rail treatments.** If a surface needs a type indicator, it lives in the surface itself (a banner, a header pill, a column-header badge) — never in the icon rail. The existing `RailDivider` in `icon-rail.tsx` is a different pattern: a chevron toggle that **repositions** utility items between top and bottom of the rail. It is spatial, not a group label. Do not generalize it into section headers. ## IconRail item shape + active styling ```ts interface IconRailItem { id: string; label: string; // shown only in tooltip, never inline icon: LucideIcon; // Lucide React only — no other icon libs to: string; badge?: ReactNode; // small status indicator, top-right of the slot } ``` Slot is `h-11 w-11 rounded-xl`. Active state uses `bg-primary/12 text-primary` (a 12%-alpha brand tint, NOT a solid brand fill — this is the legitimate `bg-primary` use case from the `brand` skill's note). Inactive uses `text-muted-foreground hover:bg-accent hover:text-foreground`. Don't override unless you're consciously diverging. Pass `activeId` to mark which item is active; usually derived from your router's current path. ## `renderLink` — when to use it `IconRail` and other rail components accept a `renderLink` prop that wraps the default `<a>`: ```tsx renderLink={({ item, isActive, className, children, linkProps }) => ( <NextLink href={item.to} className={className} {...linkProps}> {children} </NextLink> )} ``` **Use it for:** integrating with Next.js `Link`, TanStack Router, React Router — anything that needs client-side navigation hooks. **Do NOT use it for:** injecting dividers, headers, badges outside the slot, or any non-link content (see the flat-rail rule). The `className` parameter constrains your wrapper to the 44×44 slot — non-link content overflows. ## Command palette `CommandPalette` is registry-driven. Components register their commands via the **public hooks** `useCommands` and `useCommandGroup` from `@medalsocial/meda/shell` — both must run inside a `<CommandPalette>` (they throw otherwise). ```tsx import { useCommands, useCommandGroup } from '@medalsocial/meda/shell'; function MyFeature() { // Optional: register the group first so its label + ordering are known. useCommandGroup({ id: 'tools', label: 'Tools', priority: 50 }); useCommands([ { id: 'my.action', label: 'Run my action', group: 'tools', run: () => doIt() }, ]); return null; // or your real UI } ``` Each hook auto-handles register-on-mount and unregister-on-unmount via `useEffect`. Lower `priority` numbers render the group earlier (default 100). `CommandRegistryContext` is internal — don't import or `useContext` it directly. The hooks are the supported API. The default palette hotkey is `'mod+k'` — override via `MedaShellProvider.commandPaletteHotkey`. Hotkey matching is strict modifier-aware: `'mod+k'` does NOT fire on `mod+shift+k`. Use `'mod'` (resolves to ⌘ on macOS, Ctrl on Windows/Linux), not platform-specific keywords. ## Right panel patterns Use a single `RightPanel` per shell. Don't build a parallel right-side surface — multiple right panels create state and dismiss-behavior conflicts. For a stacked detail experience, register multiple `PanelView`s with the existing `PanelViewsProvider` (`src/shell/panel-views-provider.tsx`). ## Drag patterns `RailDropSlot`, `RailDropZones`, `DragModeBanner` are the canonical drag patterns. Don't add custom drag handlers to the rail — use these so the visual + a11y behavior matches across consumers. ## Anti-patterns | Anti-pattern | Why it's wrong | Correct approach | |---|---|---| | Adding a section divider/header to `IconRail` | Three prior attempts abandoned; slot geometry can't host labels | Keep the rail flat; put type indicators in surfaces | | Using `renderLink` for non-link content | Wraps inside the 44×44 slot — overflows/overlaps | Use `mainItems` / `utilityItems` / `footer` only | | Picking `'workspace'` variant on a sign-in route | Renders rails on routes with no app context | Use `'auth'` variant; switch to `'workspace'` after auth | | Re-implementing the theme toggle when overriding `workspace.menuItems` | The package inserts the toggle automatically | Just ship your menu items; toggle is added between items and footer | | Custom icon library | Inconsistent sizing + brand tone | Lucide React only | | Multiple `RightPanel`s in one shell | Dismiss/state conflicts | Use `PanelViewsProvider` for stacked detail | | Forking `MedaShellProvider` per app | Loses cross-consumer parity | Compose around it; pass a custom `ThemeAdapter` for theme integration | | Hard-coded modifier in hotkey strings (`'cmd+k'`) | Breaks on Windows/Linux | Use `'mod+k'` — resolves per-platform |