@medalsocial/meda 2.4.1 → 2.4.2
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.
- package/package.json +10 -2
- package/skills/brand/SKILL.md +198 -0
- package/skills/components/SKILL.md +196 -0
- package/skills/shell/SKILL.md +151 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/meda",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "Shared Meda UI shell and runtime package.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"README.md",
|
|
22
22
|
"components.json",
|
|
23
|
-
"dist"
|
|
23
|
+
"dist",
|
|
24
|
+
"skills",
|
|
25
|
+
"!skills/_artifacts",
|
|
26
|
+
"!skills/**/_artifacts",
|
|
27
|
+
"!skills/**/_artifacts/**"
|
|
24
28
|
],
|
|
25
29
|
"exports": {
|
|
26
30
|
".": {
|
|
@@ -191,6 +195,7 @@
|
|
|
191
195
|
"@storybook/addon-vitest": "10.3.6",
|
|
192
196
|
"@storybook/react-vite": "10.3.6",
|
|
193
197
|
"@tailwindcss/vite": "4.2.4",
|
|
198
|
+
"@tanstack/intent": "0.0.41",
|
|
194
199
|
"@testing-library/jest-dom": "6.9.1",
|
|
195
200
|
"@testing-library/react": "16.3.2",
|
|
196
201
|
"@types/react": "19.2.14",
|
|
@@ -220,6 +225,9 @@
|
|
|
220
225
|
"vitest-axe": "0.1.0",
|
|
221
226
|
"wrangler": "4.87.0"
|
|
222
227
|
},
|
|
228
|
+
"keywords": [
|
|
229
|
+
"tanstack-intent"
|
|
230
|
+
],
|
|
223
231
|
"scripts": {
|
|
224
232
|
"build": "pnpm exec tsc -p tsconfig.build.json && node ./scripts/build.mjs",
|
|
225
233
|
"lint": "biome check .",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: brand
|
|
3
|
+
description: Use when picking a CTA color, choosing brand-vs-semantic tokens, building a button/badge/pill, styling a chart or sidebar, configuring focus rings, editing any file under `src/styles/`, or scaffolding theme tokens in a new app that consumes `@medalsocial/meda`. Required reading before any visual work — covers the constant-brand CTA rule, the full token contract, and Tailwind v4 wiring.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Meda Brand & Tokens
|
|
7
|
+
|
|
8
|
+
## When to load this skill
|
|
9
|
+
|
|
10
|
+
- Editing `tokens.css`, `theme.css`, `globals.css`, or any file under `src/styles/`.
|
|
11
|
+
- Choosing a color for a CTA, link, badge, status pill, sidebar item, or chart series.
|
|
12
|
+
- Adding, renaming, or removing a token.
|
|
13
|
+
- Picking typography (font family, weight, size scale).
|
|
14
|
+
- Configuring focus rings, radii, or spacing.
|
|
15
|
+
- Wiring a brand logo or accent color in a consumer app.
|
|
16
|
+
|
|
17
|
+
## Overview
|
|
18
|
+
|
|
19
|
+
`@medalsocial/meda` is **purple-forward with a zinc-aligned neutral base** — not a custom warm/cool scheme. Two coexisting layers of tokens:
|
|
20
|
+
|
|
21
|
+
- **Primitive ramps** — `brand-*`, `neutral-*`, `error-*`, `info-*`, `success-*`, `warning-*`, plus dark-mode `surface-*` constants. Constant across themes.
|
|
22
|
+
- **Semantic tokens** — `--primary`, `--accent`, `--background`, `--card`, `--sidebar`, `--chart-1`...`--chart-5`, `--text-*`. Flip by theme.
|
|
23
|
+
|
|
24
|
+
Most components reference semantic tokens. Primary CTAs and brand-identity surfaces pin to the brand ramp directly — see the rule below.
|
|
25
|
+
|
|
26
|
+
CSS framework: **Tailwind v4**, CSS-first via `@theme inline` in `src/styles/theme.css`. **No `tailwind.config.js`**, no JS preset. Primitives are exposed as Tailwind theme keys (`--color-brand-*`, `--color-neutral-*`, `--color-chart-*`, etc.), so `bg-brand-500`, `text-success-600`, `bg-chart-3` work natively. Dark mode uses the Tailwind `dark:` variant on the `<html>` root.
|
|
27
|
+
|
|
28
|
+
## Brand ramp (constant across themes — purple, 11 stops)
|
|
29
|
+
|
|
30
|
+
| Stop | Hex |
|
|
31
|
+
|---|---|
|
|
32
|
+
| brand-50 | `#FAFAFD` |
|
|
33
|
+
| brand-100 | `#EEEAF5` |
|
|
34
|
+
| brand-200 | `#DCD4E8` |
|
|
35
|
+
| brand-300 | `#B8A3D2` |
|
|
36
|
+
| brand-400 | `#9A6AC2` |
|
|
37
|
+
| brand-500 | `#7E3FAC` |
|
|
38
|
+
| brand-600 | `#6A2E96` |
|
|
39
|
+
| brand-700 | `#482070` |
|
|
40
|
+
| brand-800 | `#2F1552` |
|
|
41
|
+
| brand-900 | `#1C0E38` |
|
|
42
|
+
| brand-950 | `#120A24` |
|
|
43
|
+
|
|
44
|
+
## The constant-brand CTA rule (critical)
|
|
45
|
+
|
|
46
|
+
**Primary CTAs and brand-identity surfaces use `bg-brand-500 hover:bg-brand-600 text-white` in BOTH light and dark themes. Do NOT use `bg-primary` / `text-primary-foreground` for primary CTAs.**
|
|
47
|
+
|
|
48
|
+
**Why:** `--primary` deliberately flips — `brand-800` in light, `brand-100` in dark. `brand-100` is a pale near-white, so `bg-primary` renders a primary CTA as a washed-out near-white pill in dark mode. Medal's brand identity is the rich `#7E3FAC` purple applied constantly, not theme-inverted.
|
|
49
|
+
|
|
50
|
+
**`bg-primary` is fine when used as a tint/overlay** (e.g. `bg-primary/12 text-primary` for an active rail item — see the `shell` skill). The prohibition is specifically about solid CTA fills where the user expects a vivid brand presence.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
// ❌ Wrong: pale in dark
|
|
54
|
+
<Button className="bg-primary text-primary-foreground">Get started</Button>
|
|
55
|
+
|
|
56
|
+
// ✅ Right: constant brand-purple in both themes
|
|
57
|
+
<Button className="bg-brand-500 hover:bg-brand-600 text-white">Get started</Button>
|
|
58
|
+
|
|
59
|
+
// ✅ Also right (active state, tinted overlay)
|
|
60
|
+
<Link className="bg-primary/12 text-primary">Dashboard</Link>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Logos and brand marks render in `var(--color-brand-500)`, not `currentColor`.
|
|
64
|
+
|
|
65
|
+
## Neutral ramp (zinc-aligned)
|
|
66
|
+
|
|
67
|
+
| Stop | Hex | Tailwind zinc |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| neutral-50 | `#FAFAFA` | zinc-50 |
|
|
70
|
+
| neutral-100 | `#F4F4F5` | zinc-100 |
|
|
71
|
+
| neutral-200 | `#E4E4E7` | zinc-200 |
|
|
72
|
+
| neutral-300 | `#A1A1AA` | zinc-400 |
|
|
73
|
+
| neutral-400 | `#71717A` | zinc-500 |
|
|
74
|
+
| neutral-500 | `#52525B` | zinc-600 |
|
|
75
|
+
| neutral-600 | `#3F3F46` | zinc-700 |
|
|
76
|
+
| neutral-700 | `#27272A` | zinc-800 |
|
|
77
|
+
| neutral-800 | `#18181B` | zinc-900 |
|
|
78
|
+
| neutral-900 | `#111113` | between zinc-900/950 |
|
|
79
|
+
| neutral-950 | `#09090B` | zinc-950 |
|
|
80
|
+
|
|
81
|
+
**Do not** ship a generic gray neutral palette (Apple-style cool grays, Tailwind `gray-*`). Use the zinc-aligned ramp.
|
|
82
|
+
|
|
83
|
+
## Status ramps
|
|
84
|
+
|
|
85
|
+
| Status | Family | Mid (600) |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| error | rose | `#E11D48` |
|
|
88
|
+
| info | blue | `#2563EB` |
|
|
89
|
+
| **success** | **teal**, not green | `#0D9488` |
|
|
90
|
+
| warning | amber | `#D97706` |
|
|
91
|
+
|
|
92
|
+
Each ramp has 10 stops (50, 100–900).
|
|
93
|
+
|
|
94
|
+
## Surface primitives (dark-mode constants)
|
|
95
|
+
|
|
96
|
+
- `--surface-bg`: `#09090B`
|
|
97
|
+
- `--surface-card`: `#18181B`
|
|
98
|
+
- `--surface-muted`: `#27272A`
|
|
99
|
+
- `--surface-border`: `#2E2E33`
|
|
100
|
+
- `--surface-sidebar`: `#0F0F12`
|
|
101
|
+
|
|
102
|
+
## Semantic tokens (flip by theme)
|
|
103
|
+
|
|
104
|
+
### shadcn-shape core
|
|
105
|
+
|
|
106
|
+
| Token | Light | Dark |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| `--background` | `neutral-50` | `surface-bg` |
|
|
109
|
+
| `--foreground` | `neutral-950` | `neutral-50` |
|
|
110
|
+
| `--card` | `neutral-50` | `surface-card` |
|
|
111
|
+
| `--card-foreground` | `neutral-950` | `neutral-50` |
|
|
112
|
+
| `--popover` | `neutral-50` | `surface-card` |
|
|
113
|
+
| **`--primary`** | **`brand-800`** | **`brand-100`** |
|
|
114
|
+
| `--primary-foreground` | `brand-50` | `brand-900` |
|
|
115
|
+
| `--secondary` | `neutral-200` | `surface-muted` |
|
|
116
|
+
| `--muted` | `neutral-100` | `surface-muted` |
|
|
117
|
+
| `--muted-foreground` | `neutral-500` | `neutral-300` |
|
|
118
|
+
| `--accent` | `neutral-200` | `surface-muted` |
|
|
119
|
+
| `--destructive` | `error-600` | `error-400` |
|
|
120
|
+
| `--border` | `neutral-200` | `surface-border` |
|
|
121
|
+
| `--input` | `neutral-200` | `surface-muted` |
|
|
122
|
+
| `--ring` | `brand-600` | `brand-400` |
|
|
123
|
+
|
|
124
|
+
**Critical:** `--accent` is a NEUTRAL by contract (not a brand color). Use `brand-*` tokens directly when you want brand color. `--ring` (focus ring) is brand-keyed and theme-flips between brand-600 and brand-400.
|
|
125
|
+
|
|
126
|
+
### Status (semantic)
|
|
127
|
+
|
|
128
|
+
| Token | Light | Dark |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| `--success` / `--success-foreground` | `success-600` / `neutral-950` | `success-400` / `neutral-950` |
|
|
131
|
+
| `--warning` / `--warning-foreground` | `warning-600` / `neutral-950` | `warning-400` / `neutral-950` |
|
|
132
|
+
| `--info` / `--info-foreground` | `info-600` / `neutral-50` | `info-400` / `neutral-950` |
|
|
133
|
+
| `--danger` (alias for destructive) | `error-600` | `error-400` |
|
|
134
|
+
|
|
135
|
+
### Sidebar
|
|
136
|
+
|
|
137
|
+
| Token | Light | Dark |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `--sidebar` | `neutral-100` | `surface-sidebar` |
|
|
140
|
+
| `--sidebar-foreground` | `neutral-950` | `neutral-50` |
|
|
141
|
+
| `--sidebar-primary` | `brand-700` | `brand-400` |
|
|
142
|
+
| `--sidebar-accent` | `neutral-50` | `surface-card` |
|
|
143
|
+
| `--sidebar-border` | `neutral-200` | `surface-border` |
|
|
144
|
+
| `--sidebar-ring` | `brand-500` | `brand-400` |
|
|
145
|
+
|
|
146
|
+
### Chart (5 series)
|
|
147
|
+
|
|
148
|
+
| Token | Light | Dark | Role |
|
|
149
|
+
|---|---|---|---|
|
|
150
|
+
| `--chart-1` | `brand-600` | `brand-500` | Brand purple |
|
|
151
|
+
| `--chart-2` | `info-600` | `info-500` | Blue |
|
|
152
|
+
| `--chart-3` | `success-600` | `success-400` | Teal |
|
|
153
|
+
| `--chart-4` | `warning-600` | `warning-500` | Amber |
|
|
154
|
+
| `--chart-5` | `error-600` | `error-400` | Rose |
|
|
155
|
+
|
|
156
|
+
Always cycle in this order — `--chart-1` first — so brand purple owns the primary series across consumers.
|
|
157
|
+
|
|
158
|
+
### Text
|
|
159
|
+
|
|
160
|
+
| Token | Light | Dark |
|
|
161
|
+
|---|---|---|
|
|
162
|
+
| `--text` | `neutral-950` | `neutral-50` |
|
|
163
|
+
| `--text-muted` | `neutral-500` | `neutral-300` |
|
|
164
|
+
| `--text-link` | `brand-600` | `brand-400` |
|
|
165
|
+
| `--text-primary` | `neutral-950` | `neutral-100` |
|
|
166
|
+
| `--text-secondary` | `neutral-600` | `neutral-300` |
|
|
167
|
+
|
|
168
|
+
## Typography — Geist only
|
|
169
|
+
|
|
170
|
+
- `--font-family-sans`: **`"Geist"`**
|
|
171
|
+
- `--font-family-mono`: **`"Geist Mono"`**
|
|
172
|
+
|
|
173
|
+
**No Inter, no system stacks.** Geist + Geist Mono only.
|
|
174
|
+
|
|
175
|
+
Size scale: `display` 36, `h1` 30, `h2` 24, `h3` 20, `h4` 18, `body-lg` 16, `body` 14, `body-sm` 13, `caption` 11, `overline` 10.
|
|
176
|
+
|
|
177
|
+
## Radii
|
|
178
|
+
|
|
179
|
+
`--radius` (base) 8px. Scale: `radius-sm` 4, `radius-md` 6, `radius-lg` 8, `radius-xl` 12, `radius-2xl` 16, `radius-3xl` 20, `radius-4xl` 24, `radius-full` 9999.
|
|
180
|
+
|
|
181
|
+
## Spacing
|
|
182
|
+
|
|
183
|
+
`xs` 4, `sm` 8, `md` 12, `lg` 16, `xl` 24, `2xl` 32, `3xl` 48, `4xl` 64 (px). Use the named tokens, not raw px values, so consumer overrides flow through.
|
|
184
|
+
|
|
185
|
+
## Anti-patterns
|
|
186
|
+
|
|
187
|
+
| Anti-pattern | Why it's wrong | Correct approach |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `bg-primary` for a primary CTA | `--primary` flips to pale brand-100 in dark — washed-out CTAs | `bg-brand-500 text-white` (constant) |
|
|
190
|
+
| Generic gray neutrals (Tailwind `gray-*`) | Medal is zinc-aligned, not cool gray | Use `neutral-*` from the ramp |
|
|
191
|
+
| `--primary` mapped to brand-500 | Lib has primary as brand-800 light / brand-100 dark | Map exactly — light=brand-800, dark=brand-100 |
|
|
192
|
+
| Treating `--accent` as a brand color | `--accent` is neutral by contract | Use `brand-*` tokens directly |
|
|
193
|
+
| Hard-coded `ring-blue-500` for focus rings | Focus rings are brand-keyed via `--ring` | `focus-visible:ring-2 focus-visible:ring-ring` |
|
|
194
|
+
| Picking chart colors ad-hoc per app | Loses cross-consumer parity | Cycle `--chart-1`..`--chart-5` in order |
|
|
195
|
+
| Adding Inter or system fonts | Canonical is Geist + Geist Mono | Stick to the two families |
|
|
196
|
+
| `success` as green | Canonical success is teal | Use the teal scale |
|
|
197
|
+
| `[...].join(' ')` for classNames | Noisy, error-prone | Use a consumer-owned `cn()` helper — see the `components` skill |
|
|
198
|
+
| Raw px (e.g. `pt-[12px]`) for spacing | Bypasses consumer overrides | Use the named spacing tokens |
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: components
|
|
3
|
+
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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Meda Components
|
|
7
|
+
|
|
8
|
+
## When to load this skill
|
|
9
|
+
|
|
10
|
+
- About to author or modify a React component that renders UI.
|
|
11
|
+
- Choosing where a new component lives (primitive / ui-adapter / domain / recipe).
|
|
12
|
+
- Composing `Card`, `EmptyState`, `StatusPill`, or another exported primitive.
|
|
13
|
+
- Picking between primitives and shadcn-style ui adapters.
|
|
14
|
+
- Writing or updating a `*.stories.tsx`.
|
|
15
|
+
- Importing `MarkdownView` or another optional-peer-deps component.
|
|
16
|
+
|
|
17
|
+
## Look up first, generate second
|
|
18
|
+
|
|
19
|
+
**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.
|
|
20
|
+
|
|
21
|
+
If the MCP is not running, start it:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pnpm storybook
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The MCP is registered in `.mcp.json`.
|
|
28
|
+
|
|
29
|
+
## Where things live
|
|
30
|
+
|
|
31
|
+
| Folder | What it is |
|
|
32
|
+
|---|---|
|
|
33
|
+
| `src/primitives/` | Standalone visual building blocks. Each owns its own styling. |
|
|
34
|
+
| `src/components/ui/` | shadcn-style adapters that wrap `@base-ui/react`. |
|
|
35
|
+
| `src/shell/` | Layout regions — see the `shell` skill. |
|
|
36
|
+
| `src/{chat,kanban,calendar,timeline,marketing,workflow-builder,...}/` | Domain folders for higher-level composed surfaces. |
|
|
37
|
+
| `src/recipes/` | Framework-specific glue (e.g. `recipes/next.ts`). |
|
|
38
|
+
| `src/lib/` | Shared utilities (`cn()`, etc.). |
|
|
39
|
+
|
|
40
|
+
**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.
|
|
41
|
+
|
|
42
|
+
## Primitive vs ui-adapter vs domain — picking where to add
|
|
43
|
+
|
|
44
|
+
| If you need… | Reach for | Example |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| A self-contained visual element with no behavior dependency | `src/primitives/` | `Card`, `StatusPill`, `EmptyState`, `Skeleton`, `FilterRail` |
|
|
47
|
+
| An overlay / menu / form control wrapping `@base-ui/react` | `src/components/ui/` | `DropdownMenu`, `Dialog`, `Drawer`, `Tooltip`, `Command`, `Checkbox`, `Collapsible` |
|
|
48
|
+
| A composed surface tied to a product area | A domain folder | A chat thread (`src/chat/`), a kanban column (`src/kanban/`) |
|
|
49
|
+
| Framework-specific glue | `src/recipes/` | `recipes/next.ts` |
|
|
50
|
+
|
|
51
|
+
## Primitives layer
|
|
52
|
+
|
|
53
|
+
**`@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`.
|
|
54
|
+
|
|
55
|
+
## Card — compound component
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
import { Card } from '@medalsocial/meda';
|
|
59
|
+
|
|
60
|
+
<Card>
|
|
61
|
+
<Card.Header>
|
|
62
|
+
<h3 className="text-h4">Workspace usage</h3>
|
|
63
|
+
</Card.Header>
|
|
64
|
+
<Card.Body>
|
|
65
|
+
Body content here.
|
|
66
|
+
</Card.Body>
|
|
67
|
+
<Card.Footer>
|
|
68
|
+
<Button variant="ghost">Cancel</Button>
|
|
69
|
+
<Button>Save</Button>
|
|
70
|
+
</Card.Footer>
|
|
71
|
+
</Card>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`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.
|
|
75
|
+
|
|
76
|
+
## EmptyState — variants matter
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import { EmptyState } from '@medalsocial/meda';
|
|
80
|
+
import { Inbox } from 'lucide-react';
|
|
81
|
+
|
|
82
|
+
<EmptyState
|
|
83
|
+
icon={Inbox}
|
|
84
|
+
title="No messages yet"
|
|
85
|
+
description="When someone messages your workspace, it'll show up here."
|
|
86
|
+
action={<Button>Invite teammates</Button>}
|
|
87
|
+
variant="panel"
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Variants:
|
|
92
|
+
- `'default'` — full-page empty state, generous vertical spacing.
|
|
93
|
+
- `'panel'` — panel-sized empty state (e.g. inside a right panel or card).
|
|
94
|
+
- `'inline'` — compact, for list-row or table-row empty states.
|
|
95
|
+
|
|
96
|
+
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.
|
|
97
|
+
|
|
98
|
+
## StatusPill — solid tones, two sizes
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { StatusPill } from '@medalsocial/meda';
|
|
102
|
+
|
|
103
|
+
<StatusPill tone="success">Synced</StatusPill>
|
|
104
|
+
<StatusPill tone="warning" size="md">Action required</StatusPill>
|
|
105
|
+
<StatusPill tone="danger" dot={false}>Failed</StatusPill>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
- `tone`: `'neutral' | 'info' | 'success' | 'warning' | 'danger'`
|
|
109
|
+
- `size`: `'sm'` (default, 11px text) or `'md'` (12px text)
|
|
110
|
+
- `dot`: `true` (default) to show a leading dot
|
|
111
|
+
|
|
112
|
+
**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`.
|
|
113
|
+
|
|
114
|
+
## ui-adapters — NOT exported from the npm package
|
|
115
|
+
|
|
116
|
+
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.
|
|
117
|
+
|
|
118
|
+
Two supported ways to consume them:
|
|
119
|
+
|
|
120
|
+
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:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npx shadcn@latest add https://meda.medalsocial.com/r/<component>.json
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The components land in your app's `@/ui` folder (configurable via your `components.json` aliases). You own and can modify the code.
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
Do NOT try `import { Dialog } from '@medalsocial/meda'` — it will fail with a missing-export error.
|
|
131
|
+
|
|
132
|
+
## Optional peer dependencies — MarkdownView pattern
|
|
133
|
+
|
|
134
|
+
`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:
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { MarkdownView, type MarkdownViewProps } from '@medalsocial/meda/markdown-view';
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
When you add another optional-peer component, follow this pattern: own subpath, no root re-export, peers declared as `peerDependenciesMeta.*.optional`.
|
|
141
|
+
|
|
142
|
+
## Storybook discipline
|
|
143
|
+
|
|
144
|
+
**Every exported component has a `*.stories.tsx`.** New components without stories fail review.
|
|
145
|
+
|
|
146
|
+
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).
|
|
147
|
+
|
|
148
|
+
Visual snapshots run through Chromatic on every PR.
|
|
149
|
+
|
|
150
|
+
## Icons
|
|
151
|
+
|
|
152
|
+
**Lucide React only.** No other icon libraries in new code. Never use emojis as UI icons. Custom-drawn icons require explicit review.
|
|
153
|
+
|
|
154
|
+
Shell sizes: 22px (rails), 16px (header), 14px (inline). Inside primitives like `EmptyState`, the icon auto-sizes to match the variant.
|
|
155
|
+
|
|
156
|
+
## className composition
|
|
157
|
+
|
|
158
|
+
Do not use `[...].join(' ')` or template-string concatenation for classes — noisy, merge-unsafe.
|
|
159
|
+
|
|
160
|
+
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):
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
pnpm add clsx tailwind-merge
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
// your-app/src/lib/utils.ts
|
|
168
|
+
import { type ClassValue, clsx } from 'clsx';
|
|
169
|
+
import { twMerge } from 'tailwind-merge';
|
|
170
|
+
|
|
171
|
+
export function cn(...inputs: ClassValue[]): string {
|
|
172
|
+
return twMerge(clsx(inputs));
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import { cn } from '@/lib/utils';
|
|
178
|
+
|
|
179
|
+
<div className={cn('base classes', isActive && 'active', className)} />
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Anti-patterns
|
|
183
|
+
|
|
184
|
+
| Anti-pattern | Why it's wrong | Correct approach |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Generating UI without checking Storybook first | Duplicates existing primitives | Query `meda-storybook` MCP first |
|
|
187
|
+
| `@radix-ui/*` in new code | Deprecated primitives layer | Wrap `@base-ui/react` in `src/components/ui/` |
|
|
188
|
+
| Re-exporting `MarkdownView` from root | Breaks consumers without optional peers | Import from `@medalsocial/meda/markdown-view` |
|
|
189
|
+
| 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 |
|
|
190
|
+
| Raw `<div>` inside `<Card>` with custom padding | Bypasses border-collapse logic; off-grid spacing | Use `Card.Header` / `Card.Body` / `Card.Footer` |
|
|
191
|
+
| `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` |
|
|
192
|
+
| `import { cn } from '@medalsocial/meda'` | Not exported | Declare your own three-line `cn()` using `clsx` + `tailwind-merge` (already transitive) |
|
|
193
|
+
| Adding emojis as UI icons | Inconsistent sizing + brand | Use Lucide equivalents (`Clock`, `Check`, `AlertTriangle`) |
|
|
194
|
+
| Inventing a new top-level layer folder | Loses the established taxonomy | Use primitives / ui / domain / recipes |
|
|
195
|
+
| Skipping `*.stories.tsx` for an exported component | Breaks the lookup-first discipline for the next contributor | Always ship a story |
|
|
196
|
+
| `[...].join(' ')` for classes | Error-prone | Use `cn()` |
|
|
@@ -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 |
|