@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/dist/tools.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { findComponent, findGuide, loadCatalog, normalizeName, resolveLocale, textResult, toKebab, } from './catalog.js';
2
2
  import { componentDecisions, findDecision, scoreDecision } from './decisions.js';
3
+ import { listGoldenPages, readGoldenPageSource } from './golden-pages.js';
4
+ import { filterPageSnippets, findPageSnippet, pageSnippets, scorePageSnippet } from './page-snippets.js';
3
5
  import { designRules, findPattern, pagePatterns, scorePattern } from './patterns.js';
4
6
  import { countCatalogResourceTemplates, countCatalogResources } from './resources.js';
5
7
  function inspectButtonIconOnlyUsage(code, issues) {
@@ -60,16 +62,19 @@ function generatedPageCode(patternId, intent, locale) {
60
62
  const layoutImports = useLayoutShell
61
63
  ? ', MLayout, MLayoutContent, MLayoutHeader, MLayoutSider, MBreadcrumb'
62
64
  : '';
65
+ const pageImports = useLayoutShell
66
+ ? ', MPageContent, MPageFilters, MPageHeader, MPageSection, MPageToolbar'
67
+ : '';
63
68
  const listImports = isList ? ', MSelect, MSpace, MTable' : '';
64
69
  const formImports = isForm || isAuth || isWizard ? ', MForm, MFormItem, MSelect' : '';
65
- const dashboardImports = isDashboard ? ', MCard, MGrid, MGridItem, MSkeleton, MTable' : '';
70
+ const dashboardImports = isDashboard ? ', MCard, MGrid, MGridItem, MPagePlaceholder, MPageStat, MSkeleton, MTable' : '';
66
71
  const detailImports = isDetail ? ', MDivider' : '';
67
72
  const emptyImports = isEmpty ? ', MDataView' : '';
68
73
  const wizardImports = isWizard ? ', MStepper' : '';
69
74
  const settingsImports = isSettings ? ', MTabs' : '';
70
75
  const script = `<script setup lang="ts">
71
76
  import { ref } from 'vue'
72
- import { MButton, MCard, MConfigProvider, MInput, MTag, zhCN${layoutImports}${listImports}${formImports}${dashboardImports}${detailImports}${emptyImports}${wizardImports}${settingsImports} } from 'morya-ui'
77
+ import { MButton, MCard, MConfigProvider, MInput, MTag, zhCN${layoutImports}${pageImports}${listImports}${formImports}${dashboardImports}${detailImports}${emptyImports}${wizardImports}${settingsImports} } from 'morya-ui'
73
78
 
74
79
  const loading = ref(false)
75
80
  const error = ref('')
@@ -94,62 +99,55 @@ async function submit() {
94
99
  }
95
100
  }
96
101
  </script>`;
97
- const listContent = ` <section class="m-generated-filters" aria-label="${zh ? '筛选' : 'Filters'}">
98
- <MSpace wrap>
99
- <MInput v-model="keyword" placeholder="${zh ? '搜索关键词' : 'Search keyword'}" clearable style="width: 14rem" />
100
- <MButton severity="primary">${zh ? '查询' : 'Search'}</MButton>
101
- <MButton severity="secondary">${zh ? '重置' : 'Reset'}</MButton>
102
- </MSpace>
103
- </section>
104
- <header class="m-generated-toolbar">
105
- <h1 class="m-generated-title">${title}</h1>
106
- <MButton severity="primary">${zh ? '新建' : 'Create'}</MButton>
107
- </header>
108
- <MTable :columns="columns" :rows="rows" :loading="loading" paginator :rows-per-page="10" striped bordered row-key="id">
109
- <template #empty>
110
- <p class="m-generated-muted">${zh ? '暂无数据' : 'No data yet'}</p>
111
- </template>
112
- </MTable>`;
113
- const formContent = ` <header class="m-generated-intro">
114
- <h1 class="m-generated-title">${title}</h1>
115
- <p class="m-generated-muted">${zh ? '填写表单并保存。' : 'Fill in the form and save.'}</p>
116
- </header>
117
- <MForm class="m-generated-form" @submit.prevent="submit">
118
- <MFormItem label="${zh ? '名称' : 'Name'}" name="name" required>
119
- <MInput v-model="model.name" fluid />
120
- </MFormItem>
121
- <footer class="m-generated-actions">
122
- <MButton native-type="submit" severity="primary" :loading="loading">${zh ? '保存' : 'Save'}</MButton>
123
- <MButton severity="secondary">${zh ? '取消' : 'Cancel'}</MButton>
124
- </footer>
125
- </MForm>`;
126
- const dashboardContent = ` <h1 class="m-generated-title">${title}</h1>
127
- <MGrid :cols="2" :x-gap="16" :y-gap="16" responsive="screen">
128
- <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
129
- <MCard>
130
- <p class="m-generated-muted">{{ metric.label }}</p>
131
- <strong class="m-generated-metric">{{ metric.value }}</strong>
132
- </MCard>
133
- </MGridItem>
134
- </MGrid>
135
- <MCard :title="${zh ? '趋势概览' : 'Trend overview'}">
136
- <MSkeleton v-if="loading" height="8rem" />
137
- <p v-else class="m-generated-muted">${zh ? '接入图表或业务组件。' : 'Connect charts or business widgets here.'}</p>
138
- </MCard>`;
139
- const detailContent = ` <header class="m-generated-toolbar">
140
- <div>
141
- <h1 class="m-generated-title">${title}</h1>
142
- <MTag value="${zh ? '正常' : 'Active'}" severity="success" />
143
- </div>
144
- <MButton severity="primary" outlined>${zh ? '编辑' : 'Edit'}</MButton>
145
- </header>
146
- <MCard>
147
- <MDivider />
148
- <dl class="m-generated-details">
149
- <div><dt>${zh ? '名称' : 'Name'}</dt><dd>${zh ? '示例资源' : 'Example resource'}</dd></div>
150
- <div><dt>${zh ? '更新时间' : 'Updated'}</dt><dd>—</dd></div>
151
- </dl>
152
- </MCard>`;
102
+ const listContent = ` <MPageFilters :aria-label="${zh ? '筛选' : 'Filters'}">
103
+ <MSpace wrap>
104
+ <MInput v-model="keyword" placeholder="${zh ? '搜索关键词' : 'Search keyword'}" clearable style="width: 14rem" />
105
+ <MButton severity="primary">${zh ? '查询' : 'Search'}</MButton>
106
+ <MButton severity="secondary">${zh ? '重置' : 'Reset'}</MButton>
107
+ </MSpace>
108
+ </MPageFilters>
109
+ <MPageToolbar :title="title">
110
+ <template #actions>
111
+ <MButton severity="primary">${zh ? '新建' : 'Create'}</MButton>
112
+ </template>
113
+ </MPageToolbar>
114
+ <MTable :columns="columns" :rows="rows" :loading="loading" paginator :rows-per-page="10" striped bordered row-key="id">
115
+ <template #empty>
116
+ <p style="margin:0;padding:var(--m-space-8);text-align:center;color:var(--m-color-text-muted)">${zh ? '暂无数据' : 'No data yet'}</p>
117
+ </template>
118
+ </MTable>`;
119
+ const formContent = ` <MPageHeader :title="title" :description="${zh ? '填写表单并保存。' : 'Fill in the form and save.'}" />
120
+ <MPageSection variant="form">
121
+ <MForm @submit.prevent="submit">
122
+ <MFormItem label="${zh ? '名称' : 'Name'}" name="name" required>
123
+ <MInput v-model="model.name" fluid />
124
+ </MFormItem>
125
+ <MPageSection variant="actions">
126
+ <MButton native-type="submit" severity="primary" :loading="loading">${zh ? '保存' : 'Save'}</MButton>
127
+ <MButton severity="secondary">${zh ? '取消' : 'Cancel'}</MButton>
128
+ </MPageSection>
129
+ </MForm>
130
+ </MPageSection>`;
131
+ const dashboardContent = ` <MPageHeader :title="title" />
132
+ <MGrid :cols="2" :x-gap="16" :y-gap="16" responsive="screen">
133
+ <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
134
+ <MPageStat :label="metric.label" :value="metric.value" icon="activity" />
135
+ </MGridItem>
136
+ </MGrid>
137
+ <MCard :title="${zh ? '趋势概览' : 'Trend overview'}">
138
+ <MSkeleton v-if="loading" height="8rem" />
139
+ <MPagePlaceholder v-else :description="${zh ? '接入图表或业务组件。' : 'Connect charts or business widgets here.'}" aria-label="${zh ? '图表占位' : 'Chart placeholder'}" />
140
+ </MCard>`;
141
+ const detailContent = ` <MPageToolbar :title="title">
142
+ <template #actions>
143
+ <MButton severity="primary" outlined>${zh ? '编辑' : 'Edit'}</MButton>
144
+ </template>
145
+ </MPageToolbar>
146
+ <MTag value="${zh ? '正常' : 'Active'}" severity="success" />
147
+ <MCard>
148
+ <MDivider />
149
+ <p style="margin:0;color:var(--m-color-text-muted)">${zh ? '示例资源详情' : 'Example resource details'}</p>
150
+ </MCard>`;
153
151
  const settingsContent = ` <h1 class="m-generated-title">${title}</h1>
154
152
  <MTabs :value="'general'" :items="[{ label: '${zh ? '常规' : 'General'}', value: 'general' }]" />
155
153
  <MForm class="m-generated-form" @submit.prevent="submit">
@@ -161,14 +159,16 @@ async function submit() {
161
159
  let innerTemplate = '';
162
160
  if (isList) {
163
161
  innerTemplate = `<MConfigProvider :locale="zhCN">
164
- <MLayout has-sider class="m-generated-page">
165
- <MLayoutSider class="m-generated-sider" />
162
+ <MLayout has-sider fill-viewport>
163
+ <MLayoutSider bordered />
166
164
  <MLayout>
167
- <MLayoutHeader class="m-generated-header">
165
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
168
166
  <MBreadcrumb :model="[{ label: '${zh ? '首页' : 'Home'}', to: '/' }, { label: '${title}' }]" />
169
167
  </MLayoutHeader>
170
- <MLayoutContent class="m-generated-content">
168
+ <MLayoutContent>
169
+ <MPageContent>
171
170
  ${listContent}
171
+ </MPageContent>
172
172
  </MLayoutContent>
173
173
  </MLayout>
174
174
  </MLayout>
@@ -176,13 +176,16 @@ ${listContent}
176
176
  }
177
177
  else if (useLayoutShell) {
178
178
  const content = isDashboard ? dashboardContent : isDetail ? detailContent : isSettings ? settingsContent : formContent;
179
+ const pageContentAttrs = isForm || isSettings ? ' width="narrow"' : isDashboard ? ' density="spacious"' : '';
179
180
  innerTemplate = `<MConfigProvider :locale="zhCN">
180
- <MLayout class="m-generated-page">
181
- <MLayoutHeader class="m-generated-header">
181
+ <MLayout fill-viewport>
182
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
182
183
  <MBreadcrumb :model="[{ label: '${zh ? '首页' : 'Home'}', to: '/' }, { label: '${title}' }]" />
183
184
  </MLayoutHeader>
184
- <MLayoutContent class="m-generated-content">
185
+ <MLayoutContent>
186
+ <MPageContent${pageContentAttrs}>
185
187
  ${content}
188
+ </MPageContent>
186
189
  </MLayoutContent>
187
190
  </MLayout>
188
191
  </MConfigProvider>`;
@@ -249,24 +252,9 @@ ${content}
249
252
  <p v-if="error" role="alert" class="m-generated-error">${'{{ error }}'}</p>
250
253
  </template>`;
251
254
  const style = `<style scoped>
252
- .m-generated-page { min-height: 100vh; background: var(--m-color-surface); }
253
- .m-generated-sider { border-right: 1px solid var(--m-color-border); }
254
- .m-generated-header { padding: var(--m-space-4) var(--m-space-6); border-bottom: 1px solid var(--m-color-border); }
255
- .m-generated-content { padding: var(--m-space-6); display: flex; flex-direction: column; gap: var(--m-space-4); }
256
- .m-generated-filters { padding: var(--m-space-4); background: color-mix(in srgb, var(--m-color-border) 25%, transparent); border-radius: var(--m-radius-md); border: 1px solid var(--m-color-border); }
257
- .m-generated-toolbar, .m-generated-actions { display: flex; gap: var(--m-space-3); align-items: center; justify-content: space-between; flex-wrap: wrap; }
258
- .m-generated-title { margin: 0; font-size: var(--m-font-size-lg); font-weight: 600; color: var(--m-color-text); }
259
- .m-generated-intro { margin-bottom: var(--m-space-2); }
260
- .m-generated-form { padding: var(--m-space-6); border: 1px solid var(--m-color-border); border-radius: var(--m-radius-md); box-shadow: var(--m-shadow-sm); }
261
- .m-generated-auth { display: grid; place-items: center; padding: var(--m-space-8); max-width: 24rem; margin: 0 auto; }
262
- .m-generated-metric { display: block; font-size: var(--m-font-size-lg); margin: var(--m-space-2) 0; }
263
- .m-generated-details { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--m-space-4); margin: 0; }
264
- .m-generated-details dt { color: var(--m-color-text-muted); font-size: var(--m-font-size-sm); }
265
- .m-generated-details dd { margin: var(--m-space-1) 0 0; }
255
+ .m-generated-auth { display: grid; place-items: center; padding: var(--m-space-8); max-width: 24rem; margin: 0 auto; min-height: 100vh; }
266
256
  .m-generated-empty { display: grid; gap: var(--m-space-2); justify-items: center; padding: var(--m-space-8); text-align: center; }
267
- .m-generated-muted { margin: 0; color: var(--m-color-text-muted); }
268
257
  .m-generated-error { color: var(--m-color-danger); padding: 0 var(--m-space-6); }
269
- @media (max-width: 48rem) { .m-generated-content { padding: var(--m-space-4); } .m-generated-details { grid-template-columns: 1fr; } }
270
258
  </style>`;
271
259
  return { script, template, style };
272
260
  }
@@ -430,6 +418,20 @@ export function createToolHandlers(catalog = loadCatalog()) {
430
418
  }
431
419
  }
432
420
  }
421
+ if (scope === 'all' || scope === 'snippets') {
422
+ for (const snippet of pageSnippets) {
423
+ const score = scorePageSnippet(snippet, query);
424
+ if (score > 0) {
425
+ hits.push({
426
+ type: 'snippet',
427
+ id: snippet.id,
428
+ title: locale === 'en-US' ? snippet.titleEn : snippet.title,
429
+ score,
430
+ snippet: locale === 'en-US' ? snippet.descriptionEn : snippet.description,
431
+ });
432
+ }
433
+ }
434
+ }
433
435
  if (scope === 'all' || scope === 'patterns') {
434
436
  for (const pattern of pagePatterns) {
435
437
  const score = scorePattern(pattern, query);
@@ -678,9 +680,14 @@ export function createToolHandlers(catalog = loadCatalog()) {
678
680
  interactionRules: best.pattern.interactionRules,
679
681
  avoid: best.pattern.avoid,
680
682
  alternatives: ranked.slice(1, 3).filter((item) => item.score > 0).map((item) => ({ id: item.pattern.id, score: item.score })),
681
- nextStep: locale === 'en-US'
682
- ? `Read goldenPage (${best.pattern.goldenPage || 'none'}) and matchedPattern with get_pattern, then verify component APIs with get_component or get_example. Pass includeScaffold: true for a starter Vue file aligned with MLayout shell.`
683
- : `先阅读 goldenPage(${best.pattern.goldenPage || '无'})并用 get_pattern 读取 matchedPattern,再用 get_component 或 get_example 核对组件 API。需要 starter 代码时传 includeScaffold: true(已对齐 MLayout 骨架)。`,
683
+ nextStep: (() => {
684
+ const goldenId = best.pattern.goldenPage?.split('/').pop()?.replace(/\.vue$/i, '') ||
685
+ listGoldenPages().find((item) => item.patternId === best.pattern.id)?.id ||
686
+ best.pattern.id;
687
+ return locale === 'en-US'
688
+ ? `Call get_golden_page("${goldenId}"), get_pattern, get_design_rules, then get_component/get_example. Pass includeScaffold: true for starter code. Optionally validate_page.`
689
+ : `调用 get_golden_page("${goldenId}")、get_pattern、get_design_rules,再用 get_component/get_example 查 API。需要 starter 时传 includeScaffold: true。可选用 validate_page。`;
690
+ })(),
684
691
  };
685
692
  if (args.includeScaffold) {
686
693
  const code = generatedPageCode(best.pattern.id, args.intent, locale);
@@ -697,11 +704,226 @@ export function createToolHandlers(catalog = loadCatalog()) {
697
704
  }
698
705
  return textResult(result);
699
706
  }
707
+ function getGoldenPage(args) {
708
+ const locale = resolveLocale(args.mode);
709
+ const payload = readGoldenPageSource(args.page);
710
+ if (!payload) {
711
+ return textResult({
712
+ error: `Golden page not found: ${args.page}`,
713
+ availablePages: listGoldenPages().map((item) => item.id),
714
+ });
715
+ }
716
+ return textResult({
717
+ id: payload.record.id,
718
+ patternId: payload.record.patternId,
719
+ title: locale === 'en-US' ? payload.record.titleEn : payload.record.title,
720
+ file: payload.record.file,
721
+ source: payload.source,
722
+ nextStep: locale === 'en-US'
723
+ ? 'Copy this structure and replace business data. Prefer MPage* components; see get_design_rules for page standards.'
724
+ : '复制此结构并替换业务数据。优先使用 MPage* 组件;页面标准见 get_design_rules。',
725
+ });
726
+ }
727
+ function listGoldenPageCatalog(args = {}) {
728
+ const locale = resolveLocale(args.mode);
729
+ return textResult({
730
+ items: listGoldenPages().map((item) => ({
731
+ id: item.id,
732
+ patternId: item.patternId,
733
+ title: locale === 'en-US' ? item.titleEn : item.title,
734
+ file: item.file,
735
+ })),
736
+ });
737
+ }
738
+ function listPageSnippets(args) {
739
+ const locale = resolveLocale(args.mode);
740
+ const result = filterPageSnippets(args);
741
+ return textResult({
742
+ ...result,
743
+ items: result.items.map((item) => ({
744
+ id: item.id,
745
+ title: locale === 'en-US' ? item.titleEn : item.title,
746
+ description: locale === 'en-US' ? item.descriptionEn : item.description,
747
+ pageTypes: item.pageTypes,
748
+ keywords: item.keywords,
749
+ imports: item.imports,
750
+ })),
751
+ });
752
+ }
753
+ function getPageSnippet(args) {
754
+ const locale = resolveLocale(args.mode);
755
+ const ranked = pageSnippets
756
+ .map((snippet) => ({ snippet, score: scorePageSnippet(snippet, args.section) }))
757
+ .sort((a, b) => b.score - a.score || a.snippet.id.localeCompare(b.snippet.id));
758
+ const match = findPageSnippet(args.section) || (ranked[0]?.score ? ranked[0].snippet : undefined);
759
+ if (!match || (ranked[0]?.score ?? 0) === 0) {
760
+ return textResult({
761
+ error: `Page snippet not found: ${args.section}`,
762
+ availableSnippets: pageSnippets.map((item) => item.id),
763
+ suggestion: locale === 'en-US'
764
+ ? 'Try list_page_snippets with query like "filters", "toolbar", or pageType="list".'
765
+ : '可尝试 list_page_snippets,query 如 "filters"、"toolbar",或 pageType="list"。',
766
+ });
767
+ }
768
+ const imports = [...new Set(match.imports)].sort();
769
+ const payload = {
770
+ id: match.id,
771
+ title: locale === 'en-US' ? match.titleEn : match.title,
772
+ description: locale === 'en-US' ? match.descriptionEn : match.description,
773
+ pageTypes: match.pageTypes,
774
+ imports,
775
+ template: match.template,
776
+ rules: locale === 'en-US' ? match.rulesEn : match.rules,
777
+ avoid: locale === 'en-US' ? match.avoidEn : match.avoid,
778
+ nextStep: locale === 'en-US'
779
+ ? 'Merge this block into the existing page. Call get_component/get_example for unfamiliar imports, then validate_usage and validate_page.'
780
+ : '将此区块合并进现有页面。不熟悉的 import 用 get_component/get_example 核对,完成后 validate_usage / validate_page。',
781
+ };
782
+ if (match.scriptSetup)
783
+ payload.scriptSetup = match.scriptSetup;
784
+ if (args.includeScript) {
785
+ payload.vue = {
786
+ imports: `import { ${imports.join(', ')} } from 'morya-ui'`,
787
+ scriptSetup: match.scriptSetup || '',
788
+ template: match.template,
789
+ };
790
+ }
791
+ if (!findPageSnippet(args.section) && ranked.length > 1 && ranked[0]?.score) {
792
+ payload.matchedBy = 'query';
793
+ payload.alternatives = ranked
794
+ .slice(1, 4)
795
+ .filter((entry) => entry.score > 0)
796
+ .map((entry) => ({ id: entry.snippet.id, score: entry.score }));
797
+ }
798
+ return textResult(payload);
799
+ }
800
+ function validatePage(args) {
801
+ const locale = resolveLocale(args.mode);
802
+ const code = args.code || '';
803
+ const suggestions = [];
804
+ if (/#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})\b/i.test(code)) {
805
+ suggestions.push({
806
+ standardId: 'tokens',
807
+ type: 'raw-color',
808
+ message: locale === 'en-US'
809
+ ? 'Standard: prefer --m-* design tokens over hex colors.'
810
+ : '标准建议:颜色优先使用 --m-* 设计令牌,而非 hex 色值。',
811
+ });
812
+ }
813
+ if (/\brgb\s*\(/i.test(code) && !/--m-/.test(code)) {
814
+ suggestions.push({
815
+ standardId: 'tokens',
816
+ type: 'raw-color',
817
+ message: locale === 'en-US'
818
+ ? 'Standard: prefer --m-* tokens over raw rgb()/rgba() in page styles.'
819
+ : '标准建议:页面样式优先 --m-* 令牌,而非裸 rgb()/rgba()。',
820
+ });
821
+ }
822
+ if (/<MCard\b[^>]*>[\s\S]*?<MTable\b[^>]*\bbordered\b/i.test(code)) {
823
+ suggestions.push({
824
+ standardId: 'page-sections',
825
+ type: 'double-border',
826
+ message: locale === 'en-US'
827
+ ? 'Standard: place bordered MTable directly in MPageContent instead of wrapping it with MCard.'
828
+ : '标准建议:bordered 的 MTable 直接放在 MPageContent 内,通常不必再包 MCard。',
829
+ });
830
+ }
831
+ if (/<MLayoutContent\b/i.test(code) && !/<MPageContent\b/i.test(code)) {
832
+ suggestions.push({
833
+ standardId: 'layout-shell',
834
+ type: 'missing-page-content',
835
+ message: locale === 'en-US'
836
+ ? 'Standard: prefer MPageContent inside MLayoutContent for consistent page spacing.'
837
+ : '标准建议:MLayoutContent 内使用 MPageContent 统一页面间距。',
838
+ });
839
+ }
840
+ if ((/\bpage[-_](?:list|form|dashboard)?[-_]?(?:filters|toolbar|content|header)\b/i.test(code) ||
841
+ /class="[^"]*filters/i.test(code)) &&
842
+ !/<MPage(?:Content|Filters|Toolbar|Header|Section|Stat)\b/i.test(code)) {
843
+ suggestions.push({
844
+ standardId: 'page-sections',
845
+ type: 'custom-page-css',
846
+ message: locale === 'en-US'
847
+ ? 'Standard: prefer MPageFilters, MPageToolbar, MPageHeader, or MPageSection over custom page section CSS.'
848
+ : '标准建议:筛选/工具栏/表单区块优先 MPage* 组件,少写自定义 page section CSS。',
849
+ });
850
+ }
851
+ if (/<MPageFilters\b/i.test(code) && /<MCard\b[^>]*>[\s\S]*?<MPageFilters\b/i.test(code)) {
852
+ suggestions.push({
853
+ standardId: 'page-sections',
854
+ type: 'redundant-wrapper',
855
+ message: locale === 'en-US'
856
+ ? 'Standard: MPageFilters already provides a surface; wrapping it in MCard is usually redundant.'
857
+ : '标准建议:MPageFilters 已自带表面样式,通常不必再用 MCard 包裹。',
858
+ });
859
+ }
860
+ if (/style="[^"]*overflow(?:-y|-x)?\s*:\s*(auto|scroll)/i.test(code) ||
861
+ /<style\b[^>]*>[\s\S]*?overflow(?:-y|-x)?\s*:\s*(auto|scroll)/i.test(code)) {
862
+ suggestions.push({
863
+ standardId: 'scroll',
864
+ type: 'native-scroll',
865
+ message: locale === 'en-US'
866
+ ? 'Standard: prefer MScrollbar or MLayout scroll regions over overflow:auto/scroll in page styles.'
867
+ : '标准建议:页面滚动优先 MLayout 主滚动或 MScrollbar,少写 overflow:auto/scroll。',
868
+ });
869
+ }
870
+ if (/::-webkit-scrollbar|scrollbar-width\s*:/i.test(code)) {
871
+ suggestions.push({
872
+ standardId: 'scroll',
873
+ type: 'custom-scrollbar-css',
874
+ message: locale === 'en-US'
875
+ ? 'Standard: prefer MScrollbar or layout defaults over native scrollbar CSS in product code.'
876
+ : '标准建议:业务代码优先 MScrollbar 或 Layout 默认滚动,少定制原生滚动条样式。',
877
+ });
878
+ }
879
+ if (/min-height\s*:\s*100vh/i.test(code) && !/<MLayout\b[^>]*fill-viewport/i.test(code)) {
880
+ suggestions.push({
881
+ standardId: 'layout-shell',
882
+ type: 'viewport-height',
883
+ message: locale === 'en-US'
884
+ ? 'Standard: prefer MLayout fillViewport over hand-written min-height:100vh page shells.'
885
+ : '标准建议:整页高度优先 MLayout fillViewport,少写 min-height:100vh。',
886
+ });
887
+ }
888
+ return textResult({
889
+ ok: true,
890
+ advisory: true,
891
+ summary: locale === 'en-US'
892
+ ? suggestions.length === 0
893
+ ? 'No deviations from page standards detected.'
894
+ : `${suggestions.length} advisory suggestion(s).`
895
+ : suggestions.length === 0
896
+ ? '未发现与页面标准的明显偏差。'
897
+ : `${suggestions.length} 条参考建议。`,
898
+ suggestions,
899
+ nextStep: locale === 'en-US'
900
+ ? 'See get_design_rules.standards for the full recommended page standards.'
901
+ : '完整页面标准见 get_design_rules.standards。',
902
+ });
903
+ }
700
904
  function getDesignRules(args = {}) {
701
905
  const locale = resolveLocale(args.mode);
702
- if (locale === 'zh-CN')
703
- return textResult(designRules);
906
+ if (locale === 'zh-CN') {
907
+ return textResult({
908
+ meta: designRules.meta,
909
+ standards: designRules.standards,
910
+ tokens: designRules.tokens,
911
+ composition: designRules.composition,
912
+ actions: designRules.actions,
913
+ status: designRules.status,
914
+ feedback: designRules.feedback,
915
+ global: designRules.global,
916
+ });
917
+ }
704
918
  return textResult({
919
+ meta: designRules.meta,
920
+ standards: designRules.standards.map((item) => ({
921
+ id: item.id,
922
+ title: item.titleEn,
923
+ recommend: item.recommendEn,
924
+ discouraged: item.discouragedEn,
925
+ mcp: item.mcp,
926
+ })),
705
927
  tokens: {
706
928
  colors: ['--m-color-primary', '--m-color-surface', '--m-color-text', '--m-color-border'],
707
929
  spacing: '--m-space-*',
@@ -709,6 +931,7 @@ export function createToolHandlers(catalog = loadCatalog()) {
709
931
  typography: '--m-font-size-xs/sm/md/lg',
710
932
  motion: '--m-motion-fast/normal',
711
933
  },
934
+ composition: designRules.composition,
712
935
  actions: {
713
936
  primary: { component: 'MButton', props: ['omit severity or use primary'] },
714
937
  secondary: { component: 'MButton', props: ['severity="secondary"', 'outlined or text'] },
@@ -724,10 +947,10 @@ export function createToolHandlers(catalog = loadCatalog()) {
724
947
  doc: 'docs/feedback-message-vs-toast.md',
725
948
  },
726
949
  global: [
727
- 'Prefer library components and --m-* tokens; do not maintain a second color system.',
728
- 'Default action feedback to message; do not use toast with summary-only text.',
729
- 'Icon-only buttons must provide aria-label or ariaLabel.',
730
- 'Form controls must have a visible label or an equivalent accessible name.',
950
+ 'Prefer library components and --m-* tokens.',
951
+ 'Default action feedback to message; prefer message over toast for single-line-only text.',
952
+ 'Icon-only buttons should provide aria-label or ariaLabel.',
953
+ 'Form controls should have a visible label or an equivalent accessible name.',
731
954
  'Overlays teleport to body by default; only change appendTo for a clear layout constraint.',
732
955
  'Prefer documented component variants over deep CSS overrides.',
733
956
  ],
@@ -952,6 +1175,11 @@ export function createToolHandlers(catalog = loadCatalog()) {
952
1175
  'recommend_page',
953
1176
  'get_design_rules',
954
1177
  'recommend_component',
1178
+ 'list_golden_pages',
1179
+ 'get_golden_page',
1180
+ 'list_page_snippets',
1181
+ 'get_page_snippet',
1182
+ 'validate_page',
955
1183
  'version',
956
1184
  ],
957
1185
  });
@@ -970,6 +1198,11 @@ export function createToolHandlers(catalog = loadCatalog()) {
970
1198
  recommendPage,
971
1199
  getDesignRules,
972
1200
  recommendComponent,
1201
+ listGoldenPageCatalog,
1202
+ getGoldenPage,
1203
+ listPageSnippets,
1204
+ getPageSnippet,
1205
+ validatePage,
973
1206
  version,
974
1207
  };
975
1208
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morya-ui/mcp",
3
3
  "type": "module",
4
- "version": "0.2.2",
4
+ "version": "0.2.4",
5
5
  "description": "MCP server for morya-ui — component docs, examples, and usage guidance for AI coding agents.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/morya-space/morya-ui/tree/main/packages/ui-mcp#readme",