@morya-ui/mcp 0.2.2 → 0.2.4
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 +18 -4
- package/data/catalog.json +225 -41
- package/data/example-coverage.json +31 -8
- package/data/golden-pages/dashboard-page.vue +103 -0
- package/data/golden-pages/form-page.vue +103 -0
- package/data/golden-pages/list-page.vue +117 -0
- package/dist/__tests__/page-snippets.test.d.ts +1 -0
- package/dist/__tests__/page-snippets.test.js +17 -0
- package/dist/__tests__/tools.test.js +80 -1
- package/dist/catalog.js +14 -0
- package/dist/decisions.js +191 -0
- package/dist/golden-pages.d.ts +16 -0
- package/dist/golden-pages.js +67 -0
- package/dist/index.js +22 -1
- package/dist/page-snippets.d.ts +33 -0
- package/dist/page-snippets.js +454 -0
- package/dist/patterns.d.ts +43 -1
- package/dist/patterns.js +197 -24
- package/dist/tools.d.ts +54 -1
- package/dist/tools.js +323 -90
- package/package.json +1 -1
package/dist/decisions.js
CHANGED
|
@@ -137,6 +137,197 @@ export const componentDecisions = [
|
|
|
137
137
|
},
|
|
138
138
|
],
|
|
139
139
|
},
|
|
140
|
+
{
|
|
141
|
+
id: 'layout-spacing-choice',
|
|
142
|
+
title: '如何选择页面间距组件',
|
|
143
|
+
titleEn: 'Choosing page spacing/layout components',
|
|
144
|
+
question: '控件组、页面区块、栅格之间需要哪种间距与对齐方式?',
|
|
145
|
+
questionEn: 'What kind of spacing and alignment is needed between controls, page sections, or grid areas?',
|
|
146
|
+
keywords: ['间距', '布局', 'gap', 'flex', 'space', 'page', 'toolbar', 'filters', 'spacing', 'layout', 'stack'],
|
|
147
|
+
options: [
|
|
148
|
+
{
|
|
149
|
+
component: 'PageContent',
|
|
150
|
+
when: ['页面主内容区需要统一的 padding 和垂直 gap', '列表/表单/仪表盘区块纵向堆叠'],
|
|
151
|
+
whenEn: ['The main content area needs consistent padding and vertical gap', 'List, form, or dashboard sections stack vertically'],
|
|
152
|
+
avoidWhen: ['只是两个按钮之间的水平间距'],
|
|
153
|
+
avoidWhenEn: ['Only horizontal spacing between two buttons is needed'],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
component: 'Flex',
|
|
157
|
+
when: ['同一行控件需要 gap 和对齐', '工具栏内操作组、筛选控件组'],
|
|
158
|
+
whenEn: ['Controls on one row need gap and alignment', 'Toolbar action groups or filter control rows'],
|
|
159
|
+
avoidWhen: ['整个页面区块的 padding 和垂直 rhythm'],
|
|
160
|
+
avoidWhenEn: ['Padding and vertical rhythm for whole page sections'],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
component: 'Space',
|
|
164
|
+
when: ['简单 wrap 控件组且不需要 justify 语义', '表格行内操作按钮组'],
|
|
165
|
+
whenEn: ['A simple wrapped control group without justify semantics', 'Inline row action button groups'],
|
|
166
|
+
avoidWhen: ['页面级标题与主操作两端对齐'],
|
|
167
|
+
avoidWhenEn: ['Page-level title and primary action need space-between alignment'],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
component: 'Grid',
|
|
171
|
+
when: ['KPI 卡片、仪表盘双栏、响应式列布局'],
|
|
172
|
+
whenEn: ['KPI cards, dashboard two-column layouts, responsive columns'],
|
|
173
|
+
avoidWhen: ['单个筛选行或表单字段列'],
|
|
174
|
+
avoidWhenEn: ['A single filter row or form field column'],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: 'page-section-choice',
|
|
180
|
+
title: '如何选择页面区块组件',
|
|
181
|
+
titleEn: 'Choosing page section components',
|
|
182
|
+
question: '这是筛选区、标题区、表单表面、KPI 还是图表占位?',
|
|
183
|
+
questionEn: 'Is this a filter bar, page header, form surface, KPI metric, or chart placeholder?',
|
|
184
|
+
keywords: ['筛选', '工具栏', '标题', '表单', 'kpi', '占位', 'page filters', 'toolbar', 'header', 'stat', 'placeholder'],
|
|
185
|
+
options: [
|
|
186
|
+
{
|
|
187
|
+
component: 'PageFilters',
|
|
188
|
+
when: ['列表页或搜索页的筛选/查询区域', '需要统一浅色背景与边框'],
|
|
189
|
+
whenEn: ['Filter or search region on list pages', 'A consistent muted background and border is needed'],
|
|
190
|
+
avoidWhen: ['普通表单字段分组', 'KPI 指标展示'],
|
|
191
|
+
avoidWhenEn: ['Regular form field grouping', 'KPI metric display'],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
component: 'PageToolbar',
|
|
195
|
+
when: ['页面标题 + 右侧主操作', '列表页新建按钮区域'],
|
|
196
|
+
whenEn: ['Page title plus primary actions on the right', 'Create button area on list pages'],
|
|
197
|
+
avoidWhen: ['带长描述的表单引导区'],
|
|
198
|
+
avoidWhenEn: ['Form intro areas with long descriptions'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
component: 'PageHeader',
|
|
202
|
+
when: ['页面标题 + 描述文案', '表单页/详情页引导'],
|
|
203
|
+
whenEn: ['Page title plus descriptive copy', 'Form or detail page intros'],
|
|
204
|
+
avoidWhen: ['只有标题和单个主按钮的列表工具栏'],
|
|
205
|
+
avoidWhenEn: ['List toolbars with only a title and one primary button'],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
component: 'PageSection',
|
|
209
|
+
when: ['表单主体表面 (variant="form")', '底栏操作区 (variant="actions")'],
|
|
210
|
+
whenEn: ['Form body surface (variant="form")', 'Footer actions (variant="actions")'],
|
|
211
|
+
avoidWhen: ['列表页筛选区', 'KPI 卡片'],
|
|
212
|
+
avoidWhenEn: ['List page filter areas', 'KPI cards'],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
component: 'PageStat',
|
|
216
|
+
when: ['仪表盘 KPI 单指标卡', '需要 label/value/trend/icon 结构'],
|
|
217
|
+
whenEn: ['Dashboard KPI cards', 'Needs label/value/trend/icon structure'],
|
|
218
|
+
avoidWhen: ['普通内容分组', '表格或表单'],
|
|
219
|
+
avoidWhenEn: ['Generic content grouping', 'Tables or forms'],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
component: 'PagePlaceholder',
|
|
223
|
+
when: ['图表、地图或媒体区域尚未接入', '需要 dashed 占位表面'],
|
|
224
|
+
whenEn: ['Charts, maps, or media areas are not wired yet', 'A dashed placeholder surface is needed'],
|
|
225
|
+
avoidWhen: ['真实数据表格或表单'],
|
|
226
|
+
avoidWhenEn: ['Real data tables or forms'],
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'page-scroll-choice',
|
|
232
|
+
title: '页面滚动如何选择',
|
|
233
|
+
titleEn: 'Choosing page scroll strategy',
|
|
234
|
+
question: '这是整页滚动、组件内置滚动,还是业务手写的局部滚动区?',
|
|
235
|
+
questionEn: 'Is this whole-page scroll, built-in component scroll, or a hand-written local scroll region?',
|
|
236
|
+
keywords: [
|
|
237
|
+
'scroll',
|
|
238
|
+
'scrollbar',
|
|
239
|
+
'滚动',
|
|
240
|
+
'滚动条',
|
|
241
|
+
'overflow',
|
|
242
|
+
'页面滚动',
|
|
243
|
+
'layout scroll',
|
|
244
|
+
'panel scroll',
|
|
245
|
+
],
|
|
246
|
+
options: [
|
|
247
|
+
{
|
|
248
|
+
component: 'MLayout fillViewport',
|
|
249
|
+
when: [
|
|
250
|
+
'整页后台列表/表单/仪表盘',
|
|
251
|
+
'需要 Header + Content + 可选 Sider 的应用骨架',
|
|
252
|
+
'页面主滚动应随 Layout 主题化',
|
|
253
|
+
],
|
|
254
|
+
whenEn: [
|
|
255
|
+
'Full admin list/form/dashboard pages',
|
|
256
|
+
'App shell with header, content, and optional sider',
|
|
257
|
+
'Main page scroll should follow the layout theme',
|
|
258
|
+
],
|
|
259
|
+
avoidWhen: ['单个卡片内部的小块内容', '需要业务自行控制滚动的 Dialog/Drawer 内容'],
|
|
260
|
+
avoidWhenEn: ['Small regions inside a single card', 'Dialog/Drawer bodies where apps control scrolling'],
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
component: 'MScrollbar',
|
|
264
|
+
when: [
|
|
265
|
+
'业务自行限高的卡片正文、侧栏、日志列表,且希望主题化滚动条',
|
|
266
|
+
'Dialog / Drawer 等内容区需要主题滚动时由业务显式包一层',
|
|
267
|
+
'组件未内置滚动、又需要统一滚动外观时',
|
|
268
|
+
],
|
|
269
|
+
whenEn: [
|
|
270
|
+
'App-owned capped regions (card bodies, side panels, logs) that want themed scrollbars',
|
|
271
|
+
'Dialog / Drawer content where the app opts into themed scrolling',
|
|
272
|
+
'No built-in component scroll, but a consistent scrollbar look is desired',
|
|
273
|
+
],
|
|
274
|
+
avoidWhen: [
|
|
275
|
+
'Dialog / Drawer / Popover / Splitter 等用户内容插槽被组件库强行包滚动',
|
|
276
|
+
'Textarea 等原生控件自身的滚动',
|
|
277
|
+
'MTable / MLayoutContent / Select 弹出层等已内置滚动的区域',
|
|
278
|
+
],
|
|
279
|
+
avoidWhenEn: [
|
|
280
|
+
'Library-forced scroll around user content slots such as Dialog, Drawer, Popover, or Splitter',
|
|
281
|
+
'Native control scrolling such as Textarea',
|
|
282
|
+
'Areas that already scroll internally (MTable, MLayoutContent, Select popups)',
|
|
283
|
+
],
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
component: 'Built-in (no extra wrapper)',
|
|
287
|
+
when: [
|
|
288
|
+
'MLayout、MTable、MVirtualScroller、菜单/下拉面板等已内置 MScrollbar',
|
|
289
|
+
'浮层菜单与子菜单(Dropdown/ContextMenu/TieredMenu/Menu flyout)',
|
|
290
|
+
],
|
|
291
|
+
whenEn: [
|
|
292
|
+
'MLayout, MTable, MVirtualScroller, and menu/select panels already scroll internally',
|
|
293
|
+
'Overlay menus and nested flyouts (Dropdown/ContextMenu/TieredMenu/Menu flyout)',
|
|
294
|
+
],
|
|
295
|
+
avoidWhen: ['在已内置滚动的组件外再包一层滚动容器'],
|
|
296
|
+
avoidWhenEn: ['Wrapping another scroll container around built-in scroll chrome'],
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: 'surface-nesting-choice',
|
|
302
|
+
title: '如何避免双边框与多余容器',
|
|
303
|
+
titleEn: 'Avoiding double borders and redundant wrappers',
|
|
304
|
+
question: '内容是否已经被 Page 组件或 Table 提供了边界?',
|
|
305
|
+
questionEn: 'Does Page or Table already provide the boundary for this content?',
|
|
306
|
+
keywords: ['边框', '双边框', 'card', 'nested', 'border', 'wrapper', 'table', 'filters'],
|
|
307
|
+
options: [
|
|
308
|
+
{
|
|
309
|
+
component: 'PageContent + MTable',
|
|
310
|
+
when: ['列表页数据表格', 'MPageFilters 已提供筛选区边界'],
|
|
311
|
+
whenEn: ['List page data tables', 'MPageFilters already provides the filter boundary'],
|
|
312
|
+
avoidWhen: ['需要独立卡片标题的内容模块'],
|
|
313
|
+
avoidWhenEn: ['Content modules that need an independent card title'],
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
component: 'MCard',
|
|
317
|
+
when: ['仪表盘中的图表区/明细区', '需要 card 标题的内容模块'],
|
|
318
|
+
whenEn: ['Chart or detail areas on dashboards', 'Content modules that need a card title'],
|
|
319
|
+
avoidWhen: ['列表页表格外层', '筛选区外层'],
|
|
320
|
+
avoidWhenEn: ['Wrapping list page tables', 'Wrapping filter regions'],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
component: 'PageSection',
|
|
324
|
+
when: ['表单页主体或 actions 底栏', '需要统一 form surface'],
|
|
325
|
+
whenEn: ['Form page bodies or action footers', 'A unified form surface is needed'],
|
|
326
|
+
avoidWhen: ['再套 MCard variant form 导致双边框'],
|
|
327
|
+
avoidWhenEn: ['Wrapping again with MCard and causing double borders'],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
140
331
|
];
|
|
141
332
|
export function findDecision(name) {
|
|
142
333
|
const key = name.trim().toLowerCase().replace(/[-_\s]/g, '');
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface GoldenPageRecord {
|
|
2
|
+
id: string;
|
|
3
|
+
file: string;
|
|
4
|
+
title: string;
|
|
5
|
+
titleEn: string;
|
|
6
|
+
patternId: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const goldenPageCatalog: GoldenPageRecord[];
|
|
9
|
+
export declare function listGoldenPages(): GoldenPageRecord[];
|
|
10
|
+
export declare function findGoldenPage(id: string): GoldenPageRecord | undefined;
|
|
11
|
+
export declare function readGoldenPageSource(id: string): {
|
|
12
|
+
record: GoldenPageRecord;
|
|
13
|
+
source: string;
|
|
14
|
+
} | null;
|
|
15
|
+
export declare function goldenPagesAvailable(): boolean;
|
|
16
|
+
export declare function listGoldenPageFilesOnDisk(): string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export const goldenPageCatalog = [
|
|
5
|
+
{
|
|
6
|
+
id: 'list-page',
|
|
7
|
+
file: 'list-page.vue',
|
|
8
|
+
title: '列表页黄金样例',
|
|
9
|
+
titleEn: 'List page golden sample',
|
|
10
|
+
patternId: 'admin-list',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'form-page',
|
|
14
|
+
file: 'form-page.vue',
|
|
15
|
+
title: '表单页黄金样例',
|
|
16
|
+
titleEn: 'Form page golden sample',
|
|
17
|
+
patternId: 'form-page',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
id: 'dashboard-page',
|
|
21
|
+
file: 'dashboard-page.vue',
|
|
22
|
+
title: '仪表盘黄金样例',
|
|
23
|
+
titleEn: 'Dashboard golden sample',
|
|
24
|
+
patternId: 'dashboard',
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
+
const bundledDir = join(pkgRoot, 'data/golden-pages');
|
|
29
|
+
const repoDir = join(pkgRoot, '../../ai-design-config/docs/golden-pages');
|
|
30
|
+
function resolveGoldenPagesDir() {
|
|
31
|
+
if (existsSync(bundledDir))
|
|
32
|
+
return bundledDir;
|
|
33
|
+
if (existsSync(repoDir))
|
|
34
|
+
return repoDir;
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
export function listGoldenPages() {
|
|
38
|
+
return goldenPageCatalog;
|
|
39
|
+
}
|
|
40
|
+
export function findGoldenPage(id) {
|
|
41
|
+
const key = id.trim().toLowerCase().replace(/\.vue$/i, '').replace(/[-_\s]/g, '');
|
|
42
|
+
return goldenPageCatalog.find((item) => item.id.replace(/[-_\s]/g, '') === key);
|
|
43
|
+
}
|
|
44
|
+
export function readGoldenPageSource(id) {
|
|
45
|
+
const record = findGoldenPage(id);
|
|
46
|
+
if (!record)
|
|
47
|
+
return null;
|
|
48
|
+
const dir = resolveGoldenPagesDir();
|
|
49
|
+
if (!dir)
|
|
50
|
+
return null;
|
|
51
|
+
const path = join(dir, record.file);
|
|
52
|
+
if (!existsSync(path))
|
|
53
|
+
return null;
|
|
54
|
+
return { record, source: readFileSync(path, 'utf8').replace(/^\uFEFF/, '') };
|
|
55
|
+
}
|
|
56
|
+
export function goldenPagesAvailable() {
|
|
57
|
+
const dir = resolveGoldenPagesDir();
|
|
58
|
+
if (!dir)
|
|
59
|
+
return false;
|
|
60
|
+
return goldenPageCatalog.every((item) => existsSync(join(dir, item.file)));
|
|
61
|
+
}
|
|
62
|
+
export function listGoldenPageFilesOnDisk() {
|
|
63
|
+
const dir = resolveGoldenPagesDir();
|
|
64
|
+
if (!dir)
|
|
65
|
+
return [];
|
|
66
|
+
return readdirSync(dir).filter((name) => name.endsWith('.vue'));
|
|
67
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ register('list', 'List morya-ui components, guides, examples, categories, or pag
|
|
|
33
33
|
}, async (args) => handlers.list(args));
|
|
34
34
|
register('search', 'Search components, guides, API text, examples, page patterns, and component decision guides.', {
|
|
35
35
|
query: z.string().min(1),
|
|
36
|
-
scope: z.enum(['all', 'components', 'guides', 'api', 'examples', 'patterns', 'decisions']).optional(),
|
|
36
|
+
scope: z.enum(['all', 'components', 'guides', 'api', 'examples', 'patterns', 'decisions', 'snippets']).optional(),
|
|
37
37
|
mode: z.string().optional(),
|
|
38
38
|
limit: z.number().int().min(1).max(50).optional(),
|
|
39
39
|
offset: z.number().int().min(0).optional(),
|
|
@@ -101,6 +101,27 @@ register('recommend_component', 'List, read, or recommend component selection gu
|
|
|
101
101
|
limit: z.number().int().min(1).max(200).optional(),
|
|
102
102
|
offset: z.number().int().min(0).optional(),
|
|
103
103
|
}, async (args) => handlers.recommendComponent(args));
|
|
104
|
+
register('list_golden_pages', 'List golden page samples for list, form, and dashboard layouts.', { mode: z.string().optional() }, async (args) => handlers.listGoldenPageCatalog(args));
|
|
105
|
+
register('get_golden_page', 'Read a golden page Vue source sample (list-page, form-page, dashboard-page).', {
|
|
106
|
+
page: z.string().min(1),
|
|
107
|
+
mode: z.string().optional(),
|
|
108
|
+
}, async (args) => handlers.getGoldenPage(args));
|
|
109
|
+
register('validate_page', 'Validate a generated page for composition, spacing, and border anti-patterns.', {
|
|
110
|
+
code: z.string().min(1),
|
|
111
|
+
mode: z.string().optional(),
|
|
112
|
+
}, async (args) => handlers.validatePage(args));
|
|
113
|
+
register('list_page_snippets', 'List reusable page section snippets for local edits (filters, toolbar, form actions, KPI grid, etc.).', {
|
|
114
|
+
query: z.string().optional(),
|
|
115
|
+
pageType: z.string().optional(),
|
|
116
|
+
mode: z.string().optional(),
|
|
117
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
118
|
+
offset: z.number().int().min(0).optional(),
|
|
119
|
+
}, async (args) => handlers.listPageSnippets(args));
|
|
120
|
+
register('get_page_snippet', 'Read a standard page section snippet by id or keyword (e.g. filters, toolbar, form-actions). Pass includeScript: true for import + template bundle.', {
|
|
121
|
+
section: z.string().min(1),
|
|
122
|
+
mode: z.string().optional(),
|
|
123
|
+
includeScript: z.boolean().optional(),
|
|
124
|
+
}, async (args) => handlers.getPageSnippet(args));
|
|
104
125
|
register('version', 'Return MCP package, library version, and catalog status.', {}, async () => handlers.version());
|
|
105
126
|
const transport = new StdioServerTransport();
|
|
106
127
|
await server.connect(transport);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type PageSnippetPageType = 'list' | 'form' | 'dashboard' | 'detail' | 'common';
|
|
2
|
+
export interface PageSnippet {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
titleEn: string;
|
|
6
|
+
description: string;
|
|
7
|
+
descriptionEn: string;
|
|
8
|
+
pageTypes: PageSnippetPageType[];
|
|
9
|
+
keywords: string[];
|
|
10
|
+
imports: string[];
|
|
11
|
+
scriptSetup?: string;
|
|
12
|
+
template: string;
|
|
13
|
+
rules: string[];
|
|
14
|
+
rulesEn: string[];
|
|
15
|
+
avoid: string[];
|
|
16
|
+
avoidEn: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare const pageSnippets: PageSnippet[];
|
|
19
|
+
export declare function findPageSnippet(id: string): PageSnippet | undefined;
|
|
20
|
+
export declare function scorePageSnippet(snippet: PageSnippet, query: string): number;
|
|
21
|
+
export declare function filterPageSnippets(args: {
|
|
22
|
+
query?: string;
|
|
23
|
+
pageType?: string;
|
|
24
|
+
limit?: number;
|
|
25
|
+
offset?: number;
|
|
26
|
+
}): {
|
|
27
|
+
total: number;
|
|
28
|
+
items: PageSnippet[];
|
|
29
|
+
offset: number;
|
|
30
|
+
limit: number;
|
|
31
|
+
hasMore: boolean;
|
|
32
|
+
nextOffset: number | undefined;
|
|
33
|
+
};
|