@urbicon-ui/mcp-server 6.21.1 → 6.21.3

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/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # @urbicon-ui/mcp-server
2
2
 
3
- Model Context Protocol server for the Urbicon UI design system. Gives LLMs first-class access to the component catalog, design tokens, recipes, and implementation guidance — the AI-native DX pillar of Urbicon.
4
-
5
- **Transports:** stdio (default, for IDE integrations like Claude Code / Cursor) and streamable HTTP (for remote editors).
3
+ Model Context Protocol server for the Urbicon UI design system. Gives LLMs first-class access to the component catalog, design tokens, recipes, and implementation guidance.
4
+
5
+ > **Status (Option B, 2026-07-10): built, green, not advertised.** The consumer-facing
6
+ > surface is the `urbicon` CLI in [`@urbicon-ui/design`](../design/) — one dev-dependency,
7
+ > version-pinned, covers the full knowledge/judgment/memory surface locally. This server is
8
+ > the thin **remote adapter** over the same engine (`@urbicon-ui/design-engine`) and content
9
+ > bundle (`@urbicon-ui/design-content`), kept for the launch decision of hosting a public
10
+ > endpoint (evaluation/reach: "point your agent at Urbicon without installing anything").
11
+ > It is deliberately absent from the public docs until that endpoint exists, and a local
12
+ > install is **not** a supported consumer path (see docs/internal/DESIGN-MCP-V2.md §11).
13
+
14
+ **Transports:** stdio (default, for in-repo development) and streamable HTTP (the intended remote deployment).
6
15
 
7
16
  ## Installation
8
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/mcp-server",
3
- "version": "6.21.1",
3
+ "version": "6.21.3",
4
4
  "description": "Model Context Protocol server exposing the Urbicon UI component catalog, recipes and design intelligence to LLM agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -32,8 +32,8 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "^1.29.0",
35
- "@urbicon-ui/design-content": "6.21.1",
36
- "@urbicon-ui/design-engine": "6.21.1",
35
+ "@urbicon-ui/design-content": "6.21.3",
36
+ "@urbicon-ui/design-engine": "6.21.3",
37
37
  "zod": "^4.3.6"
38
38
  },
39
39
  "devDependencies": {
@@ -1,24 +1,17 @@
1
1
  import { readdir, readFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
3
  import { getDesignSystemDir } from '@urbicon-ui/design-content';
4
-
5
- export interface PatternEntry {
6
- name: string;
7
- title: string;
8
- description: string;
9
- content: string;
10
- }
11
-
12
- export const PRINCIPLE_TOPICS = [
13
- 'visual-hierarchy',
14
- 'interaction',
15
- 'component-selection',
16
- 'layout',
17
- 'accessibility',
18
- 'theming'
19
- ] as const;
20
-
21
- export type PrincipleTopic = (typeof PRINCIPLE_TOPICS)[number];
4
+ import { type PatternEntry, parsePatternEntry } from '@urbicon-ui/design-engine/reference';
5
+
6
+ // Parsing lives in the engine (shared with the `urbicon` CLI's `principles`/`pattern`
7
+ // commands, so local and remote slice the same files identically); this loader owns
8
+ // only the server-side I/O + caching.
9
+ export {
10
+ extractPrincipleSection,
11
+ type PatternEntry,
12
+ PRINCIPLE_TOPICS,
13
+ type PrincipleTopic
14
+ } from '@urbicon-ui/design-engine/reference';
22
15
 
23
16
  let cachedPrinciples: string | null = null;
24
17
  let cachedPatterns: PatternEntry[] | null = null;
@@ -60,11 +53,7 @@ export async function loadPatterns(): Promise<PatternEntry[]> {
60
53
  continue;
61
54
  }
62
55
 
63
- const name = file.replace(/\.md$/, '');
64
- const title = extractTitle(content);
65
- const description = extractDescription(content);
66
-
67
- entries.push({ name, title, description, content });
56
+ entries.push(parsePatternEntry(file.replace(/\.md$/, ''), content));
68
57
  }
69
58
 
70
59
  entries.sort((a, b) => a.name.localeCompare(b.name));
@@ -76,50 +65,3 @@ export async function getPatternByName(name: string): Promise<PatternEntry | nul
76
65
  const patterns = await loadPatterns();
77
66
  return patterns.find((p) => p.name === name) ?? null;
78
67
  }
79
-
80
- function extractTitle(content: string): string {
81
- const match = content.match(/^#\s+(.+)$/m);
82
- return match?.[1]?.trim() ?? '';
83
- }
84
-
85
- function extractDescription(content: string): string {
86
- const lines = content.split('\n');
87
- const titleIdx = lines.findIndex((l) => /^#\s+/.test(l));
88
- if (titleIdx === -1) return '';
89
-
90
- for (let i = titleIdx + 1; i < lines.length; i++) {
91
- const line = lines[i]?.trim();
92
- if (!line) continue;
93
- if (line.startsWith('#')) break;
94
- return line;
95
- }
96
- return '';
97
- }
98
-
99
- const TOPIC_HEADINGS: Record<PrincipleTopic, string> = {
100
- 'visual-hierarchy': '## Visual Hierarchy',
101
- interaction: '## Interaction',
102
- 'component-selection': '## Component Selection',
103
- layout: '## Layout',
104
- accessibility: '## Accessibility',
105
- theming: '## Theming'
106
- };
107
-
108
- export function extractPrincipleSection(content: string, topic: PrincipleTopic): string | null {
109
- const heading = TOPIC_HEADINGS[topic];
110
- if (!heading) return null;
111
-
112
- const lines = content.split('\n');
113
- const startIdx = lines.findIndex((l) => l.trim() === heading);
114
- if (startIdx === -1) return null;
115
-
116
- let endIdx = lines.length;
117
- for (let i = startIdx + 1; i < lines.length; i++) {
118
- if (/^## /.test(lines[i] ?? '')) {
119
- endIdx = i;
120
- break;
121
- }
122
- }
123
-
124
- return lines.slice(startIdx, endIdx).join('\n').trim();
125
- }
@@ -1,13 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import { getIconsPath } from '@urbicon-ui/design-content';
3
+ import type { IconEntry } from '@urbicon-ui/design-engine/search';
3
4
 
4
- export interface IconEntry {
5
- name: string;
6
- componentName: string;
7
- label: string;
8
- categories: string[];
9
- keywords: string[];
10
- }
5
+ // The schema is the engine's (shared with `urbicon icons`); this loader owns only
6
+ // the server-side I/O + caching.
7
+ export type { IconEntry } from '@urbicon-ui/design-engine/search';
11
8
 
12
9
  let cachedIcons: IconEntry[] | null = null;
13
10
 
@@ -1,17 +1,7 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { ICON_CATEGORY_ORDER } from '@urbicon-ui/design-engine/search';
2
3
  import { loadIcons } from '../data/icon-loader.js';
3
4
 
4
- const CATEGORY_ORDER = [
5
- 'navigation',
6
- 'action',
7
- 'status',
8
- 'media',
9
- 'communication',
10
- 'data',
11
- 'layout',
12
- 'toggle'
13
- ] as const;
14
-
15
5
  export function registerFindIconsTool(server: McpServer): void {
16
6
  server.tool(
17
7
  'find_icons',
@@ -59,7 +49,7 @@ export function registerFindIconsTool(server: McpServer): void {
59
49
  '**Props:** `size` (default 24), `strokeWidth` (default 2), `class`, `rotate`, `flip`, `animation`\n\n';
60
50
  md += '---\n\n';
61
51
 
62
- for (const cat of CATEGORY_ORDER) {
52
+ for (const cat of ICON_CATEGORY_ORDER) {
63
53
  const catIcons = byCategory.get(cat);
64
54
  if (!catIcons || catIcons.length === 0) continue;
65
55
 
@@ -1,446 +1,21 @@
1
1
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import {
3
+ CSS_REFERENCE_SECTION_NAMES,
4
+ renderCssReference
5
+ } from '@urbicon-ui/design-engine/reference';
2
6
  import { z } from 'zod';
3
7
 
4
- export const OVERVIEW = `# Urbicon UI CSS Design Tokens
5
-
6
- ## Architecture
7
- Three CSS layers, imported in order:
8
- 1. \`foundation.css\` — Raw OKLCH color scales (neutral, warm-neutral, primary, secondary, success, warning, danger, info)
9
- 2. \`semantic.css\` — Purpose-based tokens that reference foundation (\`--color-surface-base\`, \`--color-text-primary\`, etc.)
10
- 3. \`interaction.css\` — Animation timing, easing, shadows, focus rings
11
-
12
- ## Naming Convention
13
- ALL semantic tokens use the \`--color-*\` CSS variable prefix.
14
- Tailwind utilities map directly:
15
-
16
- | Tailwind Utility | CSS Variable |
17
- |---|---|
18
- | \`bg-surface-base\` | \`var(--color-surface-base)\` |
19
- | \`text-text-primary\` | \`var(--color-text-primary)\` |
20
- | \`border-border-default\` | \`var(--color-border-default)\` |
21
- | \`bg-primary\` | \`var(--color-primary)\` |
22
- | \`text-success\` | \`var(--color-success)\` |
23
-
24
- ## Theme Override
25
- Override tokens in your app CSS using Tailwind's \`@theme\` block:
26
- \`\`\`css
27
- @theme {
28
- --color-surface-base: #080818;
29
- --color-surface-elevated: #0e0e2a;
30
- --color-text-primary: #e0e0ff;
31
- }
32
- \`\`\`
33
-
34
- ## Dark Mode
35
- Mechanism: semantic tokens use the CSS \`light-dark()\` function; \`:root\` declares \`color-scheme: light dark\` so the browser resolves the matching branch automatically (following the OS \`prefers-color-scheme\`). No \`dark:\` overrides.
36
- Manual override: add a \`.light\` or \`.dark\` class to \`<html>\` (via the \`ThemeSwitcher\` component) — the class only flips \`color-scheme\`, and \`light-dark()\` re-resolves on its own.
37
-
38
- To override a token for ALL modes (light, dark, and manual overrides):
39
- \`\`\`css
40
- @theme {
41
- --color-surface-base: #080818;
42
- }
43
- :root, :root.light, :root.dark {
44
- --color-surface-base: #080818;
45
- }
46
- \`\`\`
47
- The \`@theme\` block sets the Tailwind utility value. The \`:root\` rule overrides the runtime value for all theme modes.
48
-
49
- ## Available Sections
50
- → \`get_css_reference(section="surfaces")\` — 11 surface background tokens
51
- → \`get_css_reference(section="text")\` — 9 text color tokens
52
- → \`get_css_reference(section="borders")\` — 5 border color tokens
53
- → \`get_css_reference(section="intents")\` — 6 component intents + the \`info\` status colour, feedback + interactive tokens
54
- → \`get_css_reference(section="shadows")\` — 5 shadow tokens + z-index scale
55
- → \`get_css_reference(section="theming")\` — How to create custom themes, available presets
56
- `;
57
-
58
- const SURFACES = `# Surface Tokens
59
-
60
- 11 tokens for background colors. All auto-switch in dark mode.
61
-
62
- | CSS Variable | Tailwind Utility | Purpose |
63
- |---|---|---|
64
- | \`--color-surface-base\` | \`bg-surface-base\` | Page background |
65
- | \`--color-surface-quiet\` | \`bg-surface-quiet\` | Softly tinted in-page zone (Lighter default) |
66
- | \`--color-surface-subtle\` | \`bg-surface-subtle\` | Subtle differentiation |
67
- | \`--color-surface-elevated\` | \`bg-surface-elevated\` | Cards, panels (floating with shadow) |
68
- | \`--color-surface-overlay\` | \`bg-surface-overlay\` | Modals, popovers |
69
- | \`--color-surface-interactive\` | \`bg-surface-interactive\` | Interactive element backgrounds |
70
- | \`--color-surface-hover\` | \`bg-surface-hover\` | Hover state |
71
- | \`--color-surface-active\` | \`bg-surface-active\` | Active/pressed state |
72
- | \`--color-surface-disabled\` | \`bg-surface-disabled\` | Disabled elements |
73
- | \`--color-surface-selected\` | \`bg-surface-selected\` | Selected items (uses primary-50) |
74
- | \`--color-surface-inverted\` | \`bg-surface-inverted\` | Inverted surfaces (tooltips) |
75
-
76
- Light → Dark mapping examples:
77
- - \`surface-base\`: neutral-0 (white) → neutral-900 (near-black)
78
- - \`surface-quiet\`: neutral-25 → neutral-850
79
- - \`surface-elevated\`: neutral-50 → neutral-800
80
- - \`surface-hover\`: neutral-100 → neutral-750
81
-
82
- Override example (dark neon theme):
83
- \`\`\`css
84
- @theme {
85
- --color-surface-base: #080818;
86
- --color-surface-quiet: #0a0a20;
87
- --color-surface-elevated: #0e0e2a;
88
- --color-surface-overlay: #12123a;
89
- }
90
- \`\`\`
91
- `;
92
-
93
- const TEXT = `# Text Tokens
94
-
95
- 9 tokens for text colors. All auto-switch in dark mode.
96
-
97
- | CSS Variable | Tailwind Utility | Purpose |
98
- |---|---|---|
99
- | \`--color-text-primary\` | \`text-text-primary\` | Main text |
100
- | \`--color-text-secondary\` | \`text-text-secondary\` | Supporting text |
101
- | \`--color-text-tertiary\` | \`text-text-tertiary\` | Muted text, metadata |
102
- | \`--color-text-quaternary\` | \`text-text-quaternary\` | Most subtle text |
103
- | \`--color-text-disabled\` | \`text-text-disabled\` | Disabled text |
104
- | \`--color-text-inverted\` | \`text-text-inverted\` | Text on inverted surfaces |
105
- | \`--color-text-on-primary\` | \`text-text-on-primary\` | Text on intent-colored backgrounds |
106
- | \`--color-text-on-dark\` | \`text-text-on-dark\` | Text on dark surfaces |
107
- | \`--color-text-on-surface\` | \`text-text-on-surface\` | Text on any surface (auto-contrast) |
108
-
109
- Light → Dark mapping:
110
- - \`text-primary\`: neutral-900 (dark) → neutral-100 (light)
111
- - \`text-secondary\`: neutral-700 → neutral-300
112
- - \`text-tertiary\`: neutral-500 → neutral-400
113
- `;
114
-
115
- const BORDERS = `# Border Tokens
116
-
117
- 5 tokens for border colors. All auto-switch in dark mode.
118
-
119
- | CSS Variable | Tailwind Utility | Purpose |
120
- |---|---|---|
121
- | \`--color-border-hairline\` | \`border-border-hairline\` | Faintest divider — translucent (alpha), not a neutral step |
122
- | \`--color-border-subtle\` | \`border-border-subtle\` | Gentle grouping |
123
- | \`--color-border-default\` | \`border-border-default\` | Standard borders |
124
- | \`--color-border-emphasis\` | \`border-border-emphasis\` | Emphasized borders |
125
- | \`--color-border-strong\` | \`border-border-strong\` | High-contrast borders |
126
-
127
- Light → Dark mapping:
128
- - \`border-hairline\`: black 8% → white 6% (translucent, blends onto any surface)
129
- - \`border-subtle\`: neutral-200 → neutral-700
130
- - \`border-default\`: neutral-300 → neutral-600
131
- - \`border-emphasis\`: neutral-400 → neutral-500
132
- - \`border-strong\`: neutral-500 → neutral-400
133
-
134
- Also available for intent-colored borders:
135
- \`border-primary\`, \`border-success\`, \`border-warning\`, \`border-danger\`, \`border-secondary\`, \`border-neutral\`
136
- `;
137
-
138
- const INTENTS = `# Intent Color System
139
-
140
- 6 component intents (primary, secondary, success, warning, danger, neutral) plus a status \`info\` colour — each a full palette with 5 semantic variants + 11 foundation steps.
141
-
142
- ## Semantic Intent Tokens (auto dark mode)
143
-
144
- Each intent has these variants (example: \`primary\`):
145
-
146
- | CSS Variable | Tailwind Utility | Purpose | Light | Dark |
147
- |---|---|---|---|---|
148
- | \`--color-primary\` | \`bg-primary\` / \`text-primary\` | Base intent color | primary-600 | primary-500 |
149
- | \`--color-primary-hover\` | \`bg-primary-hover\` | Hover state | primary-700 | primary-400 |
150
- | \`--color-primary-active\` | \`bg-primary-active\` | Pressed state | primary-800 | — |
151
- | \`--color-primary-subtle\` | \`bg-primary-subtle\` | Soft background | primary-50 | primary-900 |
152
- | \`--color-primary-emphasis\` | \`bg-primary-emphasis\` | Strong/dark variant | primary-900 | — |
153
-
154
- Same pattern applies to: \`success-*\`, \`warning-*\`, \`danger-*\`, \`secondary-*\`, \`neutral-*\`.
155
-
156
- \`info-*\` has the identical shape (\`--color-info\`, \`-hover\`, \`-active\`, \`-subtle\`, \`-emphasis\` → \`bg-info\`, \`text-info\`, \`bg-info-subtle\`, …) — the status/feedback blue (hue 220) behind Alert/Toast/Tooltip's info state, \`--color-feedback-info\`, and \`--color-chart-5\`. It is NOT in the global \`ComponentIntent\` union above: the feedback components with a built-in info state (Alert, Toast, Tooltip) do accept \`intent="info"\`, but generic components (Button, Badge, …) take the six-value union, so on those reach for the \`bg-info\`/\`text-info\` utilities rather than \`intent="info"\`.
157
-
158
- ## Foundation Intent Scales
159
-
160
- Each intent has 11 numbered steps (50–950) for granular control:
161
- \`\`\`
162
- --color-primary-50 through --color-primary-950
163
- --color-success-50 through --color-success-950
164
- --color-warning-50 through --color-warning-950
165
- --color-danger-50 through --color-danger-950
166
- --color-secondary-50 through --color-secondary-950
167
- --color-neutral-50 through --color-neutral-950
168
- --color-info-50 through --color-info-950
169
- \`\`\`
170
-
171
- Two ramps carry stops beyond the standard 50–950:
172
- - \`neutral\` adds finer steps — \`--color-neutral-0\` (pure white), \`-25\`, \`-650\`, \`-750\`, \`-850\` — so \`bg-neutral-650\` and friends are real tokens (they drive the surface/border light↔dark mappings), not hallucinations.
173
- - \`--color-warm-neutral-50\` through \`-950\` is a separate warm-tinted greyscale ramp; the \`neutral\` intent borrows its lightness profile (see the theming section) rather than exposing it as a surface.
174
-
175
- Tailwind usage: \`bg-primary-500\`, \`text-danger-700\`, \`border-success-300\`, etc.
176
-
177
- ## Feedback Tokens (for status messages)
178
-
179
- | CSS Variable | Tailwind Utility | Maps to |
180
- |---|---|---|
181
- | \`--color-feedback-info\` | \`bg-feedback-info\` / \`text-feedback-info\` | info-500 |
182
- | \`--color-feedback-info-subtle\` | \`bg-feedback-info-subtle\` | info-50 |
183
- | \`--color-feedback-success\` | \`bg-feedback-success\` / \`text-feedback-success\` | success-500 |
184
- | \`--color-feedback-success-subtle\` | \`bg-feedback-success-subtle\` | success-50 |
185
- | \`--color-feedback-warning\` | \`bg-feedback-warning\` / \`text-feedback-warning\` | warning-500 |
186
- | \`--color-feedback-warning-subtle\` | \`bg-feedback-warning-subtle\` | warning-50 |
187
- | \`--color-feedback-error\` | \`bg-feedback-error\` / \`text-feedback-error\` | danger-500 |
188
- | \`--color-feedback-error-subtle\` | \`bg-feedback-error-subtle\` | danger-50 |
189
-
190
- ## Interactive Tokens
191
-
192
- | CSS Variable | Tailwind Utility | Purpose |
193
- |---|---|---|
194
- | \`--color-interactive-hover\` | \`bg-interactive-hover\` | 10% primary overlay |
195
- | \`--color-interactive-active\` | \`bg-interactive-active\` | 20% primary overlay |
196
- | \`--color-interactive-focus\` | \`ring-interactive-focus\` | Focus ring color (primary-500) |
197
- | \`--color-interactive-disabled\` | \`bg-interactive-disabled\` | Disabled state (neutral-200) |
198
-
199
- ## Override Example: Custom Intent
200
- \`\`\`css
201
- @theme {
202
- --color-primary-50: oklch(0.95 0.03 280);
203
- --color-primary-100: oklch(0.9 0.06 280);
204
- --color-primary-200: oklch(0.82 0.09 280);
205
- --color-primary-300: oklch(0.74 0.12 280);
206
- --color-primary-400: oklch(0.66 0.14 280);
207
- --color-primary-500: oklch(0.58 0.14 280);
208
- --color-primary-600: oklch(0.52 0.14 280);
209
- --color-primary-700: oklch(0.44 0.12 280);
210
- --color-primary-800: oklch(0.36 0.1 280);
211
- --color-primary-900: oklch(0.28 0.07 280);
212
- --color-primary-950: oklch(0.18 0.04 280);
213
- }
214
- \`\`\`
215
- Semantic tokens (\`--color-primary\`, \`--color-primary-hover\`, etc.) automatically reference the new scale.
216
- `;
217
-
218
- const SHADOWS = `# Shadow & Z-Index Tokens
219
-
220
- ## Shadow Tokens
221
-
222
- | CSS Variable | Tailwind Utility | Purpose |
223
- |---|---|---|
224
- | \`--color-shadow-xs\` | \`shadow-[var(--blocks-shadow-xs)]\` | Minimal shadow |
225
- | \`--color-shadow-sm\` | \`shadow-[var(--blocks-shadow-sm)]\` | Buttons, small elements |
226
- | \`--color-shadow-base\` | \`shadow-[var(--blocks-shadow-base)]\` | Default elevation |
227
- | \`--color-shadow-md\` | \`shadow-[var(--blocks-shadow-md)]\` | Hover states, menus |
228
- | \`--color-shadow-lg\` | \`shadow-[var(--blocks-shadow-lg)]\` | Modals, popovers |
229
-
230
- Shadows automatically increase opacity in dark mode (0.05 → 0.2 for xs, etc.).
231
-
232
- ## Z-Index Scale
233
-
234
- | CSS Variable | Value | Purpose |
235
- |---|---|---|
236
- | \`--z-hide\` | -1 | Hidden |
237
- | \`--z-base\` | 0 | Default |
238
- | \`--z-docked\` | 10 | Docked elements |
239
- | \`--z-sticky\` | 1100 | Sticky headers |
240
- | \`--z-dropdown\` | 1150 | Menus, listboxes, floating panels (above sticky, below banner) |
241
- | \`--z-banner\` | 1200 | Banners |
242
- | \`--z-overlay\` | 1300 | Overlays |
243
- | \`--z-sidebar\` | 1350 | Sidebars |
244
- | \`--z-modal\` | 1400 | Modals |
245
- | \`--z-popover\` | 1500 | Popovers |
246
- | \`--z-toast\` | 1700 | Toasts |
247
- | \`--z-tooltip\` | 1800 | Tooltips |
248
-
249
- Usage: \`z-[var(--z-modal)]\`
250
-
251
- ## Border Radius Scale
252
-
253
- | CSS Variable | Tailwind | Value |
254
- |---|---|---|
255
- | \`--radius-xs\` | \`rounded-xs\` | 0.125rem |
256
- | \`--radius-sm\` | \`rounded-sm\` | 0.25rem |
257
- | \`--radius-md\` | \`rounded-md\` | 0.375rem |
258
- | \`--radius-lg\` | \`rounded-lg\` | 0.5rem |
259
- | \`--radius-xl\` | \`rounded-xl\` | 0.75rem |
260
- | \`--radius-2xl\` | \`rounded-2xl\` | 1rem |
261
- | \`--radius-3xl\` | \`rounded-3xl\` | 1.5rem |
262
- | \`--radius-4xl\` | \`rounded-4xl\` | 2rem |
263
-
264
- ## Duration & Easing Tokens
265
-
266
- | CSS Variable | Value |
267
- |---|---|
268
- | \`--blocks-duration-instant\` | 75ms |
269
- | \`--blocks-duration-fast\` | 150ms |
270
- | \`--blocks-duration-normal\` | 250ms |
271
- | \`--blocks-duration-slow\` | 350ms |
272
- | \`--blocks-duration-slower\` | 500ms |
273
- | \`--blocks-ease-confident\` | Standard transitions |
274
- | \`--blocks-ease-springy\` | Bouncy animations |
275
- | \`--blocks-ease-smooth\` | Gentle animations |
276
- | \`--blocks-ease-snappy\` | Quick, decisive |
277
- `;
278
-
279
- const THEMING = `# Theming Guide
280
-
281
- ## Built-in Themes
282
-
283
- Import AFTER base styles:
284
- \`\`\`css
285
- @import '@urbicon-ui/blocks/style/index.css';
286
- @import '@urbicon-ui/blocks/style/themes/ocean.css';
287
- \`\`\`
288
-
289
- Available: \`ocean.css\` (blue-teal), \`forest.css\` (green), \`sunset.css\` (orange-amber), \`rose.css\` (pink), \`neutral.css\` (grayscale)
290
-
291
- Each theme overrides three foundation ramps: \`--color-primary-*\`, \`--color-secondary-*\`, and \`--color-neutral-*\`. The neutral ramp is the chassis — surfaces, text and borders all derive from it (see below), so each theme re-tints it to match the accent's temperature (Sunset shifts it warm, Ocean cool). Each colored theme additionally re-tunes only the intent ramps that would collide with its accent (e.g. Forest pushes success/warning off its green/lime, Ocean moves info off the brand blue), sets a matching \`--blocks-shadow-tint\`, and re-tints the NEUTRAL intent (\`bg-neutral\` / \`text-neutral\` / neutral borders) to its temperature via \`--neutral-chrome-hue\`. Other non-colliding intents (success/warning/danger/info) stay at the library defaults. The Neutral theme inherits the cool default chrome and leaves those intents + shadow tint untouched.
292
-
293
- The neutral intent is special: it keeps the \`--color-warm-neutral-*\` ramp's lightness profile (tuned so white-on-\`bg-neutral\` AND \`text-neutral\`-on-surface both stay legible across light/dark) and only swaps the hue via \`--neutral-chrome-hue\`. Set that token (default 240) in a \`:root\` rule to re-tint neutral controls without touching contrast — e.g. \`:root { --neutral-chrome-hue: 50; }\` for a warm theme. Do NOT repoint the neutral intent onto the chassis \`--color-neutral-*\` ramp: its surface-tuned lightness breaks the white-text / neutral-text contrast balance.
294
-
295
- ## The neutral ramp IS the chassis — match its temperature to your accent
296
-
297
- This is the single most common theming mistake: recolor \`--color-primary-*\` only, ship it, and the warm brand button ends up sitting on cool blue-grey cards. Reason: \`surface-*\`, \`text-*\` and \`border-*\` do NOT derive from primary — they derive from \`--color-neutral-*\`, which defaults to a cool Hue 240. A warm accent on a Hue-240 chassis reads broken.
298
-
299
- Fix: when you change the accent hue, re-tint the neutral ramp to the same temperature family. Keep each stop's lightness and chroma identical to the foundation ramp (chroma stays tiny, ≤0.017) and shift only the hue — this keeps the chassis near-grey, preserves WCAG contrast, and the warmth/coolness flows automatically to every surface, text and border token via \`semantic.css\`. No per-token surface overrides needed.
300
-
301
- ## Creating a Custom Theme
302
-
303
- Pick an accent hue (0–360) and a chassis hue in the same temperature family (often the accent hue itself, or pulled slightly toward grey). Generate matched OKLCH ramps — the Theme Builder at \`/customization/theme-builder\` does this for you:
304
- \`\`\`css
305
- /* my-theme.css — warm brand on a warm chassis */
306
- @theme {
307
- /* Primary: hue 280 (purple) */
308
- --color-primary-50: oklch(0.95 0.03 280);
309
- --color-primary-100: oklch(0.9 0.06 280);
310
- --color-primary-200: oklch(0.82 0.09 280);
311
- --color-primary-300: oklch(0.74 0.12 280);
312
- --color-primary-400: oklch(0.66 0.14 280);
313
- --color-primary-500: oklch(0.58 0.14 280);
314
- --color-primary-600: oklch(0.52 0.14 280);
315
- --color-primary-700: oklch(0.44 0.12 280);
316
- --color-primary-800: oklch(0.36 0.1 280);
317
- --color-primary-900: oklch(0.28 0.07 280);
318
- --color-primary-950: oklch(0.18 0.04 280);
319
-
320
- /* Secondary: hue 320 (magenta) */
321
- --color-secondary-50: oklch(0.95 0.03 320);
322
- /* ... same pattern with hue 320 ... */
323
- --color-secondary-950: oklch(0.18 0.04 320);
324
-
325
- /* Chassis: neutral ramp re-tinted to hue 290 (matches the purple accent).
326
- Same L + C as the foundation neutral ramp — only the hue moves. */
327
- --color-neutral-25: oklch(0.99 0.002 290);
328
- --color-neutral-50: oklch(0.98 0.005 290);
329
- --color-neutral-100: oklch(0.95 0.008 290);
330
- --color-neutral-200: oklch(0.89 0.012 290);
331
- --color-neutral-300: oklch(0.83 0.014 290);
332
- --color-neutral-400: oklch(0.7 0.015 290);
333
- --color-neutral-500: oklch(0.55 0.016 290);
334
- --color-neutral-600: oklch(0.42 0.017 290);
335
- --color-neutral-650: oklch(0.38 0.016 290);
336
- --color-neutral-700: oklch(0.32 0.016 290);
337
- --color-neutral-750: oklch(0.28 0.014 290);
338
- --color-neutral-800: oklch(0.23 0.015 290);
339
- --color-neutral-850: oklch(0.18 0.014 290);
340
- --color-neutral-900: oklch(0.15 0.012 290);
341
- --color-neutral-950: oklch(0.08 0.008 290);
342
- }
343
- \`\`\`
344
- For a temperature-free, true grayscale chassis (content-focused UIs), set chroma to 0 on every neutral stop instead of shifting the hue.
345
-
346
- ## Intent-hue collisions
347
-
348
- If your accent hue lands near an intent hue, the two become hard to tell apart. The library intents sit at: success 140, warning 80, danger 25, info 220, secondary 280. A green brand (~140) collides with success; an amber brand (~80) collides with warning. When that happens, re-tune the colliding intent ramp away from the accent (push the hue ±15–25° and/or drop its lightness so it reads as "status", not "brand"). \`apps/docs/src/lib/style/rooms-docs.css\` is a worked example (green brand → success pushed to 150 + darkened, warning pulled to amber 55).
349
-
350
- ## Overriding Semantic Tokens
351
-
352
- To override semantic tokens directly (e.g. a fully art-directed dark theme, or custom surface stops beyond what re-tinting the neutral ramp gives you):
353
- \`\`\`css
354
- @theme {
355
- --color-surface-base: #080818;
356
- --color-surface-elevated: #0e0e2a;
357
- --color-text-primary: #e0e0ff;
358
- }
359
- \`\`\`
360
-
361
- If your override should apply regardless of light/dark mode, also set:
362
- \`\`\`css
363
- :root, :root.light, :root.dark {
364
- --color-surface-base: #080818;
365
- --color-surface-elevated: #0e0e2a;
366
- --color-text-primary: #e0e0ff;
367
- }
368
- \`\`\`
369
-
370
- ## Gotcha: SCOPED themes must re-declare derived tokens
371
-
372
- This trap only applies when you scope a theme to a CLASS (e.g. \`.theme-sunset { ... }\` toggled at runtime) instead of a global \`@theme\`/\`:root\` block.
373
-
374
- Custom-property substitution happens per element at computed-value time, and inheritance passes the ALREADY-substituted value. \`semantic.css\` defines derived tokens at \`:root\`, e.g.
375
- \`\`\`css
376
- :root { --color-primary: light-dark(var(--color-primary-600), var(--color-primary-500)); }
377
- \`\`\`
378
- That \`var(--color-primary-600)\` resolves against the \`:root\` ramp. If you override \`--color-primary-600\` only inside \`.theme-sunset\`, the element inherits the already-resolved (default) \`--color-primary\` — your new ramp is ignored for every derived token.
379
-
380
- Fix: inside the scoped block, re-declare the derived tokens too, so substitution re-runs at that level:
381
- \`\`\`css
382
- .theme-sunset {
383
- --color-primary-600: oklch(0.55 0.15 55);
384
- /* ...rest of the ramp... */
385
-
386
- /* re-declare so the derived tokens re-substitute against the new ramp */
387
- --color-primary: light-dark(var(--color-primary-600), var(--color-primary-500));
388
- --color-primary-hover: light-dark(var(--color-primary-700), var(--color-primary-400));
389
- --color-primary-subtle: light-dark(var(--color-primary-50), var(--color-primary-900));
390
- /* ...and the same for any neutral-derived surface/text/border tokens you rely on */
391
- }
392
- \`\`\`
393
- A global \`@theme\` block (the built-in themes, the Theme Builder output) does NOT hit this — everything lands on \`:root\`, the same element where the derived tokens compute, so re-declaration is unnecessary. Prefer global themes unless you genuinely need multiple themes live on one page. \`apps/docs/src/lib/style/rooms-docs.css\` is the canonical scoped example.
394
-
395
- ## Component-Level Overrides
396
-
397
- Use \`BlocksProvider\` to style components project-wide — unconditional \`defaults\`, named \`presets\` (opt-in via the \`preset\` prop), and prop-conditional \`overrides\`:
398
- \`\`\`svelte
399
- <BlocksProvider defaults={{
400
- Card: { slotClasses: { base: 'rounded-2xl shadow-lg' } },
401
- Button: { slotClasses: { base: 'rounded-full font-bold uppercase' } },
402
- // prop-conditional: style ONLY the outlined variant — what an unconditional slotClasses cannot express
403
- Badge: { overrides: [{ variant: 'outlined', class: { base: 'border' } }] }
404
- }}>
405
- \`\`\`
406
- Cascade (conflict-resolved per Tailwind bucket, later wins): \`defaults.slotClasses → defaults.overrides → preset.slotClasses → preset.overrides → instance slotClasses → instance class\`.
407
-
408
- Or override per-instance:
409
- \`\`\`svelte
410
- <Card class="rounded-2xl" padding="lg">...</Card>
411
- <Button unstyled slotClasses={{ base: 'custom-button-class' }}>...</Button>
412
- \`\`\`
413
-
414
- ## Tailwind 4 — Scanning Component Classes
415
-
416
- Tailwind 4 does not scan \`node_modules\` by default, so the responsive utilities (\`lg:hidden\`, \`md:grid-cols-2\`, etc.) used inside Urbicon UI components must be registered as content sources. **The library does this for you:** \`@urbicon-ui/blocks/style/index.css\` ships the \`@source\` directives that point Tailwind at the component classes, and \`@urbicon-ui/table/style/index.css\` does the same for the Table.
417
-
418
- So the only requirement is to import \`index.css\` (your app owns the Tailwind import; it comes first):
419
- \`\`\`css
420
- @import 'tailwindcss';
421
- @import '@urbicon-ui/blocks/style/index.css'; /* tokens + @source directives */
422
- @import '@urbicon-ui/table/style/index.css'; /* if using Table */
423
- \`\`\`
424
-
425
- **Do NOT add manual \`@source\` directives, and do NOT import the \`foundation\`/\`semantic\`/\`interaction\` subfiles instead of \`index.css\`** — the subfiles omit the \`@source\` directives (and global classes), which is the usual cause of "responsive layouts break in production".
426
- `;
427
-
428
- export const SECTIONS: Record<string, string> = {
429
- surfaces: SURFACES,
430
- text: TEXT,
431
- borders: BORDERS,
432
- intents: INTENTS,
433
- shadows: SHADOWS,
434
- theming: THEMING
435
- };
436
-
8
+ // The reference text lives in `@urbicon-ui/design-engine/reference`, shared with the
9
+ // `urbicon css-reference` CLI command so local and remote answers agree; this tool is
10
+ // only the MCP facade. Drift against the real blocks CSS is guarded by the engine's
11
+ // `css-reference.test.ts`.
437
12
  export function registerGetCssReferenceTool(server: McpServer): void {
438
13
  server.tool(
439
14
  'get_css_reference',
440
15
  'Get CSS variable names, Tailwind utility mappings, and override patterns for the Urbicon UI design token system. Essential for theming and custom styling.',
441
16
  {
442
17
  section: z
443
- .enum(['surfaces', 'text', 'borders', 'intents', 'shadows', 'theming'])
18
+ .enum(CSS_REFERENCE_SECTION_NAMES)
444
19
  .optional()
445
20
  .describe(
446
21
  'Token category. Omit for overview with naming conventions and dark mode mechanism.'
@@ -448,10 +23,8 @@ export function registerGetCssReferenceTool(server: McpServer): void {
448
23
  },
449
24
  { readOnlyHint: true },
450
25
  async ({ section }) => {
451
- const text = section ? SECTIONS[section] : OVERVIEW;
452
-
453
26
  return {
454
- content: [{ type: 'text' as const, text: text ?? OVERVIEW }]
27
+ content: [{ type: 'text' as const, text: renderCssReference(section) }]
455
28
  };
456
29
  }
457
30
  );
@@ -1,100 +0,0 @@
1
- import { existsSync, readFileSync } from 'node:fs';
2
- import { dirname, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { describe, expect, it } from 'vitest';
5
- import { OVERVIEW, SECTIONS } from './get-css-reference.js';
6
-
7
- /**
8
- * Drift guard for the hand-maintained CSS token reference.
9
- *
10
- * `get_css_reference` inlines its token tables as TS strings — the MCP server
11
- * ships standalone (no blocks CSS at runtime), the same constraint that keeps
12
- * design-engine's `VALID_TOKEN_CORES` inline. The hazard of any hand-copied list
13
- * is silent drift: `--color-border-hairline` was added to the CSS and went
14
- * unmirrored here for a while. When the blocks CSS is present (i.e. running
15
- * in-repo) we re-derive the semantic surface / text / border token cores and
16
- * assert each is documented, so a newly added token can no longer disappear.
17
- *
18
- * Scope: the three families `get_css_reference` enumerates exhaustively (one row
19
- * per token), plus a lighter check that every intent is at least *named* (its base
20
- * `--color-<intent>` token or a `bg-<intent>` utility appears in the prose) — the
21
- * F-B drift where a whole intent (`info`, a real ramp behind `bg-info` /
22
- * `--color-feedback-info`) went undocumented while the six others were listed. It
23
- * deliberately does NOT require every intent scale step (`primary-50 … primary-950`,
24
- * documented via shorthand), feedback/interactive, chart, or internal-only token
25
- * (e.g. `skeleton-shimmer`, used by the Skeleton wave, never a consumer utility) to
26
- * be spelled out. Whole-set token validity is already guarded by design-engine's
27
- * `tokens.test.ts`.
28
- */
29
-
30
- const __dirname = dirname(fileURLToPath(import.meta.url));
31
- const semantic = resolve(
32
- __dirname,
33
- '..',
34
- '..',
35
- '..',
36
- 'blocks',
37
- 'src',
38
- 'lib',
39
- 'style',
40
- 'semantic.css'
41
- );
42
- const cssAvailable = existsSync(semantic);
43
-
44
- const ALL_CONTENT = [OVERVIEW, ...Object.values(SECTIONS)].join('\n');
45
-
46
- /** Families `get_css_reference` tables exhaustively, with the prose count to verify. */
47
- const TABLED_FAMILIES = [
48
- { family: 'surface', section: SECTIONS.surfaces! },
49
- { family: 'text', section: SECTIONS.text! },
50
- { family: 'border', section: SECTIONS.borders! }
51
- ] as const;
52
-
53
- /** Unique semantic `--color-<family>-*` cores in the CSS (scoped re-declarations collapse). */
54
- function deriveSemanticCores(family: string): string[] {
55
- const css = readFileSync(semantic, 'utf-8');
56
- const re = new RegExp(`--color-(${family}-[a-z-]+)\\s*:`, 'g');
57
- const cores = new Set<string>();
58
- for (const m of css.matchAll(re)) cores.add(m[1]!);
59
- return [...cores].sort();
60
- }
61
-
62
- /** Intent names from the `=== X INTENT ===` section markers in the CSS — robust to
63
- * the multi-line `--color-neutral` definition a self-referential regex would miss. */
64
- function deriveIntents(): string[] {
65
- const css = readFileSync(semantic, 'utf-8');
66
- const cores = new Set<string>();
67
- for (const m of css.matchAll(/=== ([A-Z-]+) INTENT ===/g)) cores.add(m[1]!.toLowerCase());
68
- return [...cores].sort();
69
- }
70
-
71
- describe.skipIf(!cssAvailable)('get_css_reference token drift guard', () => {
72
- for (const { family } of TABLED_FAMILIES) {
73
- it(`documents every semantic \`${family}-*\` token defined in the CSS`, () => {
74
- const missing = deriveSemanticCores(family).filter((c) => !ALL_CONTENT.includes(c));
75
- expect(
76
- missing,
77
- `Semantic ${family} tokens in the CSS but absent from get_css_reference: ${missing.join(', ')}`
78
- ).toEqual([]);
79
- });
80
- }
81
-
82
- for (const { family, section } of TABLED_FAMILIES) {
83
- it(`states the correct ${family}-token count`, () => {
84
- const stated = Number(section.match(/(\d+)\s+tokens for/)?.[1]);
85
- expect(stated, 'prose count drifted from the real token count').toBe(
86
- deriveSemanticCores(family).length
87
- );
88
- });
89
- }
90
-
91
- it('names every intent defined in the CSS (base token or utility)', () => {
92
- const missing = deriveIntents().filter(
93
- (c) => !ALL_CONTENT.includes(`--color-${c}`) && !ALL_CONTENT.includes(`bg-${c}`)
94
- );
95
- expect(
96
- missing,
97
- `Intents in the CSS but absent from get_css_reference: ${missing.join(', ')}`
98
- ).toEqual([]);
99
- });
100
- });