@medalsocial/meda 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: shell
3
+ 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.
4
+ ---
5
+
6
+ # Meda Shell
7
+
8
+ ## When to load this skill
9
+
10
+ - Scaffolding a new shell — picking `AppShell` variant and configuring `MedaShellProvider`.
11
+ - Adding/removing/reordering icon rail items.
12
+ - Registering command palette actions.
13
+ - Wiring the workspace switcher or theme toggle in the header.
14
+ - Building or modifying the right panel.
15
+ - Touching anything under `src/shell/` in `@medalsocial/meda`.
16
+
17
+ ## App shell variants
18
+
19
+ The package exports a single `<AppShell>` component with a discriminated `variant` prop. Pick exactly one variant per route group:
20
+
21
+ | Variant | When to use | Key config |
22
+ |---|---|---|
23
+ | `'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) |
24
+ | `'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` |
25
+ | `'chat'` | Chat-first surfaces (full-bleed messaging UI; no rails). | `globalActions` |
26
+
27
+ `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.
28
+
29
+ ## MedaShellProvider — the runtime root
30
+
31
+ Wrap your app once with `<MedaShellProvider>` (typically in the root layout). Props:
32
+
33
+ ```ts
34
+ interface MedaShellProviderProps {
35
+ workspace: WorkspaceDefinition; // required
36
+ workspaces?: WorkspaceDefinition[]; // for the switcher
37
+ apps: AppDefinition[]; // required, non-empty (throws if empty)
38
+ defaultActiveApp?: string; // app id; defaults to apps[0]
39
+ storage?: ShellStorageAdapter; // defaults to localStorage adapter
40
+ mobileBottomNav?: MobileBottomNavItem[]; // mobile nav config
41
+ commandPaletteHotkey?: string; // e.g. 'mod+k' (default)
42
+ themeAdapter?: 'default' | 'next-themes' | ThemeAdapter; // pick per-framework
43
+ children: ReactNode;
44
+ }
45
+ ```
46
+
47
+ - **`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.
48
+ - **`themeAdapter: 'default'`** uses the built-in adapter (no external dep).
49
+ - Pass a custom `ThemeAdapter` object to integrate with any other theme system.
50
+ - `apps` MUST have at least one entry — the provider throws on empty array.
51
+
52
+ ## Regions and component map
53
+
54
+ | Region | Component | Source |
55
+ |---|---|---|
56
+ | Icon rail | `IconRail` | `src/shell/icon-rail.tsx` |
57
+ | Context rail | `ContextRail` | `src/shell/context-rail.tsx` |
58
+ | Header | `ShellHeader` + `WorkspaceSwitcher` | `src/shell/shell-header.tsx` |
59
+ | Main | `ShellMain` | `src/shell/shell-main.tsx` |
60
+ | Right panel | `RightPanel` | `src/shell/right-panel.tsx` |
61
+ | Command palette | `CommandPalette` + `CommandRegistryContext` | `src/shell/command-palette.tsx` |
62
+
63
+ ## The flat-rail rule (critical)
64
+
65
+ **The `IconRail` is a flat list of equal-weight icons. No section headers, no per-group labels, no in-line dividers between groups.**
66
+
67
+ `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.
68
+
69
+ **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.**
70
+
71
+ 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.
72
+
73
+ 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.
74
+
75
+ ## IconRail item shape + active styling
76
+
77
+ ```ts
78
+ interface IconRailItem {
79
+ id: string;
80
+ label: string; // shown only in tooltip, never inline
81
+ icon: LucideIcon; // Lucide React only — no other icon libs
82
+ to: string;
83
+ badge?: ReactNode; // small status indicator, top-right of the slot
84
+ }
85
+ ```
86
+
87
+ 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.
88
+
89
+ Pass `activeId` to mark which item is active; usually derived from your router's current path.
90
+
91
+ ## `renderLink` — when to use it
92
+
93
+ `IconRail` and other rail components accept a `renderLink` prop that wraps the default `<a>`:
94
+
95
+ ```tsx
96
+ renderLink={({ item, isActive, className, children, linkProps }) => (
97
+ <NextLink href={item.to} className={className} {...linkProps}>
98
+ {children}
99
+ </NextLink>
100
+ )}
101
+ ```
102
+
103
+ **Use it for:** integrating with Next.js `Link`, TanStack Router, React Router — anything that needs client-side navigation hooks.
104
+
105
+ **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.
106
+
107
+ ## Command palette
108
+
109
+ `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).
110
+
111
+ ```tsx
112
+ import { useCommands, useCommandGroup } from '@medalsocial/meda/shell';
113
+
114
+ function MyFeature() {
115
+ // Optional: register the group first so its label + ordering are known.
116
+ useCommandGroup({ id: 'tools', label: 'Tools', priority: 50 });
117
+
118
+ useCommands([
119
+ { id: 'my.action', label: 'Run my action', group: 'tools', run: () => doIt() },
120
+ ]);
121
+
122
+ return null; // or your real UI
123
+ }
124
+ ```
125
+
126
+ Each hook auto-handles register-on-mount and unregister-on-unmount via `useEffect`. Lower `priority` numbers render the group earlier (default 100).
127
+
128
+ `CommandRegistryContext` is internal — don't import or `useContext` it directly. The hooks are the supported API.
129
+
130
+ 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.
131
+
132
+ ## Right panel patterns
133
+
134
+ 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`).
135
+
136
+ ## Drag patterns
137
+
138
+ `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.
139
+
140
+ ## Anti-patterns
141
+
142
+ | Anti-pattern | Why it's wrong | Correct approach |
143
+ |---|---|---|
144
+ | 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 |
145
+ | Using `renderLink` for non-link content | Wraps inside the 44×44 slot — overflows/overlaps | Use `mainItems` / `utilityItems` / `footer` only |
146
+ | Picking `'workspace'` variant on a sign-in route | Renders rails on routes with no app context | Use `'auth'` variant; switch to `'workspace'` after auth |
147
+ | 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 |
148
+ | Custom icon library | Inconsistent sizing + brand tone | Lucide React only |
149
+ | Multiple `RightPanel`s in one shell | Dismiss/state conflicts | Use `PanelViewsProvider` for stacked detail |
150
+ | Forking `MedaShellProvider` per app | Loses cross-consumer parity | Compose around it; pass a custom `ThemeAdapter` for theme integration |
151
+ | Hard-coded modifier in hotkey strings (`'cmd+k'`) | Breaks on Windows/Linux | Use `'mod+k'` — resolves per-platform |