@morya-ui/mcp 0.2.9 → 0.3.0

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.
@@ -74,16 +74,60 @@ describe('@morya-ui/mcp handlers', () => {
74
74
  }));
75
75
  expect(result.matchedPattern).toBe('admin-list');
76
76
  });
77
- it('returns a dashboard scaffold when includeScaffold is true', () => {
77
+ it('returns golden-page source as scaffold when available', () => {
78
78
  const result = read(handlers.recommendPage({
79
79
  intent: '生产监控仪表盘',
80
80
  pageType: 'dashboard',
81
81
  includeScaffold: true,
82
82
  }));
83
83
  expect(result.matchedPattern).toBe('dashboard');
84
+ expect(result.scaffold.source).toBe('golden-page');
85
+ expect(result.scaffold.goldenPage).toBe('dashboard-page');
84
86
  expect(result.scaffold.files.component).toContain('MGrid');
85
87
  expect(result.scaffold.files.component).toContain('MPageStat');
86
- expect(result.scaffold.files.component).toContain('MSkeleton');
88
+ expect(result.scaffold.files.component).toContain('黄金样例');
89
+ expect(result.nextStep).toContain('validate_usage');
90
+ });
91
+ it('prefers list-status-dot for status cell queries', () => {
92
+ const result = read(handlers.getPageSnippet({ section: '状态' }));
93
+ expect(result.id).toBe('list-status-dot');
94
+ expect(result.template).toContain('MStatus');
95
+ });
96
+ it('returns result-page golden sample with MResult footer slot', () => {
97
+ const result = read(handlers.getGoldenPage({ page: 'result-page' }));
98
+ expect(result.id).toBe('result-page');
99
+ expect(result.source).toContain('MResult');
100
+ expect(result.source).toContain('status="403"');
101
+ expect(result.source).toContain('<template #footer>');
102
+ expect(result.source).not.toContain('<template #extra>');
103
+ });
104
+ it('returns settings-page and wizard-form golden samples with correct APIs', () => {
105
+ const settings = read(handlers.getGoldenPage({ page: 'settings-page' }));
106
+ expect(settings.id).toBe('settings-page');
107
+ expect(settings.source).toContain('MTabs');
108
+ expect(settings.source).toContain(':tabs=');
109
+ expect(settings.source).not.toContain(':items=');
110
+ const wizard = read(handlers.getGoldenPage({ page: 'wizard-form' }));
111
+ expect(wizard.id).toBe('wizard-form');
112
+ expect(wizard.source).toContain('MStepper');
113
+ expect(wizard.source).toContain(':steps=');
114
+ expect(wizard.source).not.toContain(':items=');
115
+ });
116
+ it('scaffolds settings from golden page source', () => {
117
+ const result = read(handlers.recommendPage({
118
+ intent: '系统设置页',
119
+ pageType: 'settings',
120
+ includeScaffold: true,
121
+ }));
122
+ expect(result.scaffold.source).toBe('golden-page');
123
+ expect(result.scaffold.goldenPage).toBe('settings-page');
124
+ expect(result.scaffold.files.component).toContain(':tabs=');
125
+ });
126
+ it('flags MTabs items and MStepper items as contract advisories', () => {
127
+ const tabs = read(handlers.validatePage({ code: `<MTabs :items="[]" />` }));
128
+ expect(tabs.suggestions.some((item) => item.type === 'tabs-items-prop')).toBe(true);
129
+ const stepper = read(handlers.validatePage({ code: `<MStepper :items="['a']" />` }));
130
+ expect(stepper.suggestions.some((item) => item.type === 'stepper-items-prop')).toBe(true);
87
131
  });
88
132
  it('returns a golden page source sample', () => {
89
133
  const result = read(handlers.getGoldenPage({ page: 'list-page' }));
@@ -138,14 +182,15 @@ describe('@morya-ui/mcp handlers', () => {
138
182
  it('keeps recommend_page focused on pattern matching', () => {
139
183
  const result = read(handlers.recommendPage({ intent: '用户列表页', pageType: 'list' }));
140
184
  expect(result.matchedPattern).toBe('admin-list');
141
- expect(result.nextStep).toContain('get_design_rules');
185
+ expect(result.nextStep).toContain('validate_usage');
186
+ expect(result.nextStep).toContain('get_golden_page');
142
187
  expect(result.pageStandards).toBeUndefined();
143
188
  expect(result.scrollStandards).toBeUndefined();
144
189
  });
145
190
  it('keeps get_golden_page focused on source code', () => {
146
191
  const result = read(handlers.getGoldenPage({ page: 'list-page' }));
147
192
  expect(result.source).toContain('MLayout');
148
- expect(result.nextStep).toContain('get_design_rules');
193
+ expect(result.nextStep).toContain('validate_usage');
149
194
  expect(result.pageStandards).toBeUndefined();
150
195
  expect(result.scrollStandards).toBeUndefined();
151
196
  });
@@ -188,6 +233,44 @@ describe('@morya-ui/mcp handlers', () => {
188
233
  expect(result.ok).toBe(true);
189
234
  expect(result.suggestions.some((item) => item.type === 'menu-missing-icons')).toBe(false);
190
235
  });
236
+ it('flags MTable data prop as contract advisory', () => {
237
+ const result = read(handlers.validatePage({
238
+ code: '<MTable :columns="columns" :data="rows" />',
239
+ }));
240
+ expect(result.ok).toBe(true);
241
+ expect(result.suggestions.some((item) => item.type === 'table-data-prop')).toBe(true);
242
+ });
243
+ it('flags MMessage severity as inline-alert misuse', () => {
244
+ const result = read(handlers.validatePage({
245
+ code: '<MMessage severity="error">登录失败</MMessage>',
246
+ }));
247
+ expect(result.ok).toBe(true);
248
+ expect(result.suggestions.some((item) => item.type === 'mmessage-as-alert')).toBe(true);
249
+ });
250
+ it('flags one-line toast string as message preference', () => {
251
+ const result = read(handlers.validatePage({
252
+ code: `toast.success('已保存')`,
253
+ }));
254
+ expect(result.ok).toBe(true);
255
+ expect(result.suggestions.some((item) => item.type === 'toast-one-liner')).toBe(true);
256
+ });
257
+ it('flags MTag in cell-status when MStatus is missing', () => {
258
+ const result = read(handlers.validatePage({
259
+ code: `<template #cell-status="{ value }"><MTag :value="value" /></template>`,
260
+ }));
261
+ expect(result.ok).toBe(true);
262
+ expect(result.suggestions.some((item) => item.type === 'status-cell-tag')).toBe(true);
263
+ });
264
+ it('returns detail-page and form-in-dialog golden samples', () => {
265
+ const detail = read(handlers.getGoldenPage({ page: 'detail-page' }));
266
+ expect(detail.id).toBe('detail-page');
267
+ expect(detail.source).toContain('MPageHeader');
268
+ expect(detail.source).toContain('MStatus');
269
+ const dialog = read(handlers.getGoldenPage({ page: 'form-in-dialog' }));
270
+ expect(dialog.id).toBe('form-in-dialog');
271
+ expect(dialog.source).toContain('MDialog');
272
+ expect(dialog.source).toContain('message.success');
273
+ });
191
274
  it('lists component decision guides when query is omitted', () => {
192
275
  const result = read(handlers.recommendComponent({ limit: 5 }));
193
276
  expect(result.kind).toBe('decisions');
@@ -44,6 +44,41 @@ export const goldenPageCatalog = [
44
44
  titleEn: 'Empty state golden sample',
45
45
  patternId: 'empty-state',
46
46
  },
47
+ {
48
+ id: 'detail-page',
49
+ file: 'detail-page.vue',
50
+ title: '详情页黄金样例',
51
+ titleEn: 'Detail page golden sample',
52
+ patternId: 'detail-page',
53
+ },
54
+ {
55
+ id: 'form-in-dialog',
56
+ file: 'form-in-dialog.vue',
57
+ title: '列表内弹窗表单黄金样例',
58
+ titleEn: 'List create/edit dialog golden sample',
59
+ patternId: 'form-in-dialog',
60
+ },
61
+ {
62
+ id: 'result-page',
63
+ file: 'result-page.vue',
64
+ title: '结果 / 阻断页黄金样例',
65
+ titleEn: 'Result / terminal page golden sample',
66
+ patternId: 'result-page',
67
+ },
68
+ {
69
+ id: 'settings-page',
70
+ file: 'settings-page.vue',
71
+ title: '设置页黄金样例',
72
+ titleEn: 'Settings page golden sample',
73
+ patternId: 'settings-page',
74
+ },
75
+ {
76
+ id: 'wizard-form',
77
+ file: 'wizard-form.vue',
78
+ title: '分步向导黄金样例',
79
+ titleEn: 'Wizard form golden sample',
80
+ patternId: 'wizard-form',
81
+ },
47
82
  ];
48
83
  const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
49
84
  const bundledDir = join(pkgRoot, 'data/golden-pages');
package/dist/index.js CHANGED
@@ -102,7 +102,7 @@ register('recommend_component', 'List, read, or recommend component selection gu
102
102
  offset: z.number().int().min(0).optional(),
103
103
  }, async (args) => handlers.recommendComponent(args));
104
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, login-page, landing-page, empty-state).', {
105
+ register('get_golden_page', 'Read a golden page Vue source sample (list-page, form-page, dashboard-page, login-page, landing-page, empty-state, detail-page, form-in-dialog, result-page, settings-page, wizard-form).', {
106
106
  page: z.string().min(1),
107
107
  mode: z.string().optional(),
108
108
  }, async (args) => handlers.getGoldenPage(args));
@@ -120,43 +120,43 @@ const loading = ref(false)`,
120
120
  },
121
121
  {
122
122
  id: 'list-status-tag',
123
- title: '表格状态列 Tag',
124
- titleEn: 'Table status tag cell',
125
- description: ' #cell-status 中用 MTag 展示分类标签(业务状态优先用 list-status-dot / MStatus)。',
126
- descriptionEn: 'Render category labels with MTag in #cell-status (prefer MStatus for business status).',
123
+ title: '表格分类标签列 Tag',
124
+ titleEn: 'Table category tag cell',
125
+ description: '仅用于分类 / 可关闭标签。业务状态(启用/停用/在线)请用 list-status-dot / MStatus',
126
+ descriptionEn: 'Category or closable chips only. Prefer list-status-dot / MStatus for business status.',
127
127
  pageTypes: ['list'],
128
- keywords: ['状态', 'tag', 'status', 'cell-status'],
128
+ keywords: ['分类', '标签', '可关闭', 'chip', 'category', 'tag-only'],
129
129
  imports: ['MTag'],
130
- template: `<template #cell-status="{ value }">
130
+ template: `<template #cell-category="{ value }">
131
131
  <MTag
132
- :value="value === 'active' ? '启用' : '停用'"
133
- :severity="value === 'active' ? 'success' : 'secondary'"
132
+ :value="String(value ?? '')"
133
+ severity="info"
134
134
  />
135
135
  </template>`,
136
- rules: ['分类/强调态用 MTag;更轻的圆点+文案用 MStatus', '不要用 Button 颜色表达状态'],
137
- rulesEn: ['Use MTag for chip-like status; use MStatus for lighter dot+label', 'Do not use Button colors for status'],
138
- avoid: ['不要用裸文本颜色区分状态'],
139
- avoidEn: ['Do not rely on raw text color for status'],
136
+ rules: ['分类/强调态用 MTag;行内业务状态用 MStatus(list-status-dot)', '不要用 Button 颜色表达状态'],
137
+ rulesEn: ['Use MTag for categories; use MStatus for business status (list-status-dot)', 'Do not use Button colors for status'],
138
+ avoid: ['不要在 #cell-status 里默认用 MTag 表示启用/停用'],
139
+ avoidEn: ['Do not default #cell-status to MTag for active/inactive'],
140
140
  },
141
141
  {
142
142
  id: 'list-status-dot',
143
143
  title: '表格状态列 Status',
144
144
  titleEn: 'Table status dot cell',
145
- description: '在 #cell-status 中用 MStatus 展示轻量业务状态。',
146
- descriptionEn: 'Render lightweight business status with MStatus in #cell-status.',
145
+ description: '在 #cell-status 中用 MStatus 展示轻量业务状态(默认首选)。',
146
+ descriptionEn: 'Render lightweight business status with MStatus in #cell-status (default preference).',
147
147
  pageTypes: ['list'],
148
- keywords: ['状态', 'status', 'dot', 'cell-status', '在线'],
148
+ keywords: ['状态', 'status', 'dot', 'cell-status', '在线', '启用', '停用', '业务状态'],
149
149
  imports: ['MStatus'],
150
150
  template: `<template #cell-status="{ value }">
151
151
  <MStatus
152
- :label="value === 'online' ? '在线' : '离线'"
153
- :severity="value === 'online' ? 'success' : 'secondary'"
152
+ :label="value === 'active' || value === 'online' ? '启用' : '停用'"
153
+ :severity="value === 'active' || value === 'online' ? 'success' : 'secondary'"
154
154
  />
155
155
  </template>`,
156
- rules: ['行内轻量状态优先 MStatus', '不要用 Button 颜色表达状态'],
157
- rulesEn: ['Prefer MStatus for lightweight inline status', 'Do not use Button colors for status'],
158
- avoid: ['不要用裸文本颜色区分状态'],
159
- avoidEn: ['Do not rely on raw text color for status'],
156
+ rules: ['行内轻量业务状态优先 MStatus', '不要用 Button 颜色表达状态'],
157
+ rulesEn: ['Prefer MStatus for lightweight inline business status', 'Do not use Button colors for status'],
158
+ avoid: ['不要用裸文本颜色区分状态', '不要默认改用 MTag 表达启用/停用'],
159
+ avoidEn: ['Do not rely on raw text color for status', 'Do not default to MTag for active/inactive'],
160
160
  },
161
161
  {
162
162
  id: 'empty-block',
@@ -48,7 +48,7 @@ export declare const designRules: {
48
48
  readonly spacing: "--m-space-*";
49
49
  readonly radius: "--m-radius-sm/md/lg";
50
50
  readonly typography: "--m-font-size-xs/sm/md/lg";
51
- readonly motion: "--m-motion-fast/normal";
51
+ readonly motion: "--m-motion-fast/normal/enter/exit/distance/ease; intensity via useMotion (data-m-motion); enter/exit via motion.transitions / transition prop (fade, scale-fade, zoom, slide-*, dialog, drawer, …)";
52
52
  };
53
53
  readonly composition: {
54
54
  readonly workflow: readonly ["For full pages: recommend_page → get_golden_page → get_design_rules.", "For local edits: get_page_snippet(section) for filters/toolbar/form-actions/KPI/scrollable-panel blocks.", "Use MLayout fillViewport as the app shell; put MPageContent inside MLayoutContent.", "Prefer MPage* components over scoped CSS for filters, toolbars, headers, form surfaces, and KPI cards.", "Use MSpace or MFlex for control groups inside MPageFilters; use MPageToolbar for title + primary action."];
@@ -68,7 +68,7 @@ export declare const designRules: {
68
68
  readonly do: readonly ["MLayoutHeader bordered (default)", "MPageFilters for filter regions", "MPageSection variant=\"form\" for forms", "MTable bordered when table needs grid lines"];
69
69
  readonly avoid: readonly ["MCard wrapping MTable that is already bordered", "Nested MCard with bordered=true on both levels", "Hand-written section borders when MPageFilters or MPageSection applies", "Double borders on filter + table wrapper"];
70
70
  };
71
- readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page", "login-page", "landing-page", "empty-state"];
71
+ readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page", "login-page", "landing-page", "empty-state", "detail-page", "form-in-dialog", "result-page", "settings-page", "wizard-form"];
72
72
  };
73
73
  readonly actions: {
74
74
  readonly primary: {
@@ -130,7 +130,7 @@ export declare const designRules: {
130
130
  };
131
131
  readonly doc: "design-kit/.agents/skills/morya-ui-pages/references/feedback.md";
132
132
  };
133
- readonly global: readonly ["优先使用组件库组件和 --m-* Token", "操作反馈默认 message;仅一行文案时优先于 toast", "空态用 MEmpty;结果/阻断页用 MResult;行内状态优先 MStatus", "图标按钮建议提供 aria-label;表单字段建议有可见 label", "浮层默认 Teleport 到 body;有明确布局约束时再改 appendTo", "优先使用组件 documented variant,少写深层 CSS 覆盖"];
133
+ readonly global: readonly ["优先使用组件库组件和 --m-* Token", "操作反馈默认 message;仅一行文案时优先于 toast", "空态用 MEmpty;结果/阻断页用 MResult;行内状态优先 MStatus", "图标按钮建议提供 aria-label;表单字段建议有可见 label", "浮层默认 Teleport 到 body;有明确布局约束时再改 appendTo", "优先使用组件 documented variant,少写深层 CSS 覆盖", "动效强度用 useMotion;进出场预设用 motion.transitions / 组件 transition,勿臆造动画库 API"];
134
134
  };
135
135
  export declare function findPattern(input: string): PagePattern | undefined;
136
136
  export declare function scorePattern(pattern: PagePattern, query: string): number;
package/dist/patterns.js CHANGED
@@ -186,6 +186,7 @@ export const pagePatterns = [
186
186
  '不要为 ≤8 字段的列表 CRUD 再开 /entities/new 路由',
187
187
  '不要用 Toast 代替单行 message 回执',
188
188
  ],
189
+ goldenPage: 'form-in-dialog',
189
190
  },
190
191
  {
191
192
  id: 'dashboard',
@@ -260,6 +261,7 @@ export const pagePatterns = [
260
261
  styleRules: ['开关用于二元启用状态,不用 Select 模拟开关', '保存使用 primary,恢复默认值使用 secondary 或 text', '危险配置需要明确说明影响范围并使用 danger 语义', '使用 MConfigProvider 统一 size、density 和 locale'],
261
262
  interactionRules: ['显示未保存修改状态', '保存成功使用 message.success 单行反馈;仅有 summary+detail 时用 toast', '保存失败保留输入并显示字段或页面级错误', '切换 Tab 不应意外丢失未保存输入'],
262
263
  avoid: ['不要把所有设置塞进一个超长表单', '不要用 placeholder 代替配置项 label', '不要隐藏影响范围较大的配置说明'],
264
+ goldenPage: 'settings-page',
263
265
  },
264
266
  {
265
267
  id: 'empty-state',
@@ -391,6 +393,7 @@ export const pagePatterns = [
391
393
  styleRules: ['每一步只承载一个清晰目标', '步骤状态使用 Stepper,不用 Button 颜色模拟', '长流程使用 Card 分组但避免多层嵌套', '危险退出使用 ConfirmDialog 或 Dialog'],
392
394
  interactionRules: ['进入下一步前只校验当前步骤', '返回上一步保留输入', '完成前展示摘要或确认', '刷新和离开时处理未完成状态'],
393
395
  avoid: ['不要把所有字段一次性隐藏在一个超长页面', '不要允许跳过有前置依赖的步骤', '不要让完成按钮在每一步都使用相同文案'],
396
+ goldenPage: 'wizard-form',
394
397
  },
395
398
  {
396
399
  id: 'detail-page',
@@ -441,6 +444,7 @@ export const pagePatterns = [
441
444
  '不要使用颜色作为唯一的状态表达方式',
442
445
  '不要让详情页的操作按钮分散在多个无关区域',
443
446
  ],
447
+ goldenPage: 'detail-page',
444
448
  },
445
449
  {
446
450
  id: 'result-page',
@@ -471,9 +475,9 @@ export const pagePatterns = [
471
475
  structure: [
472
476
  'MPageContent (optional shell)',
473
477
  '└── MResult',
474
- ' ├── status icon',
478
+ ' ├── status icon / illustration',
475
479
  ' ├── title + description',
476
- ' └── #extra primary + optional secondary MButton',
480
+ ' └── #footer primary + optional secondary MButton',
477
481
  ],
478
482
  layout: {
479
483
  page: '结果在内容区居中,不一定占满整屏',
@@ -482,8 +486,8 @@ export const pagePatterns = [
482
486
  },
483
487
  styleRules: [
484
488
  '使用 MResult 的 status 语义,不要手写大图标+文案拼盘',
485
- '成功/失败用对应 severity 色;403/404 保持中性',
486
- '主恢复动作使用 primary MButton,次动作使用 secondary 或 text',
489
+ '成功/失败用对应 status;403/404/500 用 HTTP 类 status',
490
+ '主恢复动作使用 primary MButton,次动作使用 secondary 或 text(放在 #footer,不是 #extra)',
487
491
  '颜色与间距使用 --m-* Token',
488
492
  ],
489
493
  interactionRules: [
@@ -495,7 +499,9 @@ export const pagePatterns = [
495
499
  '不要用 Empty 表达 403/404/提交失败',
496
500
  '不要只用颜色表达结果而无标题文案',
497
501
  '不要在结果页堆砌无关营销内容',
502
+ '不要使用已移除的 #extra 插槽;操作用 #footer',
498
503
  ],
504
+ goldenPage: 'result-page',
499
505
  },
500
506
  ];
501
507
  /** Advisory page-writing standards for MCP and human authors — not enforced blockers. */
@@ -639,7 +645,7 @@ export const designRules = {
639
645
  spacing: '--m-space-*',
640
646
  radius: '--m-radius-sm/md/lg',
641
647
  typography: '--m-font-size-xs/sm/md/lg',
642
- motion: '--m-motion-fast/normal',
648
+ motion: '--m-motion-fast/normal/enter/exit/distance/ease; intensity via useMotion (data-m-motion); enter/exit via motion.transitions / transition prop (fade, scale-fade, zoom, slide-*, dialog, drawer, …)',
643
649
  },
644
650
  composition: {
645
651
  workflow: [
@@ -682,7 +688,19 @@ export const designRules = {
682
688
  'Double borders on filter + table wrapper',
683
689
  ],
684
690
  },
685
- goldenPages: ['list-page', 'form-page', 'dashboard-page', 'login-page', 'landing-page', 'empty-state'],
691
+ goldenPages: [
692
+ 'list-page',
693
+ 'form-page',
694
+ 'dashboard-page',
695
+ 'login-page',
696
+ 'landing-page',
697
+ 'empty-state',
698
+ 'detail-page',
699
+ 'form-in-dialog',
700
+ 'result-page',
701
+ 'settings-page',
702
+ 'wizard-form',
703
+ ],
686
704
  },
687
705
  actions: {
688
706
  primary: { component: 'MButton', props: ['severity omitted or primary'] },
@@ -737,6 +755,7 @@ export const designRules = {
737
755
  '图标按钮建议提供 aria-label;表单字段建议有可见 label',
738
756
  '浮层默认 Teleport 到 body;有明确布局约束时再改 appendTo',
739
757
  '优先使用组件 documented variant,少写深层 CSS 覆盖',
758
+ '动效强度用 useMotion;进出场预设用 motion.transitions / 组件 transition,勿臆造动画库 API',
740
759
  ],
741
760
  };
742
761
  export function findPattern(input) {