UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

197 lines (138 loc) 9.94 kB
--- name: components description: Use when implementing a UI component in any app consuming `@medalsocial/meda` — building a card, dialog, status pill, empty state, dropdown, tooltip — or picking a primitive vs a shadcn-style adapter vs a domain folder. Required reading before generating any new visual code; the `meda-storybook` MCP server is the source of truth for what already exists. --- # Meda Components ## When to load this skill - About to author or modify a React component that renders UI. - Choosing where a new component lives (primitive / ui-adapter / domain / recipe). - Composing `Card`, `EmptyState`, `StatusPill`, or another exported primitive. - Picking between primitives and shadcn-style ui adapters. - Writing or updating a `*.stories.tsx`. - Importing `MarkdownView` or another optional-peer-deps component. ## Look up first, generate second **Before writing any new visual component, query the `meda-storybook` MCP server.** It exposes the running Storybook (`pnpm storybook` on `http://localhost:6006`) and is the authoritative index of what already exists. Many "new" components are existing primitives composed differently. If the MCP is not running, start it: ```bash pnpm storybook ``` The MCP is registered in `.mcp.json`. ## Where things live | Folder | What it is | |---|---| | `src/primitives/` | Standalone visual building blocks. Each owns its own styling. | | `src/components/ui/` | shadcn-style adapters that wrap `@base-ui/react`. | | `src/shell/` | Layout regions — see the `shell` skill. | | `src/{chat,kanban,calendar,timeline,marketing,workflow-builder,...}/` | Domain folders for higher-level composed surfaces. | | `src/recipes/` | Framework-specific glue (e.g. `recipes/next.ts`). | | `src/lib/` | Shared utilities (`cn()`, etc.). | **Don't invent a new top-level layer** (primitives/ui/recipes). Adding a new *domain* folder for a real product area is fine; check with a maintainer first if uncertain. ## Primitive vs ui-adapter vs domain — picking where to add | If you need… | Reach for | Example | |---|---|---| | A self-contained visual element with no behavior dependency | `src/primitives/` | `Card`, `StatusPill`, `EmptyState`, `Skeleton`, `FilterRail` | | An overlay / menu / form control wrapping `@base-ui/react` | `src/components/ui/` | `DropdownMenu`, `Dialog`, `Drawer`, `Tooltip`, `Command`, `Checkbox`, `Collapsible` | | A composed surface tied to a product area | A domain folder | A chat thread (`src/chat/`), a kanban column (`src/kanban/`) | | Framework-specific glue | `src/recipes/` | `recipes/next.ts` | ## Primitives layer **`@base-ui/react` is the canonical primitives layer.** Forbidden in new code: `@radix-ui/*` packages. shadcn-style adapters in `src/components/ui/` wrap `@base-ui/react` — follow the existing patterns in `dropdown-menu.tsx` / `tooltip.tsx`. ## Card — compound component ```tsx import { Card } from '@medalsocial/meda'; <Card> <Card.Header> <h3 className="text-h4">Workspace usage</h3> </Card.Header> <Card.Body> Body content here. </Card.Body> <Card.Footer> <Button variant="ghost">Cancel</Button> <Button>Save</Button> </Card.Footer> </Card> ``` `Card`, `Card.Header`, `Card.Body`, `Card.Footer` are all `<div>`-typed and accept any standard div props. The header/footer auto-hide their borders when adjacent to the body (`last:border-b-0` / `first:border-t-0`), so a header-only or body-only card has no orphan border. Always use the subcomponents — don't reach into the card with raw padded `<div>`s. ## EmptyState — variants matter ```tsx import { EmptyState } from '@medalsocial/meda'; import { Inbox } from 'lucide-react'; <EmptyState icon={Inbox} title="No messages yet" description="When someone messages your workspace, it'll show up here." action={<Button>Invite teammates</Button>} variant="panel" /> ``` Variants: - `'default'` — full-page empty state, generous vertical spacing. - `'panel'` — panel-sized empty state (e.g. inside a right panel or card). - `'inline'` — compact, for list-row or table-row empty states. The `icon` prop accepts a Lucide component (`Inbox`) OR a rendered element (custom inline SVG). Lucide components are auto-sized; rendered elements are placed in an `inline-flex` span and you control their size. ## StatusPill — solid tones, two sizes ```tsx import { StatusPill } from '@medalsocial/meda'; <StatusPill tone="success">Synced</StatusPill> <StatusPill tone="warning" size="md">Action required</StatusPill> <StatusPill tone="danger" dot={false}>Failed</StatusPill> ``` - `tone`: `'neutral' | 'info' | 'success' | 'warning' | 'danger'` - `size`: `'sm'` (default, 11px text) or `'md'` (12px text) - `dot`: `true` (default) to show a leading dot **Why solid backgrounds (not tinted):** the 11px small size needs ≥4.5:1 contrast to pass WCAG AA. Tinted backgrounds (`bg-info/15 text-info`) fail axe gates at this size. Solid `bg-info text-info-foreground` etc. always passes via the theme tokens. If you need a tinted variant, use it at `size="md"` or larger only, and verify with `vitest-axe`. ## ui-adapters — NOT exported from the npm package shadcn-style adapters in `src/components/ui/` (`Dialog`, `DropdownMenu`, `Tooltip`, `Drawer`, `Command`, `Checkbox`, `Collapsible`) wrap `@base-ui/react`. **They are intentionally NOT exported from `@medalsocial/meda`** — meda follows the shadcn philosophy: you own the ui code. Two supported ways to consume them: 1. **Vendor via the meda shadcn registry** (recommended for shell-consistent apps). meda publishes a registry at `https://meda.medalsocial.com/r/...` — install via the shadcn CLI: ```bash npx shadcn@latest add https://meda.medalsocial.com/r/<component>.json ``` The components land in your app's `@/ui` folder (configurable via your `components.json` aliases). You own and can modify the code. 2. **Write your own wrapping `@base-ui/react` directly.** When you need a primitive meda doesn't provide a recipe for, follow the patterns visible in meda's `src/components/ui/dropdown-menu.tsx` / `tooltip.tsx` and ship the result in your own `@/ui` folder. Do NOT try `import { Dialog } from '@medalsocial/meda'` — it will fail with a missing-export error. ## Optional peer dependencies — MarkdownView pattern `MarkdownView` has optional peers (`react-markdown`, `remark-gfm`, `rehype-highlight`). It is **intentionally not re-exported from `src/primitives/index.ts`** — even type re-exports cause TS to follow the chain and break consumers who haven't installed the peers. Import via the dedicated subpath: ```ts import { MarkdownView, type MarkdownViewProps } from '@medalsocial/meda/markdown-view'; ``` When you add another optional-peer component, follow this pattern: own subpath, no root re-export, peers declared as `peerDependenciesMeta.*.optional`. ## Storybook discipline **Every exported component has a `*.stories.tsx`.** New components without stories fail review. Stories live next to the component (`card.tsx` + `card.stories.tsx`). Use the `__stories__/` folder only for cross-component demos (full shell stories, theme-switcher demos). Visual snapshots run through Chromatic on every PR. ## Icons **Lucide React only.** No other icon libraries in new code. Never use emojis as UI icons. Custom-drawn icons require explicit review. Shell sizes: 22px (rails), 16px (header), 14px (inline). Inside primitives like `EmptyState`, the icon auto-sizes to match the variant. ## className composition Do not use `[...].join(' ')` or template-string concatenation for classes — noisy, merge-unsafe. meda uses a `cn()` helper internally (`src/lib/utils.ts`) built from `clsx` + `tailwind-merge`, but it is **not exported from the package**. In consumer apps, declare your own — first install the two deps explicitly (don't rely on them being transitive — that's not safe under pnpm strict, Yarn PnP, or any layout that hides un-declared dependencies): ```bash pnpm add clsx tailwind-merge ``` ```ts // your-app/src/lib/utils.ts import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]): string { return twMerge(clsx(inputs)); } ``` ```tsx import { cn } from '@/lib/utils'; <div className={cn('base classes', isActive && 'active', className)} /> ``` ## Anti-patterns | Anti-pattern | Why it's wrong | Correct approach | |---|---|---| | Generating UI without checking Storybook first | Duplicates existing primitives | Query `meda-storybook` MCP first | | `@radix-ui/*` in new code | Deprecated primitives layer | Wrap `@base-ui/react` in `src/components/ui/` | | Re-exporting `MarkdownView` from root | Breaks consumers without optional peers | Import from `@medalsocial/meda/markdown-view` | | Tinted `StatusPill` at `size="sm"` | Fails WCAG AA at 11px (vitest-axe gate) | Use solid tones at `sm`; tints only at `md`+ with a11y verification | | Raw `<div>` inside `<Card>` with custom padding | Bypasses border-collapse logic; off-grid spacing | Use `Card.Header` / `Card.Body` / `Card.Footer` | | `import { Dialog } from '@medalsocial/meda'` | Not exported — meda follows the shadcn "you own the ui" model | Vendor via `npx shadcn@latest add https://meda.medalsocial.com/r/...` or write your own wrapping `@base-ui/react` | | `import { cn } from '@medalsocial/meda'` | Not exported | Declare your own three-line `cn()` using `clsx` + `tailwind-merge` (already transitive) | | Adding emojis as UI icons | Inconsistent sizing + brand | Use Lucide equivalents (`Clock`, `Check`, `AlertTriangle`) | | Inventing a new top-level layer folder | Loses the established taxonomy | Use primitives / ui / domain / recipes | | Skipping `*.stories.tsx` for an exported component | Breaks the lookup-first discipline for the next contributor | Always ship a story | | `[...].join(' ')` for classes | Error-prone | Use `cn()` |