@xferops/design-guide 0.2.1

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 ADDED
@@ -0,0 +1,30 @@
1
+ # @xferops/design-guide
2
+
3
+ MCP server and query helpers for XferOps frontend design guidance.
4
+
5
+ ## What It Provides
6
+
7
+ - MCP tools for searching and reading guides
8
+ - MCP resources with stable URIs like `guide://table/sorting`
9
+ - Bundled markdown guide content that ships with the package
10
+ - Coverage for app setup, token theming, layout/content primitives, forms, navigation, overlays, feedback states, and table patterns
11
+
12
+ ## Local Usage
13
+
14
+ ```bash
15
+ pnpm --filter @xferops/design-guide build
16
+ node packages/design-guide/dist/server.js
17
+ ```
18
+
19
+ ## MCP Client Example
20
+
21
+ ```json
22
+ {
23
+ "mcpServers": {
24
+ "xferops-design-guide": {
25
+ "command": "npx",
26
+ "args": ["-y", "@xferops/design-guide"]
27
+ }
28
+ }
29
+ }
30
+ ```
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/guides/frontmatter.ts
4
+ function parseFrontmatter(markdown) {
5
+ const match = markdown.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
6
+ if (!match) {
7
+ throw new Error("Guide file is missing frontmatter.");
8
+ }
9
+ const [, frontmatterBlock, content] = match;
10
+ const metadata = parseFrontmatterBlock(frontmatterBlock);
11
+ validateMetadata(metadata);
12
+ return {
13
+ metadata,
14
+ content: content.trimStart()
15
+ };
16
+ }
17
+ function parseFrontmatterBlock(block) {
18
+ const result = {};
19
+ const lines = block.split("\n");
20
+ for (let index = 0; index < lines.length; index += 1) {
21
+ const line = lines[index];
22
+ if (!line.trim()) continue;
23
+ const keyMatch = line.match(/^([A-Za-z0-9]+):(?:\s*(.*))?$/);
24
+ if (!keyMatch) {
25
+ throw new Error(`Unsupported frontmatter line: ${line}`);
26
+ }
27
+ const [, key, rawValue = ""] = keyMatch;
28
+ if (rawValue.trim()) {
29
+ result[key] = rawValue.trim();
30
+ continue;
31
+ }
32
+ const values = [];
33
+ while (index + 1 < lines.length && /^\s*-\s+/.test(lines[index + 1])) {
34
+ index += 1;
35
+ values.push(lines[index].replace(/^\s*-\s+/, "").trim());
36
+ }
37
+ result[key] = values;
38
+ }
39
+ return {
40
+ id: getString(result.id, "id"),
41
+ title: getString(result.title, "title"),
42
+ summary: getString(result.summary, "summary"),
43
+ category: getString(result.category, "category"),
44
+ slug: getString(result.slug, "slug"),
45
+ tags: getArray(result.tags, "tags"),
46
+ sourcePath: typeof result.sourcePath === "string" ? result.sourcePath : void 0,
47
+ relatedPaths: getArray(result.relatedPaths, "relatedPaths")
48
+ };
49
+ }
50
+ function getString(value, key) {
51
+ if (typeof value === "string" && value.length > 0) {
52
+ return value;
53
+ }
54
+ if (value == null) {
55
+ return void 0;
56
+ }
57
+ throw new Error(`Frontmatter key "${key}" must be a string.`);
58
+ }
59
+ function getArray(value, key) {
60
+ if (Array.isArray(value) && value.every((item) => typeof item === "string")) {
61
+ return value;
62
+ }
63
+ if (value == null) {
64
+ return void 0;
65
+ }
66
+ throw new Error(`Frontmatter key "${key}" must be a string array.`);
67
+ }
68
+ function validateMetadata(metadata) {
69
+ const requiredKeys = ["id", "title", "summary", "category", "slug", "tags"];
70
+ for (const key of requiredKeys) {
71
+ if (metadata[key] == null || Array.isArray(metadata[key]) && metadata[key].length === 0) {
72
+ throw new Error(`Guide frontmatter is missing "${key}".`);
73
+ }
74
+ }
75
+ }
76
+
77
+ // src/guides/catalog.ts
78
+ import { readdir, readFile } from "fs/promises";
79
+ import { dirname, resolve } from "path";
80
+ import { fileURLToPath } from "url";
81
+ var currentDir = dirname(fileURLToPath(import.meta.url));
82
+ var guidesDir = resolve(currentDir, "../../guides");
83
+ var guideCache = null;
84
+ async function listGuides() {
85
+ if (!guideCache) {
86
+ guideCache = loadGuides();
87
+ }
88
+ return guideCache;
89
+ }
90
+ async function getGuideById(id) {
91
+ const guides = await listGuides();
92
+ return guides.find((guide) => guide.id === id);
93
+ }
94
+ async function getGuideByUri(uri) {
95
+ const guides = await listGuides();
96
+ return guides.find((guide) => guide.uri === uri);
97
+ }
98
+ async function loadGuides() {
99
+ const fileNames = (await readdir(guidesDir)).filter((name) => name.endsWith(".md")).sort((left, right) => left.localeCompare(right));
100
+ return Promise.all(
101
+ fileNames.map(async (fileName) => {
102
+ const relativePath = fileName;
103
+ const markdown = await readFile(resolve(guidesDir, relativePath), "utf8");
104
+ const { metadata, content } = parseFrontmatter(markdown);
105
+ return {
106
+ ...metadata,
107
+ uri: `guide://${metadata.category}/${metadata.slug}`,
108
+ relativePath,
109
+ content
110
+ };
111
+ })
112
+ );
113
+ }
114
+
115
+ // src/guides/search.ts
116
+ async function searchGuides(query) {
117
+ const guides = await listGuides();
118
+ const normalized = query.trim().toLowerCase();
119
+ if (!normalized) {
120
+ return guides;
121
+ }
122
+ const terms = normalized.split(/\s+/).filter(Boolean);
123
+ return guides.map((guide) => ({
124
+ guide,
125
+ score: scoreGuide(guide, terms)
126
+ })).filter((entry) => entry.score > 0).sort((left, right) => right.score - left.score || left.guide.title.localeCompare(right.guide.title)).map((entry) => entry.guide);
127
+ }
128
+ function scoreGuide(guide, terms) {
129
+ const haystacks = [
130
+ guide.title.toLowerCase(),
131
+ guide.summary.toLowerCase(),
132
+ guide.category.toLowerCase(),
133
+ guide.slug.toLowerCase(),
134
+ guide.id.toLowerCase(),
135
+ guide.uri.toLowerCase(),
136
+ guide.tags.join(" ").toLowerCase(),
137
+ guide.content.toLowerCase()
138
+ ];
139
+ return terms.reduce((score, term) => {
140
+ let termScore = 0;
141
+ for (const haystack of haystacks) {
142
+ if (haystack === term) {
143
+ termScore = Math.max(termScore, 8);
144
+ } else if (haystack.startsWith(term)) {
145
+ termScore = Math.max(termScore, 5);
146
+ } else if (haystack.includes(term)) {
147
+ termScore = Math.max(termScore, 2);
148
+ }
149
+ }
150
+ return score + termScore;
151
+ }, 0);
152
+ }
153
+
154
+ export {
155
+ parseFrontmatter,
156
+ listGuides,
157
+ getGuideById,
158
+ getGuideByUri,
159
+ searchGuides
160
+ };
161
+ //# sourceMappingURL=chunk-R4AEJOIP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/guides/frontmatter.ts","../src/guides/catalog.ts","../src/guides/search.ts"],"sourcesContent":["export type GuideMetadata = {\n id: string;\n title: string;\n summary: string;\n category: string;\n slug: string;\n tags: string[];\n sourcePath?: string;\n relatedPaths?: string[];\n};\n\nexport function parseFrontmatter(markdown: string): {\n metadata: GuideMetadata;\n content: string;\n} {\n const match = markdown.match(/^---\\n([\\s\\S]*?)\\n---\\n?([\\s\\S]*)$/);\n\n if (!match) {\n throw new Error(\"Guide file is missing frontmatter.\");\n }\n\n const [, frontmatterBlock, content] = match;\n const metadata = parseFrontmatterBlock(frontmatterBlock);\n\n validateMetadata(metadata);\n\n return {\n metadata: metadata as GuideMetadata,\n content: content.trimStart()\n };\n}\n\nfunction parseFrontmatterBlock(block: string): Partial<GuideMetadata> {\n const result: Record<string, string | string[]> = {};\n const lines = block.split(\"\\n\");\n\n for (let index = 0; index < lines.length; index += 1) {\n const line = lines[index];\n\n if (!line.trim()) continue;\n\n const keyMatch = line.match(/^([A-Za-z0-9]+):(?:\\s*(.*))?$/);\n\n if (!keyMatch) {\n throw new Error(`Unsupported frontmatter line: ${line}`);\n }\n\n const [, key, rawValue = \"\"] = keyMatch;\n\n if (rawValue.trim()) {\n result[key] = rawValue.trim();\n continue;\n }\n\n const values: string[] = [];\n\n while (index + 1 < lines.length && /^\\s*-\\s+/.test(lines[index + 1])) {\n index += 1;\n values.push(lines[index].replace(/^\\s*-\\s+/, \"\").trim());\n }\n\n result[key] = values;\n }\n\n return {\n id: getString(result.id, \"id\"),\n title: getString(result.title, \"title\"),\n summary: getString(result.summary, \"summary\"),\n category: getString(result.category, \"category\"),\n slug: getString(result.slug, \"slug\"),\n tags: getArray(result.tags, \"tags\"),\n sourcePath: typeof result.sourcePath === \"string\" ? result.sourcePath : undefined,\n relatedPaths: getArray(result.relatedPaths, \"relatedPaths\")\n };\n}\n\nfunction getString(value: unknown, key: string): string | undefined {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n\n if (value == null) {\n return undefined;\n }\n\n throw new Error(`Frontmatter key \"${key}\" must be a string.`);\n}\n\nfunction getArray(value: unknown, key: string): string[] | undefined {\n if (Array.isArray(value) && value.every((item) => typeof item === \"string\")) {\n return value;\n }\n\n if (value == null) {\n return undefined;\n }\n\n throw new Error(`Frontmatter key \"${key}\" must be a string array.`);\n}\n\nfunction validateMetadata(metadata: Partial<GuideMetadata>): asserts metadata is GuideMetadata {\n const requiredKeys = [\"id\", \"title\", \"summary\", \"category\", \"slug\", \"tags\"] as const;\n\n for (const key of requiredKeys) {\n if (metadata[key] == null || (Array.isArray(metadata[key]) && metadata[key].length === 0)) {\n throw new Error(`Guide frontmatter is missing \"${key}\".`);\n }\n }\n}\n","import { readdir, readFile } from \"node:fs/promises\";\nimport { dirname, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseFrontmatter, type GuideMetadata } from \"./frontmatter\";\n\nexport type GuideRecord = GuideMetadata & {\n uri: string;\n relativePath: string;\n content: string;\n};\n\nconst currentDir = dirname(fileURLToPath(import.meta.url));\nconst guidesDir = resolve(currentDir, \"../../guides\");\n\nlet guideCache: Promise<GuideRecord[]> | null = null;\n\nexport async function listGuides(): Promise<GuideRecord[]> {\n if (!guideCache) {\n guideCache = loadGuides();\n }\n\n return guideCache;\n}\n\nexport async function getGuideById(id: string): Promise<GuideRecord | undefined> {\n const guides = await listGuides();\n return guides.find((guide) => guide.id === id);\n}\n\nexport async function getGuideByUri(uri: string): Promise<GuideRecord | undefined> {\n const guides = await listGuides();\n return guides.find((guide) => guide.uri === uri);\n}\n\nasync function loadGuides(): Promise<GuideRecord[]> {\n const fileNames = (await readdir(guidesDir))\n .filter((name) => name.endsWith(\".md\"))\n .sort((left, right) => left.localeCompare(right));\n\n return Promise.all(\n fileNames.map(async (fileName) => {\n const relativePath = fileName;\n const markdown = await readFile(resolve(guidesDir, relativePath), \"utf8\");\n const { metadata, content } = parseFrontmatter(markdown);\n\n return {\n ...metadata,\n uri: `guide://${metadata.category}/${metadata.slug}`,\n relativePath,\n content\n };\n })\n );\n}\n","import type { GuideRecord } from \"./catalog\";\nimport { listGuides } from \"./catalog\";\n\nexport async function searchGuides(query: string): Promise<GuideRecord[]> {\n const guides = await listGuides();\n const normalized = query.trim().toLowerCase();\n\n if (!normalized) {\n return guides;\n }\n\n const terms = normalized.split(/\\s+/).filter(Boolean);\n\n return guides\n .map((guide) => ({\n guide,\n score: scoreGuide(guide, terms)\n }))\n .filter((entry) => entry.score > 0)\n .sort((left, right) => right.score - left.score || left.guide.title.localeCompare(right.guide.title))\n .map((entry) => entry.guide);\n}\n\nfunction scoreGuide(guide: GuideRecord, terms: string[]): number {\n const haystacks = [\n guide.title.toLowerCase(),\n guide.summary.toLowerCase(),\n guide.category.toLowerCase(),\n guide.slug.toLowerCase(),\n guide.id.toLowerCase(),\n guide.uri.toLowerCase(),\n guide.tags.join(\" \").toLowerCase(),\n guide.content.toLowerCase()\n ];\n\n return terms.reduce((score, term) => {\n let termScore = 0;\n\n for (const haystack of haystacks) {\n if (haystack === term) {\n termScore = Math.max(termScore, 8);\n } else if (haystack.startsWith(term)) {\n termScore = Math.max(termScore, 5);\n } else if (haystack.includes(term)) {\n termScore = Math.max(termScore, 2);\n }\n }\n\n return score + termScore;\n }, 0);\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,27 @@
1
+ type GuideMetadata = {
2
+ id: string;
3
+ title: string;
4
+ summary: string;
5
+ category: string;
6
+ slug: string;
7
+ tags: string[];
8
+ sourcePath?: string;
9
+ relatedPaths?: string[];
10
+ };
11
+ declare function parseFrontmatter(markdown: string): {
12
+ metadata: GuideMetadata;
13
+ content: string;
14
+ };
15
+
16
+ type GuideRecord = GuideMetadata & {
17
+ uri: string;
18
+ relativePath: string;
19
+ content: string;
20
+ };
21
+ declare function listGuides(): Promise<GuideRecord[]>;
22
+ declare function getGuideById(id: string): Promise<GuideRecord | undefined>;
23
+ declare function getGuideByUri(uri: string): Promise<GuideRecord | undefined>;
24
+
25
+ declare function searchGuides(query: string): Promise<GuideRecord[]>;
26
+
27
+ export { type GuideMetadata, type GuideRecord, getGuideById, getGuideByUri, listGuides, parseFrontmatter, searchGuides };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getGuideById,
4
+ getGuideByUri,
5
+ listGuides,
6
+ parseFrontmatter,
7
+ searchGuides
8
+ } from "./chunk-R4AEJOIP.js";
9
+ export {
10
+ getGuideById,
11
+ getGuideByUri,
12
+ listGuides,
13
+ parseFrontmatter,
14
+ searchGuides
15
+ };
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,6 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+
3
+ declare function createServer(): McpServer;
4
+ declare function startServer(): Promise<void>;
5
+
6
+ export { createServer, startServer };
package/dist/server.js ADDED
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getGuideById,
4
+ listGuides,
5
+ searchGuides
6
+ } from "./chunk-R4AEJOIP.js";
7
+
8
+ // src/server.ts
9
+ import { fileURLToPath } from "url";
10
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
11
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
12
+ import { z } from "zod";
13
+
14
+ // package.json
15
+ var package_default = {
16
+ name: "@xferops/design-guide",
17
+ version: "0.1.0",
18
+ private: false,
19
+ type: "module",
20
+ main: "./dist/index.js",
21
+ types: "./dist/index.d.ts",
22
+ bin: {
23
+ "xferops-design-guide-mcp": "./dist/server.js"
24
+ },
25
+ files: [
26
+ "dist",
27
+ "guides"
28
+ ],
29
+ publishConfig: {
30
+ access: "public",
31
+ registry: "https://registry.npmjs.org/"
32
+ },
33
+ exports: {
34
+ ".": {
35
+ types: "./dist/index.d.ts",
36
+ default: "./dist/index.js"
37
+ },
38
+ "./server": {
39
+ types: "./dist/server.d.ts",
40
+ default: "./dist/server.js"
41
+ }
42
+ },
43
+ scripts: {
44
+ build: "tsup",
45
+ typecheck: "tsc -p tsconfig.json --noEmit",
46
+ test: "vitest run"
47
+ },
48
+ dependencies: {
49
+ "@modelcontextprotocol/sdk": "^1.20.2",
50
+ zod: "^4.1.11"
51
+ },
52
+ devDependencies: {
53
+ "@types/node": "^24.5.2",
54
+ tsx: "^4.20.6"
55
+ }
56
+ };
57
+
58
+ // src/server.ts
59
+ function createServer() {
60
+ const server = new McpServer(
61
+ {
62
+ name: "@xferops/design-guide",
63
+ version: package_default.version
64
+ },
65
+ {
66
+ capabilities: {
67
+ logging: {}
68
+ }
69
+ }
70
+ );
71
+ server.registerTool(
72
+ "search_guides",
73
+ {
74
+ title: "Search Design Guides",
75
+ description: "Search the XferOps frontend design guide catalog by keyword, tag, or area.",
76
+ inputSchema: z.object({
77
+ query: z.string().describe("Keywords such as table sorting, forms, overlays, or navigation.")
78
+ }),
79
+ outputSchema: z.object({
80
+ guides: z.array(
81
+ z.object({
82
+ id: z.string(),
83
+ title: z.string(),
84
+ summary: z.string(),
85
+ tags: z.array(z.string()),
86
+ uri: z.string(),
87
+ sourcePath: z.string().optional()
88
+ })
89
+ )
90
+ })
91
+ },
92
+ async ({ query }) => {
93
+ const guides = (await searchGuides(query)).map((guide) => ({
94
+ id: guide.id,
95
+ title: guide.title,
96
+ summary: guide.summary,
97
+ tags: guide.tags,
98
+ uri: guide.uri,
99
+ sourcePath: guide.sourcePath
100
+ }));
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: guides.length ? guides.map((guide) => `${guide.title} (${guide.uri})`).join("\n") : "No guides matched the query."
106
+ }
107
+ ],
108
+ structuredContent: { guides }
109
+ };
110
+ }
111
+ );
112
+ server.registerTool(
113
+ "get_guide",
114
+ {
115
+ title: "Get Design Guide",
116
+ description: "Read a guide by id such as table/sorting or by URI such as guide://table/sorting.",
117
+ inputSchema: z.object({
118
+ identifier: z.string().describe("Guide id or guide URI.")
119
+ }),
120
+ outputSchema: z.object({
121
+ id: z.string(),
122
+ title: z.string(),
123
+ summary: z.string(),
124
+ uri: z.string(),
125
+ content: z.string(),
126
+ sourcePath: z.string().optional()
127
+ })
128
+ },
129
+ async ({ identifier }) => {
130
+ const guideId = identifier.startsWith("guide://") ? identifier.replace("guide://", "") : identifier.replace(/^\/+/, "");
131
+ const guide = await getGuideById(guideId);
132
+ if (!guide) {
133
+ return {
134
+ content: [
135
+ {
136
+ type: "text",
137
+ text: `Guide not found: ${identifier}`
138
+ }
139
+ ],
140
+ isError: true
141
+ };
142
+ }
143
+ return {
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: guide.content
148
+ }
149
+ ],
150
+ structuredContent: {
151
+ id: guide.id,
152
+ title: guide.title,
153
+ summary: guide.summary,
154
+ uri: guide.uri,
155
+ content: guide.content,
156
+ sourcePath: guide.sourcePath
157
+ }
158
+ };
159
+ }
160
+ );
161
+ server.registerResource(
162
+ "guide-catalog",
163
+ "guide://catalog",
164
+ {
165
+ title: "Guide Catalog",
166
+ description: "Catalog of available XferOps frontend design guides.",
167
+ mimeType: "application/json"
168
+ },
169
+ async (uri) => {
170
+ const guides = await listGuides();
171
+ return {
172
+ contents: [
173
+ {
174
+ uri: uri.href,
175
+ text: JSON.stringify(
176
+ guides.map((guide) => ({
177
+ id: guide.id,
178
+ title: guide.title,
179
+ summary: guide.summary,
180
+ tags: guide.tags,
181
+ uri: guide.uri,
182
+ sourcePath: guide.sourcePath
183
+ })),
184
+ null,
185
+ 2
186
+ )
187
+ }
188
+ ]
189
+ };
190
+ }
191
+ );
192
+ server.registerResource(
193
+ "guide-document",
194
+ new ResourceTemplate("guide://{category}/{slug}", {
195
+ list: async () => {
196
+ const guides = await listGuides();
197
+ return {
198
+ resources: guides.map((guide) => ({
199
+ uri: guide.uri,
200
+ name: guide.title,
201
+ description: guide.summary,
202
+ mimeType: "text/markdown"
203
+ }))
204
+ };
205
+ }
206
+ }),
207
+ {
208
+ title: "Guide Document",
209
+ description: "A markdown guide for a specific frontend topic.",
210
+ mimeType: "text/markdown"
211
+ },
212
+ async (uri, { category, slug }) => {
213
+ const guide = await getGuideById(`${category}/${slug}`);
214
+ if (!guide) {
215
+ throw new Error(`Guide not found for URI: ${uri.href}`);
216
+ }
217
+ return {
218
+ contents: [
219
+ {
220
+ uri: uri.href,
221
+ text: guide.content
222
+ }
223
+ ]
224
+ };
225
+ }
226
+ );
227
+ return server;
228
+ }
229
+ async function startServer() {
230
+ const server = createServer();
231
+ const transport = new StdioServerTransport();
232
+ await server.connect(transport);
233
+ console.error("@xferops/design-guide MCP server running on stdio");
234
+ }
235
+ var entryPath = process.argv[1];
236
+ if (entryPath && fileURLToPath(import.meta.url) === entryPath) {
237
+ void startServer().catch((error) => {
238
+ console.error("Fatal error in @xferops/design-guide:", error);
239
+ process.exit(1);
240
+ });
241
+ }
242
+ export {
243
+ createServer,
244
+ startServer
245
+ };
246
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../package.json"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport { McpServer, ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport packageJson from \"../package.json\";\nimport { getGuideById, listGuides } from \"./guides/catalog\";\nimport { searchGuides } from \"./guides/search\";\n\nexport function createServer() {\n const server = new McpServer(\n {\n name: \"@xferops/design-guide\",\n version: packageJson.version\n },\n {\n capabilities: {\n logging: {}\n }\n }\n );\n\n server.registerTool(\n \"search_guides\",\n {\n title: \"Search Design Guides\",\n description: \"Search the XferOps frontend design guide catalog by keyword, tag, or area.\",\n inputSchema: z.object({\n query: z.string().describe(\"Keywords such as table sorting, forms, overlays, or navigation.\")\n }),\n outputSchema: z.object({\n guides: z.array(\n z.object({\n id: z.string(),\n title: z.string(),\n summary: z.string(),\n tags: z.array(z.string()),\n uri: z.string(),\n sourcePath: z.string().optional()\n })\n )\n })\n },\n async ({ query }) => {\n const guides = (await searchGuides(query)).map((guide) => ({\n id: guide.id,\n title: guide.title,\n summary: guide.summary,\n tags: guide.tags,\n uri: guide.uri,\n sourcePath: guide.sourcePath\n }));\n\n return {\n content: [\n {\n type: \"text\",\n text: guides.length\n ? guides.map((guide) => `${guide.title} (${guide.uri})`).join(\"\\n\")\n : \"No guides matched the query.\"\n }\n ],\n structuredContent: { guides }\n };\n }\n );\n\n server.registerTool(\n \"get_guide\",\n {\n title: \"Get Design Guide\",\n description: \"Read a guide by id such as table/sorting or by URI such as guide://table/sorting.\",\n inputSchema: z.object({\n identifier: z.string().describe(\"Guide id or guide URI.\")\n }),\n outputSchema: z.object({\n id: z.string(),\n title: z.string(),\n summary: z.string(),\n uri: z.string(),\n content: z.string(),\n sourcePath: z.string().optional()\n })\n },\n async ({ identifier }) => {\n const guideId = identifier.startsWith(\"guide://\")\n ? identifier.replace(\"guide://\", \"\")\n : identifier.replace(/^\\/+/, \"\");\n const guide = await getGuideById(guideId);\n\n if (!guide) {\n return {\n content: [\n {\n type: \"text\",\n text: `Guide not found: ${identifier}`\n }\n ],\n isError: true\n };\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: guide.content\n }\n ],\n structuredContent: {\n id: guide.id,\n title: guide.title,\n summary: guide.summary,\n uri: guide.uri,\n content: guide.content,\n sourcePath: guide.sourcePath\n }\n };\n }\n );\n\n server.registerResource(\n \"guide-catalog\",\n \"guide://catalog\",\n {\n title: \"Guide Catalog\",\n description: \"Catalog of available XferOps frontend design guides.\",\n mimeType: \"application/json\"\n },\n async (uri) => {\n const guides = await listGuides();\n\n return {\n contents: [\n {\n uri: uri.href,\n text: JSON.stringify(\n guides.map((guide) => ({\n id: guide.id,\n title: guide.title,\n summary: guide.summary,\n tags: guide.tags,\n uri: guide.uri,\n sourcePath: guide.sourcePath\n })),\n null,\n 2\n )\n }\n ]\n };\n }\n );\n\n server.registerResource(\n \"guide-document\",\n new ResourceTemplate(\"guide://{category}/{slug}\", {\n list: async () => {\n const guides = await listGuides();\n\n return {\n resources: guides.map((guide) => ({\n uri: guide.uri,\n name: guide.title,\n description: guide.summary,\n mimeType: \"text/markdown\"\n }))\n };\n }\n }),\n {\n title: \"Guide Document\",\n description: \"A markdown guide for a specific frontend topic.\",\n mimeType: \"text/markdown\"\n },\n async (uri, { category, slug }) => {\n const guide = await getGuideById(`${category}/${slug}`);\n\n if (!guide) {\n throw new Error(`Guide not found for URI: ${uri.href}`);\n }\n\n return {\n contents: [\n {\n uri: uri.href,\n text: guide.content\n }\n ]\n };\n }\n );\n\n return server;\n}\n\nexport async function startServer() {\n const server = createServer();\n const transport = new StdioServerTransport();\n\n await server.connect(transport);\n console.error(\"@xferops/design-guide MCP server running on stdio\");\n}\n\nconst entryPath = process.argv[1];\n\nif (entryPath && fileURLToPath(import.meta.url) === entryPath) {\n void startServer().catch((error) => {\n console.error(\"Fatal error in @xferops/design-guide:\", error);\n process.exit(1);\n });\n}\n","{\n \"name\": \"@xferops/design-guide\",\n \"version\": \"0.1.0\",\n \"private\": false,\n \"type\": \"module\",\n \"main\": \"./dist/index.js\",\n \"types\": \"./dist/index.d.ts\",\n \"bin\": {\n \"xferops-design-guide-mcp\": \"./dist/server.js\"\n },\n \"files\": [\n \"dist\",\n \"guides\"\n ],\n \"publishConfig\": {\n \"access\": \"public\",\n \"registry\": \"https://registry.npmjs.org/\"\n },\n \"exports\": {\n \".\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.js\"\n },\n \"./server\": {\n \"types\": \"./dist/server.d.ts\",\n \"default\": \"./dist/server.js\"\n }\n },\n \"scripts\": {\n \"build\": \"tsup\",\n \"typecheck\": \"tsc -p tsconfig.json --noEmit\",\n \"test\": \"vitest run\"\n },\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.20.2\",\n \"zod\": \"^4.1.11\"\n },\n \"devDependencies\": {\n \"@types/node\": \"^24.5.2\",\n \"tsx\": \"^4.20.6\"\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,107 @@
1
+ ---
2
+ id: feedback/status-states
3
+ title: Status, Empty, and Loading States
4
+ summary: Use Alert, Badge, Spinner, EmptyState, and Skeleton for user feedback and progressive loading.
5
+ category: feedback
6
+ slug: status-states
7
+ tags:
8
+ - feedback
9
+ - alert
10
+ - badge
11
+ - empty-state
12
+ - skeleton
13
+ - loading
14
+ sourcePath: apps/docs/src/App.tsx
15
+ relatedPaths:
16
+ - packages/ui/src/components/Alert
17
+ - packages/ui/src/components/Badge
18
+ - packages/ui/src/components/Spinner
19
+ - packages/ui/src/components/EmptyState
20
+ - packages/ui/src/components/Skeleton
21
+ - packages/ui/src/components/Button
22
+ ---
23
+
24
+ # Status, Empty, and Loading States
25
+
26
+ These primitives cover the common feedback cases around status messaging, loading, and empty datasets.
27
+
28
+ ## Import the Feedback Components
29
+
30
+ ```tsx
31
+ import {
32
+ Alert,
33
+ Badge,
34
+ Button,
35
+ EmptyState,
36
+ Skeleton,
37
+ Spinner,
38
+ Stack,
39
+ Text
40
+ } from "@xferops/ui";
41
+ ```
42
+
43
+ ## Use `Alert` and `Badge` for Status
44
+
45
+ Use `Alert` for explanatory messaging and `Badge` for compact labels inside other UI.
46
+
47
+ ```tsx
48
+ <Stack gap="sm">
49
+ <Alert
50
+ tone="warning"
51
+ title="API key expires soon"
52
+ description="Rotate the credential before Friday to avoid sync failures."
53
+ />
54
+
55
+ <Stack direction="row" gap="sm" wrap>
56
+ <Badge>Draft</Badge>
57
+ <Badge tone="accent">Live</Badge>
58
+ <Badge tone="success">Paid</Badge>
59
+ <Badge tone="danger">Overdue</Badge>
60
+ </Stack>
61
+ </Stack>
62
+ ```
63
+
64
+ ## Use `Spinner` and `Skeleton` for Loading
65
+
66
+ Choose `Spinner` when work is in progress and `Skeleton` when the shape of incoming content is already known.
67
+
68
+ ```tsx
69
+ <Stack gap="md">
70
+ <Stack direction="row" align="center" gap="sm">
71
+ <Spinner size="sm" />
72
+ <Text as="span" size="sm" tone="muted">
73
+ Syncing workspace data
74
+ </Text>
75
+ </Stack>
76
+
77
+ <Stack gap="xs">
78
+ <Skeleton height={14} width="35%" />
79
+ <Skeleton height={12} width="80%" />
80
+ <Skeleton height={12} width="72%" />
81
+ </Stack>
82
+ </Stack>
83
+ ```
84
+
85
+ ## Use `EmptyState` for No-Data Screens
86
+
87
+ ```tsx
88
+ <EmptyState
89
+ iconText="[]"
90
+ titleText="No invoices yet"
91
+ descriptionText="Create your first invoice to get paid faster."
92
+ actionsContent={
93
+ <>
94
+ <Button size="sm">Create invoice</Button>
95
+ <Button size="sm" variant="secondary">
96
+ View docs
97
+ </Button>
98
+ </>
99
+ }
100
+ />
101
+ ```
102
+
103
+ ## Notes
104
+
105
+ - Prefer the narrowest primitive that matches the job: `Badge` for inline state, `Alert` for message blocks, `EmptyState` for whole-section fallback.
106
+ - `Skeleton` is decorative and already `aria-hidden`, so pair it with real loading announcements when the wait matters.
107
+ - Keep action content inside `EmptyState` short and task-oriented.