@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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-09-14T01:38:04.233Z",
3
- "catalogGeneratedAt": "2026-09-14T01:38:01.275Z",
2
+ "generatedAt": "2026-09-14T07:44:52.185Z",
3
+ "catalogGeneratedAt": "2026-09-14T07:44:48.362Z",
4
4
  "components": [
5
5
  {
6
6
  "id": "Accordion",
@@ -1762,6 +1762,31 @@
1762
1762
  "missing": []
1763
1763
  }
1764
1764
  },
1765
+ {
1766
+ "id": "Page",
1767
+ "exportName": "MPageContent",
1768
+ "examples": 6,
1769
+ "props": {
1770
+ "total": 0,
1771
+ "covered": 0,
1772
+ "missing": []
1773
+ },
1774
+ "events": {
1775
+ "total": 0,
1776
+ "covered": 0,
1777
+ "missing": []
1778
+ },
1779
+ "slots": {
1780
+ "total": 0,
1781
+ "covered": 0,
1782
+ "missing": []
1783
+ },
1784
+ "methods": {
1785
+ "total": 0,
1786
+ "covered": 0,
1787
+ "missing": []
1788
+ }
1789
+ },
1765
1790
  {
1766
1791
  "id": "Pagination",
1767
1792
  "exportName": "MPagination",
@@ -2075,10 +2100,8 @@
2075
2100
  },
2076
2101
  "slots": {
2077
2102
  "total": 1,
2078
- "covered": 0,
2079
- "missing": [
2080
- "default"
2081
- ]
2103
+ "covered": 1,
2104
+ "missing": []
2082
2105
  },
2083
2106
  "methods": {
2084
2107
  "total": 0,
@@ -3075,8 +3098,8 @@
3075
3098
  }
3076
3099
  ],
3077
3100
  "summary": {
3078
- "components": 88,
3101
+ "components": 89,
3079
3102
  "componentsWithMissingCoverage": 84,
3080
- "totalExamples": 751
3103
+ "totalExamples": 757
3081
3104
  }
3082
3105
  }
@@ -0,0 +1,103 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 黄金样例:仪表盘页
4
+ * @see DESIGN.md §3
5
+ */
6
+ import {
7
+ MBreadcrumb,
8
+ MCard,
9
+ MConfigProvider,
10
+ MGrid,
11
+ MGridItem,
12
+ MLayout,
13
+ MLayoutContent,
14
+ MLayoutHeader,
15
+ MPageContent,
16
+ MPageHeader,
17
+ MPagePlaceholder,
18
+ MPageStat,
19
+ MTable,
20
+ MTag,
21
+ zhCN,
22
+ } from 'morya-ui'
23
+
24
+ const stats = [
25
+ { label: '总用户', value: '12,480', trend: '+8.2%', icon: 'users' },
26
+ { label: '今日活跃', value: '1,926', trend: '+3.1%', icon: 'activity' },
27
+ { label: '待处理工单', value: '47', trend: '-12%', trendSeverity: 'warn' as const, icon: 'clipboard' },
28
+ { label: '系统健康', value: '99.9%', trend: '稳定', trendSeverity: 'secondary' as const, icon: 'heart' },
29
+ ]
30
+
31
+ const recentColumns = [
32
+ { key: 'id', label: '工单号', width: 96 },
33
+ { key: 'title', label: '标题' },
34
+ { key: 'priority', label: '优先级', width: 96 },
35
+ { key: 'status', label: '状态', width: 96 },
36
+ ]
37
+
38
+ const recentRows = [
39
+ { id: 'WO-1024', title: '登录异常反馈', priority: 'high', status: 'open' },
40
+ { id: 'WO-1023', title: '导出任务超时', priority: 'medium', status: 'progress' },
41
+ { id: 'WO-1022', title: '权限配置咨询', priority: 'low', status: 'done' },
42
+ ]
43
+
44
+ function prioritySeverity(p: string) {
45
+ if (p === 'high') return 'danger'
46
+ if (p === 'medium') return 'warn'
47
+ return 'secondary'
48
+ }
49
+
50
+ function statusLabel(s: string) {
51
+ if (s === 'open') return '待处理'
52
+ if (s === 'progress') return '进行中'
53
+ return '已完成'
54
+ }
55
+ </script>
56
+
57
+ <template>
58
+ <MConfigProvider :locale="zhCN">
59
+ <MLayout fill-viewport>
60
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
61
+ <MBreadcrumb :model="[{ label: '首页' }, { label: '仪表盘' }]" />
62
+ </MLayoutHeader>
63
+
64
+ <MLayoutContent>
65
+ <MPageContent density="spacious">
66
+ <MPageHeader title="仪表盘" />
67
+
68
+ <MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
69
+ <MGridItem v-for="item in stats" :key="item.label" :span="1">
70
+ <MPageStat
71
+ :label="item.label"
72
+ :value="item.value"
73
+ :trend="item.trend"
74
+ :trend-severity="item.trendSeverity ?? 'primary'"
75
+ :icon="item.icon"
76
+ />
77
+ </MGridItem>
78
+ </MGrid>
79
+
80
+ <MGrid :cols="2" :x-gap="16" :y-gap="16">
81
+ <MGridItem :span="1">
82
+ <MCard title="趋势概览">
83
+ <MPagePlaceholder aria-label="图表占位" description="图表区域(接入 ECharts / 业务组件)" />
84
+ </MCard>
85
+ </MGridItem>
86
+ <MGridItem :span="1">
87
+ <MCard title="最近工单">
88
+ <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered>
89
+ <template #cell-priority="{ value }">
90
+ <MTag :value="String(value)" :severity="prioritySeverity(String(value))" />
91
+ </template>
92
+ <template #cell-status="{ value }">
93
+ <MTag :value="statusLabel(String(value))" severity="info" />
94
+ </template>
95
+ </MTable>
96
+ </MCard>
97
+ </MGridItem>
98
+ </MGrid>
99
+ </MPageContent>
100
+ </MLayoutContent>
101
+ </MLayout>
102
+ </MConfigProvider>
103
+ </template>
@@ -0,0 +1,103 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 黄金样例:表单页
4
+ * @see DESIGN.md §3
5
+ */
6
+ import {
7
+ MBreadcrumb,
8
+ MButton,
9
+ MConfigProvider,
10
+ MDatePicker,
11
+ MForm,
12
+ MFormItem,
13
+ MInput,
14
+ MLayout,
15
+ MLayoutContent,
16
+ MLayoutHeader,
17
+ MPageContent,
18
+ MPageHeader,
19
+ MPageSection,
20
+ MSelect,
21
+ MSpace,
22
+ MSwitch,
23
+ MTextarea,
24
+ zhCN,
25
+ } from 'morya-ui'
26
+ import { reactive, ref } from 'vue'
27
+
28
+ const submitting = ref(false)
29
+
30
+ const model = reactive({
31
+ name: '',
32
+ email: '',
33
+ role: undefined as string | undefined,
34
+ active: true,
35
+ joinedAt: null as string | null,
36
+ bio: '',
37
+ })
38
+
39
+ const roleOptions = [
40
+ { label: '管理员', value: 'admin' },
41
+ { label: '成员', value: 'member' },
42
+ ]
43
+
44
+ async function onSubmit() {
45
+ submitting.value = true
46
+ try {
47
+ // await api.save(model)
48
+ } finally {
49
+ submitting.value = false
50
+ }
51
+ }
52
+ </script>
53
+
54
+ <template>
55
+ <MConfigProvider :locale="zhCN">
56
+ <MLayout fill-viewport>
57
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
58
+ <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '用户管理', to: '/users' }, { label: '新建用户' }]" />
59
+ </MLayoutHeader>
60
+
61
+ <MLayoutContent>
62
+ <MPageContent width="narrow">
63
+ <MPageHeader title="新建用户" description="填写基本信息并分配角色。" />
64
+
65
+ <MPageSection variant="form">
66
+ <MForm @submit="onSubmit">
67
+ <MFormItem label="姓名" name="name" required>
68
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
69
+ </MFormItem>
70
+
71
+ <MFormItem label="邮箱" name="email" required>
72
+ <MInput v-model="model.email" type="email" placeholder="name@example.com" fluid />
73
+ </MFormItem>
74
+
75
+ <MFormItem label="角色" name="role" required>
76
+ <MSelect v-model="model.role" :options="roleOptions" placeholder="请选择角色" fluid />
77
+ </MFormItem>
78
+
79
+ <MFormItem label="入职日期" name="joinedAt">
80
+ <MDatePicker v-model="model.joinedAt" placeholder="选择日期" fluid />
81
+ </MFormItem>
82
+
83
+ <MFormItem label="启用账号" name="active">
84
+ <MSwitch v-model="model.active" />
85
+ </MFormItem>
86
+
87
+ <MFormItem label="简介" name="bio">
88
+ <MTextarea v-model="model.bio" :rows="4" placeholder="可选" fluid />
89
+ </MFormItem>
90
+
91
+ <MPageSection variant="actions">
92
+ <MSpace>
93
+ <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
94
+ <MButton severity="secondary">取消</MButton>
95
+ </MSpace>
96
+ </MPageSection>
97
+ </MForm>
98
+ </MPageSection>
99
+ </MPageContent>
100
+ </MLayoutContent>
101
+ </MLayout>
102
+ </MConfigProvider>
103
+ </template>
@@ -0,0 +1,117 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * 黄金样例:列表页
4
+ * @see DESIGN.md §3
5
+ */
6
+ import {
7
+ MBreadcrumb,
8
+ MButton,
9
+ MConfigProvider,
10
+ MInput,
11
+ MLayout,
12
+ MLayoutContent,
13
+ MLayoutHeader,
14
+ MLayoutSider,
15
+ MMenu,
16
+ MPageContent,
17
+ MPageFilters,
18
+ MPageToolbar,
19
+ MSelect,
20
+ MSpace,
21
+ MTable,
22
+ MTag,
23
+ zhCN,
24
+ } from 'morya-ui'
25
+ import { ref } from 'vue'
26
+
27
+ const keyword = ref('')
28
+ const status = ref<string | undefined>()
29
+
30
+ const statusOptions = [
31
+ { label: '全部', value: '' },
32
+ { label: '启用', value: 'active' },
33
+ { label: '停用', value: 'inactive' },
34
+ ]
35
+
36
+ const columns = [
37
+ { key: 'name', label: '名称' },
38
+ { key: 'status', label: '状态' },
39
+ { key: 'updatedAt', label: '更新时间' },
40
+ { key: 'actions', label: '操作', width: 128 },
41
+ ]
42
+
43
+ const rows = [
44
+ { id: '1', name: '示例项目 A', status: 'active', updatedAt: '2026-09-01' },
45
+ { id: '2', name: '示例项目 B', status: 'inactive', updatedAt: '2026-08-28' },
46
+ { id: '3', name: '示例项目 C', status: 'active', updatedAt: '2026-08-25' },
47
+ { id: '4', name: '示例项目 D', status: 'active', updatedAt: '2026-08-20' },
48
+ { id: '5', name: '示例项目 E', status: 'inactive', updatedAt: '2026-08-15' },
49
+ ]
50
+ </script>
51
+
52
+ <template>
53
+ <MConfigProvider :locale="zhCN">
54
+ <MLayout has-sider fill-viewport>
55
+ <MLayoutSider bordered>
56
+ <MMenu :model="[{ label: '用户管理', key: 'users' }, { label: '角色管理', key: 'roles' }]" />
57
+ </MLayoutSider>
58
+
59
+ <MLayout>
60
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
61
+ <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '用户管理' }]" />
62
+ </MLayoutHeader>
63
+
64
+ <MLayoutContent>
65
+ <MPageContent>
66
+ <MPageFilters aria-label="筛选">
67
+ <MSpace wrap>
68
+ <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
69
+ <MSelect
70
+ v-model="status"
71
+ :options="statusOptions"
72
+ placeholder="状态"
73
+ clearable
74
+ style="width: 10rem"
75
+ />
76
+ <MButton severity="primary">查询</MButton>
77
+ <MButton severity="secondary">重置</MButton>
78
+ </MSpace>
79
+ </MPageFilters>
80
+
81
+ <MPageToolbar title="用户管理">
82
+ <template #actions>
83
+ <MButton severity="primary">新建用户</MButton>
84
+ </template>
85
+ </MPageToolbar>
86
+
87
+ <MTable
88
+ :columns="columns"
89
+ :rows="rows"
90
+ :rows-per-page="3"
91
+ paginator
92
+ striped
93
+ bordered
94
+ row-key="id"
95
+ aria-label="用户列表"
96
+ >
97
+ <template #cell-status="{ value }">
98
+ <MTag :value="value === 'active' ? '启用' : '停用'" :severity="value === 'active' ? 'success' : 'secondary'" />
99
+ </template>
100
+ <template #cell-actions>
101
+ <MSpace>
102
+ <MButton severity="secondary" size="small">编辑</MButton>
103
+ <MButton severity="danger" size="small">删除</MButton>
104
+ </MSpace>
105
+ </template>
106
+ <template #empty>
107
+ <p style="margin: 0; padding: var(--m-space-8); text-align: center; color: var(--m-color-text-muted)">
108
+ 暂无用户数据
109
+ </p>
110
+ </template>
111
+ </MTable>
112
+ </MPageContent>
113
+ </MLayoutContent>
114
+ </MLayout>
115
+ </MLayout>
116
+ </MConfigProvider>
117
+ </template>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { filterPageSnippets, findPageSnippet, scorePageSnippet } from '../page-snippets.js';
3
+ describe('page snippets catalog', () => {
4
+ it('finds snippet by exact id', () => {
5
+ expect(findPageSnippet('form-actions')?.id).toBe('form-actions');
6
+ });
7
+ it('scores keyword matches for fuzzy lookup', () => {
8
+ const snippet = findPageSnippet('list-filters');
9
+ expect(scorePageSnippet(snippet, 'filters')).toBeGreaterThan(0);
10
+ expect(scorePageSnippet(snippet, 'search')).toBeGreaterThan(0);
11
+ });
12
+ it('filters snippets by page type', () => {
13
+ const result = filterPageSnippets({ pageType: 'dashboard' });
14
+ expect(result.items.some((item) => item.id === 'dashboard-kpi-grid')).toBe(true);
15
+ expect(result.items.some((item) => item.id === 'list-filters')).toBe(false);
16
+ });
17
+ });
@@ -82,8 +82,84 @@ describe('@morya-ui/mcp handlers', () => {
82
82
  }));
83
83
  expect(result.matchedPattern).toBe('dashboard');
84
84
  expect(result.scaffold.files.component).toContain('MGrid');
85
+ expect(result.scaffold.files.component).toContain('MPageStat');
85
86
  expect(result.scaffold.files.component).toContain('MSkeleton');
86
87
  });
88
+ it('returns a golden page source sample', () => {
89
+ const result = read(handlers.getGoldenPage({ page: 'list-page' }));
90
+ expect(result.id).toBe('list-page');
91
+ expect(result.source).toContain('MPageContent');
92
+ expect(result.source).toContain('MPageFilters');
93
+ });
94
+ it('returns a page section snippet by keyword', () => {
95
+ const result = read(handlers.getPageSnippet({ section: 'filters' }));
96
+ expect(result.id).toBe('list-filters');
97
+ expect(result.template).toContain('MPageFilters');
98
+ expect(result.imports).toContain('MPageFilters');
99
+ });
100
+ it('lists page snippets filtered by page type', () => {
101
+ const result = read(handlers.listPageSnippets({ pageType: 'form', limit: 10 }));
102
+ expect(result.items.some((item) => item.id === 'form-actions')).toBe(true);
103
+ expect(result.items.some((item) => item.id === 'page-content-form')).toBe(true);
104
+ expect(result.items.some((item) => item.id === 'list-filters')).toBe(false);
105
+ });
106
+ it('searches page snippets scope', () => {
107
+ const result = read(handlers.search({ query: 'toolbar', scope: 'snippets', limit: 5 }));
108
+ expect(result.items.some((item) => item.type === 'snippet' && item.id === 'list-toolbar')).toBe(true);
109
+ });
110
+ it('suggests double-border page composition as advisory standard', () => {
111
+ const result = read(handlers.validatePage({
112
+ code: '<MLayoutContent><MCard><MTable bordered /></MCard></MLayoutContent>',
113
+ }));
114
+ expect(result.ok).toBe(true);
115
+ expect(result.advisory).toBe(true);
116
+ expect(result.suggestions.some((item) => item.type === 'double-border')).toBe(true);
117
+ });
118
+ it('suggests MScrollbar over inline overflow scroll styles', () => {
119
+ const result = read(handlers.validatePage({
120
+ code: '<div style="overflow:auto;height:20rem"><MTable /></div>',
121
+ }));
122
+ expect(result.ok).toBe(true);
123
+ expect(result.suggestions.some((item) => item.type === 'native-scroll' && item.standardId === 'scroll')).toBe(true);
124
+ });
125
+ it('suggests MScrollbar over overflow in style blocks', () => {
126
+ const result = read(handlers.validatePage({
127
+ code: '<template><div class="panel">x</div></template><style>.panel { max-height: 20rem; overflow-y: auto; }</style>',
128
+ }));
129
+ expect(result.ok).toBe(true);
130
+ expect(result.suggestions.some((item) => item.type === 'native-scroll')).toBe(true);
131
+ });
132
+ it('returns scrollable-panel snippet for scroll queries', () => {
133
+ const result = read(handlers.getPageSnippet({ section: 'scrollable-panel' }));
134
+ expect(result.id).toBe('scrollable-panel');
135
+ expect(result.template).toContain('MScrollbar');
136
+ expect(result.imports).toContain('MScrollbar');
137
+ });
138
+ it('keeps recommend_page focused on pattern matching', () => {
139
+ const result = read(handlers.recommendPage({ intent: '用户列表页', pageType: 'list' }));
140
+ expect(result.matchedPattern).toBe('admin-list');
141
+ expect(result.nextStep).toContain('get_design_rules');
142
+ expect(result.pageStandards).toBeUndefined();
143
+ expect(result.scrollStandards).toBeUndefined();
144
+ });
145
+ it('keeps get_golden_page focused on source code', () => {
146
+ const result = read(handlers.getGoldenPage({ page: 'list-page' }));
147
+ expect(result.source).toContain('MLayout');
148
+ expect(result.nextStep).toContain('get_design_rules');
149
+ expect(result.pageStandards).toBeUndefined();
150
+ expect(result.scrollStandards).toBeUndefined();
151
+ });
152
+ it('exposes page standards from get_design_rules', () => {
153
+ const result = read(handlers.getDesignRules({ mode: 'zh-CN' }));
154
+ expect(result.meta.nature).toBe('recommended');
155
+ expect(result.standards.some((item) => item.id === 'scroll')).toBe(true);
156
+ expect(result.standards.some((item) => item.id === 'feedback')).toBe(true);
157
+ });
158
+ it('reads page scroll decision guide by id', () => {
159
+ const result = read(handlers.recommendComponent({ decision: 'page-scroll-choice' }));
160
+ expect(result.id).toBe('page-scroll-choice');
161
+ expect(result.options.some((option) => option.component === 'MScrollbar')).toBe(true);
162
+ });
87
163
  it('lists component decision guides when query is omitted', () => {
88
164
  const result = read(handlers.recommendComponent({ limit: 5 }));
89
165
  expect(result.kind).toBe('decisions');
@@ -103,7 +179,10 @@ describe('@morya-ui/mcp handlers', () => {
103
179
  expect(result.counts.decisions).toBeGreaterThan(0);
104
180
  expect(result.counts.resources).toBeGreaterThan(100);
105
181
  expect(result.counts.resourceTemplates).toBe(3);
106
- expect(result.tools).toHaveLength(13);
182
+ expect(result.tools).toHaveLength(18);
183
+ expect(result.tools).toContain('get_golden_page');
184
+ expect(result.tools).toContain('get_page_snippet');
185
+ expect(result.tools).toContain('validate_page');
107
186
  expect(result.tools).not.toContain('create_page');
108
187
  });
109
188
  });
package/dist/catalog.js CHANGED
@@ -25,6 +25,20 @@ export const componentAliases = {
25
25
  layoutcontent: 'Layout',
26
26
  layoutfooter: 'Layout',
27
27
  griditem: 'Grid',
28
+ pagecontent: 'Page',
29
+ pagefilters: 'Page',
30
+ pagetoolbar: 'Page',
31
+ pageheader: 'Page',
32
+ pagesection: 'Page',
33
+ pagestat: 'Page',
34
+ pageplaceholder: 'Page',
35
+ mpagecontent: 'Page',
36
+ mpagefilters: 'Page',
37
+ mpagetoolbar: 'Page',
38
+ mpageheader: 'Page',
39
+ mpagesection: 'Page',
40
+ mpagestat: 'Page',
41
+ mpageplaceholder: 'Page',
28
42
  };
29
43
  const here = dirname(fileURLToPath(import.meta.url));
30
44
  export function loadCatalog() {