@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 +12 -3
- package/package.json +3 -3
- package/src/data/design-system-loader.ts +12 -70
- package/src/data/icon-loader.ts +4 -7
- package/src/tools/find-icons.ts +2 -12
- package/src/tools/get-css-reference.ts +10 -437
- package/src/tools/get-css-reference.test.ts +0 -100
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
|
|
4
|
-
|
|
5
|
-
**
|
|
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.
|
|
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.
|
|
36
|
-
"@urbicon-ui/design-engine": "6.21.
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
}
|
package/src/data/icon-loader.ts
CHANGED
|
@@ -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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
package/src/tools/find-icons.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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(
|
|
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:
|
|
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
|
-
});
|