@morya-ui/mcp 0.3.5 → 0.3.6
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/LICENSE +21 -21
- package/README.md +154 -151
- package/bin/morya-ui-mcp.js +2 -2
- package/data/catalog.json +26 -26
- package/data/example-coverage.json +2 -2
- package/data/golden-pages/dashboard-page.vue +138 -138
- package/data/golden-pages/detail-page.vue +178 -178
- package/data/golden-pages/empty-state.vue +56 -56
- package/data/golden-pages/form-in-dialog.vue +166 -166
- package/data/golden-pages/form-page.vue +107 -107
- package/data/golden-pages/landing-page.vue +358 -358
- package/data/golden-pages/list-page-dense.vue +168 -168
- package/data/golden-pages/list-page-rail.vue +272 -272
- package/data/golden-pages/list-page.vue +222 -222
- package/data/golden-pages/login-page.vue +231 -231
- package/data/golden-pages/result-page.vue +84 -84
- package/data/golden-pages/settings-page.vue +163 -163
- package/data/golden-pages/wizard-form.vue +182 -182
- package/dist/__tests__/fidelity.test.d.ts +1 -0
- package/dist/__tests__/fidelity.test.js +88 -0
- package/dist/__tests__/page-snippets.test.js +13 -5
- package/dist/__tests__/tools.test.js +10 -8
- package/dist/index.js +46 -3
- package/dist/page-snippets.js +722 -635
- package/dist/reference-brief.d.ts +55 -0
- package/dist/reference-brief.js +468 -0
- package/dist/style-shells.d.ts +20 -0
- package/dist/style-shells.js +249 -0
- package/dist/tools.d.ts +26 -0
- package/dist/tools.js +464 -374
- package/package.json +1 -1
|
@@ -1,182 +1,182 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
* 黄金样例:分步向导(wizard-form)
|
|
4
|
-
* MStepper + 每步一个主任务;完成态可接 result-page。
|
|
5
|
-
* @see DESIGN.md · surfaces § Flow · pattern wizard-form
|
|
6
|
-
*/
|
|
7
|
-
import {
|
|
8
|
-
MBreadcrumb,
|
|
9
|
-
MButton,
|
|
10
|
-
MConfigProvider,
|
|
11
|
-
MForm,
|
|
12
|
-
MFormItem,
|
|
13
|
-
MInput,
|
|
14
|
-
MInputTags,
|
|
15
|
-
MLayout,
|
|
16
|
-
MLayoutContent,
|
|
17
|
-
MLayoutHeader,
|
|
18
|
-
MPageContent,
|
|
19
|
-
MPageHeader,
|
|
20
|
-
MPageSection,
|
|
21
|
-
MSelect,
|
|
22
|
-
MSpace,
|
|
23
|
-
MStepper,
|
|
24
|
-
message,
|
|
25
|
-
zhCN,
|
|
26
|
-
} from 'morya-ui'
|
|
27
|
-
import { computed, reactive, ref } from 'vue'
|
|
28
|
-
|
|
29
|
-
const activeStep = ref(0)
|
|
30
|
-
const submitting = ref(false)
|
|
31
|
-
|
|
32
|
-
const steps = [
|
|
33
|
-
{ label: '工作空间', description: '名称与地区' },
|
|
34
|
-
{ label: '邀请成员', description: '可选邮箱' },
|
|
35
|
-
{ label: '确认', description: '核对后创建' },
|
|
36
|
-
]
|
|
37
|
-
|
|
38
|
-
const model = reactive({
|
|
39
|
-
name: '',
|
|
40
|
-
region: undefined as string | undefined,
|
|
41
|
-
invites: [] as string[],
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
const regionOptions = [
|
|
45
|
-
{ label: '中国大陆', value: 'cn' },
|
|
46
|
-
{ label: '亚太', value: 'apac' },
|
|
47
|
-
{ label: '北美', value: 'na' },
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
const canNext = computed(() => {
|
|
51
|
-
if (activeStep.value === 0) return Boolean(model.name && model.region)
|
|
52
|
-
return true
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
function back() {
|
|
56
|
-
if (activeStep.value > 0) activeStep.value -= 1
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function next() {
|
|
60
|
-
if (!canNext.value) return
|
|
61
|
-
if (activeStep.value < steps.length - 1) activeStep.value += 1
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function finish() {
|
|
65
|
-
submitting.value = true
|
|
66
|
-
try {
|
|
67
|
-
// await api.createWorkspace(model)
|
|
68
|
-
message.success('工作空间已创建')
|
|
69
|
-
// 成功页可镜像 get_golden_page('result-page'),将 status 改为 success
|
|
70
|
-
} finally {
|
|
71
|
-
submitting.value = false
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
</script>
|
|
75
|
-
|
|
76
|
-
<template>
|
|
77
|
-
<MConfigProvider :locale="zhCN">
|
|
78
|
-
<MLayout fill-viewport>
|
|
79
|
-
<MLayoutHeader padding="var(--m-space-4) var(--m-space-6)">
|
|
80
|
-
<MBreadcrumb
|
|
81
|
-
:model="[
|
|
82
|
-
{ label: '首页', to: '/' },
|
|
83
|
-
{ label: '工作空间', to: '/workspaces' },
|
|
84
|
-
{ label: '创建' },
|
|
85
|
-
]"
|
|
86
|
-
/>
|
|
87
|
-
</MLayoutHeader>
|
|
88
|
-
|
|
89
|
-
<MLayoutContent>
|
|
90
|
-
<MPageContent width="narrow">
|
|
91
|
-
<MPageHeader
|
|
92
|
-
title="创建工作空间"
|
|
93
|
-
description="三步完成;每步只做一件事。Stepper 使用 steps(不是 items)。"
|
|
94
|
-
/>
|
|
95
|
-
|
|
96
|
-
<MStepper v-model="activeStep" :steps="steps" linear />
|
|
97
|
-
|
|
98
|
-
<MPageSection variant="form">
|
|
99
|
-
<MForm v-if="activeStep === 0" @submit.prevent="next">
|
|
100
|
-
<MFormItem label="名称" name="name" required>
|
|
101
|
-
<MInput v-model="model.name" placeholder="例如:青禾书房" fluid />
|
|
102
|
-
</MFormItem>
|
|
103
|
-
<MFormItem label="地区" name="region" required>
|
|
104
|
-
<MSelect
|
|
105
|
-
v-model="model.region"
|
|
106
|
-
:options="regionOptions"
|
|
107
|
-
placeholder="选择地区"
|
|
108
|
-
fluid
|
|
109
|
-
/>
|
|
110
|
-
</MFormItem>
|
|
111
|
-
</MForm>
|
|
112
|
-
|
|
113
|
-
<MForm v-else-if="activeStep === 1" @submit.prevent="next">
|
|
114
|
-
<MFormItem label="邀请邮箱" name="invites">
|
|
115
|
-
<MInputTags
|
|
116
|
-
v-model="model.invites"
|
|
117
|
-
placeholder="输入邮箱后回车"
|
|
118
|
-
/>
|
|
119
|
-
</MFormItem>
|
|
120
|
-
<p class="wizard-hint">
|
|
121
|
-
可跳过;稍后也可在成员管理中邀请。
|
|
122
|
-
</p>
|
|
123
|
-
</MForm>
|
|
124
|
-
|
|
125
|
-
<div v-else class="wizard-summary">
|
|
126
|
-
<p><strong>名称</strong> {{ model.name || '—' }}</p>
|
|
127
|
-
<p><strong>地区</strong> {{ model.region || '—' }}</p>
|
|
128
|
-
<p><strong>邀请</strong> {{ model.invites.length ? model.invites.join('、') : '暂无' }}</p>
|
|
129
|
-
</div>
|
|
130
|
-
|
|
131
|
-
<MPageSection variant="actions">
|
|
132
|
-
<MSpace>
|
|
133
|
-
<MButton
|
|
134
|
-
severity="secondary"
|
|
135
|
-
text
|
|
136
|
-
:disabled="activeStep === 0 || submitting"
|
|
137
|
-
@click="back"
|
|
138
|
-
>
|
|
139
|
-
上一步
|
|
140
|
-
</MButton>
|
|
141
|
-
<MButton
|
|
142
|
-
v-if="activeStep < steps.length - 1"
|
|
143
|
-
severity="primary"
|
|
144
|
-
:disabled="!canNext"
|
|
145
|
-
@click="next"
|
|
146
|
-
>
|
|
147
|
-
下一步
|
|
148
|
-
</MButton>
|
|
149
|
-
<MButton
|
|
150
|
-
v-else
|
|
151
|
-
severity="primary"
|
|
152
|
-
:loading="submitting"
|
|
153
|
-
@click="finish"
|
|
154
|
-
>
|
|
155
|
-
创建
|
|
156
|
-
</MButton>
|
|
157
|
-
</MSpace>
|
|
158
|
-
</MPageSection>
|
|
159
|
-
</MPageSection>
|
|
160
|
-
</MPageContent>
|
|
161
|
-
</MLayoutContent>
|
|
162
|
-
</MLayout>
|
|
163
|
-
</MConfigProvider>
|
|
164
|
-
</template>
|
|
165
|
-
|
|
166
|
-
<style scoped>
|
|
167
|
-
.wizard-hint,
|
|
168
|
-
.wizard-summary {
|
|
169
|
-
margin: 0;
|
|
170
|
-
color: var(--m-color-text-muted);
|
|
171
|
-
font-size: var(--m-font-size-sm);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
.wizard-summary {
|
|
175
|
-
display: grid;
|
|
176
|
-
gap: var(--m-space-2);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
.wizard-summary p {
|
|
180
|
-
margin: 0;
|
|
181
|
-
}
|
|
182
|
-
</style>
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:分步向导(wizard-form)
|
|
4
|
+
* MStepper + 每步一个主任务;完成态可接 result-page。
|
|
5
|
+
* @see DESIGN.md · surfaces § Flow · pattern wizard-form
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
MBreadcrumb,
|
|
9
|
+
MButton,
|
|
10
|
+
MConfigProvider,
|
|
11
|
+
MForm,
|
|
12
|
+
MFormItem,
|
|
13
|
+
MInput,
|
|
14
|
+
MInputTags,
|
|
15
|
+
MLayout,
|
|
16
|
+
MLayoutContent,
|
|
17
|
+
MLayoutHeader,
|
|
18
|
+
MPageContent,
|
|
19
|
+
MPageHeader,
|
|
20
|
+
MPageSection,
|
|
21
|
+
MSelect,
|
|
22
|
+
MSpace,
|
|
23
|
+
MStepper,
|
|
24
|
+
message,
|
|
25
|
+
zhCN,
|
|
26
|
+
} from 'morya-ui'
|
|
27
|
+
import { computed, reactive, ref } from 'vue'
|
|
28
|
+
|
|
29
|
+
const activeStep = ref(0)
|
|
30
|
+
const submitting = ref(false)
|
|
31
|
+
|
|
32
|
+
const steps = [
|
|
33
|
+
{ label: '工作空间', description: '名称与地区' },
|
|
34
|
+
{ label: '邀请成员', description: '可选邮箱' },
|
|
35
|
+
{ label: '确认', description: '核对后创建' },
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
const model = reactive({
|
|
39
|
+
name: '',
|
|
40
|
+
region: undefined as string | undefined,
|
|
41
|
+
invites: [] as string[],
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const regionOptions = [
|
|
45
|
+
{ label: '中国大陆', value: 'cn' },
|
|
46
|
+
{ label: '亚太', value: 'apac' },
|
|
47
|
+
{ label: '北美', value: 'na' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
const canNext = computed(() => {
|
|
51
|
+
if (activeStep.value === 0) return Boolean(model.name && model.region)
|
|
52
|
+
return true
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
function back() {
|
|
56
|
+
if (activeStep.value > 0) activeStep.value -= 1
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function next() {
|
|
60
|
+
if (!canNext.value) return
|
|
61
|
+
if (activeStep.value < steps.length - 1) activeStep.value += 1
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function finish() {
|
|
65
|
+
submitting.value = true
|
|
66
|
+
try {
|
|
67
|
+
// await api.createWorkspace(model)
|
|
68
|
+
message.success('工作空间已创建')
|
|
69
|
+
// 成功页可镜像 get_golden_page('result-page'),将 status 改为 success
|
|
70
|
+
} finally {
|
|
71
|
+
submitting.value = false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<template>
|
|
77
|
+
<MConfigProvider :locale="zhCN">
|
|
78
|
+
<MLayout fill-viewport>
|
|
79
|
+
<MLayoutHeader padding="var(--m-space-4) var(--m-space-6)">
|
|
80
|
+
<MBreadcrumb
|
|
81
|
+
:model="[
|
|
82
|
+
{ label: '首页', to: '/' },
|
|
83
|
+
{ label: '工作空间', to: '/workspaces' },
|
|
84
|
+
{ label: '创建' },
|
|
85
|
+
]"
|
|
86
|
+
/>
|
|
87
|
+
</MLayoutHeader>
|
|
88
|
+
|
|
89
|
+
<MLayoutContent>
|
|
90
|
+
<MPageContent width="narrow">
|
|
91
|
+
<MPageHeader
|
|
92
|
+
title="创建工作空间"
|
|
93
|
+
description="三步完成;每步只做一件事。Stepper 使用 steps(不是 items)。"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<MStepper v-model="activeStep" :steps="steps" linear />
|
|
97
|
+
|
|
98
|
+
<MPageSection variant="form">
|
|
99
|
+
<MForm v-if="activeStep === 0" @submit.prevent="next">
|
|
100
|
+
<MFormItem label="名称" name="name" required>
|
|
101
|
+
<MInput v-model="model.name" placeholder="例如:青禾书房" fluid />
|
|
102
|
+
</MFormItem>
|
|
103
|
+
<MFormItem label="地区" name="region" required>
|
|
104
|
+
<MSelect
|
|
105
|
+
v-model="model.region"
|
|
106
|
+
:options="regionOptions"
|
|
107
|
+
placeholder="选择地区"
|
|
108
|
+
fluid
|
|
109
|
+
/>
|
|
110
|
+
</MFormItem>
|
|
111
|
+
</MForm>
|
|
112
|
+
|
|
113
|
+
<MForm v-else-if="activeStep === 1" @submit.prevent="next">
|
|
114
|
+
<MFormItem label="邀请邮箱" name="invites">
|
|
115
|
+
<MInputTags
|
|
116
|
+
v-model="model.invites"
|
|
117
|
+
placeholder="输入邮箱后回车"
|
|
118
|
+
/>
|
|
119
|
+
</MFormItem>
|
|
120
|
+
<p class="wizard-hint">
|
|
121
|
+
可跳过;稍后也可在成员管理中邀请。
|
|
122
|
+
</p>
|
|
123
|
+
</MForm>
|
|
124
|
+
|
|
125
|
+
<div v-else class="wizard-summary">
|
|
126
|
+
<p><strong>名称</strong> {{ model.name || '—' }}</p>
|
|
127
|
+
<p><strong>地区</strong> {{ model.region || '—' }}</p>
|
|
128
|
+
<p><strong>邀请</strong> {{ model.invites.length ? model.invites.join('、') : '暂无' }}</p>
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<MPageSection variant="actions">
|
|
132
|
+
<MSpace>
|
|
133
|
+
<MButton
|
|
134
|
+
severity="secondary"
|
|
135
|
+
text
|
|
136
|
+
:disabled="activeStep === 0 || submitting"
|
|
137
|
+
@click="back"
|
|
138
|
+
>
|
|
139
|
+
上一步
|
|
140
|
+
</MButton>
|
|
141
|
+
<MButton
|
|
142
|
+
v-if="activeStep < steps.length - 1"
|
|
143
|
+
severity="primary"
|
|
144
|
+
:disabled="!canNext"
|
|
145
|
+
@click="next"
|
|
146
|
+
>
|
|
147
|
+
下一步
|
|
148
|
+
</MButton>
|
|
149
|
+
<MButton
|
|
150
|
+
v-else
|
|
151
|
+
severity="primary"
|
|
152
|
+
:loading="submitting"
|
|
153
|
+
@click="finish"
|
|
154
|
+
>
|
|
155
|
+
创建
|
|
156
|
+
</MButton>
|
|
157
|
+
</MSpace>
|
|
158
|
+
</MPageSection>
|
|
159
|
+
</MPageSection>
|
|
160
|
+
</MPageContent>
|
|
161
|
+
</MLayoutContent>
|
|
162
|
+
</MLayout>
|
|
163
|
+
</MConfigProvider>
|
|
164
|
+
</template>
|
|
165
|
+
|
|
166
|
+
<style scoped>
|
|
167
|
+
.wizard-hint,
|
|
168
|
+
.wizard-summary {
|
|
169
|
+
margin: 0;
|
|
170
|
+
color: var(--m-color-text-muted);
|
|
171
|
+
font-size: var(--m-font-size-sm);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.wizard-summary {
|
|
175
|
+
display: grid;
|
|
176
|
+
gap: var(--m-space-2);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.wizard-summary p {
|
|
180
|
+
margin: 0;
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { codeCoversBlock, mapReferenceBrief } from '../reference-brief.js';
|
|
3
|
+
import { findStyleShell, listStyleShells } from '../style-shells.js';
|
|
4
|
+
import { createToolHandlers } from '../tools.js';
|
|
5
|
+
function read(result) {
|
|
6
|
+
return JSON.parse(result.content[0].text);
|
|
7
|
+
}
|
|
8
|
+
describe('reference brief mapping', () => {
|
|
9
|
+
it('maps list brief to filters/table/status snippets', () => {
|
|
10
|
+
const mapped = mapReferenceBrief({
|
|
11
|
+
intent: '油井管理列表',
|
|
12
|
+
surface: 'list',
|
|
13
|
+
density: 'compact',
|
|
14
|
+
requiredBlocks: ['filters', 'table', 'status'],
|
|
15
|
+
primaryAction: '新建油井',
|
|
16
|
+
});
|
|
17
|
+
expect(mapped.surface).toBe('ops-list');
|
|
18
|
+
expect(mapped.density).toBe('compact');
|
|
19
|
+
expect(mapped.suggestedSnippets).toContain('list-filters-dense');
|
|
20
|
+
expect(mapped.suggestedSnippets).toContain('list-table');
|
|
21
|
+
expect(mapped.suggestedShellId).toBe('ops-quiet');
|
|
22
|
+
expect(mapped.craftCuesZh.some((line) => line.includes('新建油井'))).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
it('detects covered blocks in generated code', () => {
|
|
25
|
+
const code = '<MPageFilters /><MTable /><MStatus label="启用" />';
|
|
26
|
+
expect(codeCoversBlock(code, 'filters')).toBe(true);
|
|
27
|
+
expect(codeCoversBlock(code, 'table')).toBe(true);
|
|
28
|
+
expect(codeCoversBlock(code, 'status')).toBe(true);
|
|
29
|
+
expect(codeCoversBlock(code, 'kpi')).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe('style shells', () => {
|
|
33
|
+
it('lists and finds account-split shell with CSS', () => {
|
|
34
|
+
expect(listStyleShells('account').some((s) => s.id === 'account-split')).toBe(true);
|
|
35
|
+
const shell = findStyleShell('account-split');
|
|
36
|
+
expect(shell?.css).toContain('--m-color-primary');
|
|
37
|
+
expect(shell?.css).not.toMatch(/#[0-9a-f]{3,8}/i);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('fidelity tools', () => {
|
|
41
|
+
const handlers = createToolHandlers();
|
|
42
|
+
it('map_reference returns mapping and shell', () => {
|
|
43
|
+
const result = read(handlers.mapReference({
|
|
44
|
+
description: '登录页左侧品牌栏,右侧邮箱密码表单',
|
|
45
|
+
surface: 'account',
|
|
46
|
+
}));
|
|
47
|
+
expect(result.suggestedSnippets).toContain('auth-split-shell');
|
|
48
|
+
expect(result.styleShell?.id).toBe('account-split');
|
|
49
|
+
expect(result.styleShell?.css).toContain('login-brand');
|
|
50
|
+
});
|
|
51
|
+
it('recommend_page accepts density and brief', () => {
|
|
52
|
+
const result = read(handlers.recommendPage({
|
|
53
|
+
intent: '设备台账列表',
|
|
54
|
+
pageType: 'list',
|
|
55
|
+
density: 'compact',
|
|
56
|
+
brief: {
|
|
57
|
+
description: '高密筛选 + 状态列',
|
|
58
|
+
requiredBlocks: ['filters', 'table', 'status'],
|
|
59
|
+
},
|
|
60
|
+
}));
|
|
61
|
+
expect(result.density).toBe('compact');
|
|
62
|
+
expect(result.suggestedSnippets).toContain('list-filters-dense');
|
|
63
|
+
expect(result.referenceMapping?.mapping.length).toBeGreaterThan(0);
|
|
64
|
+
});
|
|
65
|
+
it('validate_page fails contract when brief blocks are missing', () => {
|
|
66
|
+
const result = read(handlers.validatePage({
|
|
67
|
+
code: '<MPageContent><MTable :rows="[]" /></MPageContent>',
|
|
68
|
+
brief: {
|
|
69
|
+
surface: 'list',
|
|
70
|
+
requiredBlocks: ['filters', 'table', 'status'],
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
expect(result.ok).toBe(false);
|
|
74
|
+
expect(result.contract.some((item) => item.type === 'brief-missing-block')).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
it('validate_page flags placeholder copy as craft', () => {
|
|
77
|
+
const result = read(handlers.validatePage({
|
|
78
|
+
code: '<MEmpty title="暂无数据" description="示例" />',
|
|
79
|
+
}));
|
|
80
|
+
expect(result.ok).toBe(true);
|
|
81
|
+
expect(result.craft.some((item) => item.type === 'placeholder-copy')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
it('get_style_shells returns pasteable css', () => {
|
|
84
|
+
const result = read(handlers.getStyleShells({ shell: 'express-hero' }));
|
|
85
|
+
expect(result.id).toBe('express-hero');
|
|
86
|
+
expect(result.css).toContain('landing-hero');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import { findPageSnippet, scorePageSnippet } from '../page-snippets.js';
|
|
3
3
|
describe('page snippets catalog', () => {
|
|
4
4
|
it('finds composition-first snippets by id', () => {
|
|
5
5
|
for (const id of [
|
|
@@ -38,9 +38,17 @@ describe('page snippets catalog', () => {
|
|
|
38
38
|
expect(scorePageSnippet(snippet, 'filters')).toBeGreaterThan(0);
|
|
39
39
|
expect(scorePageSnippet(snippet, 'search')).toBeGreaterThan(0);
|
|
40
40
|
});
|
|
41
|
-
it('
|
|
42
|
-
const
|
|
43
|
-
expect(
|
|
44
|
-
expect(
|
|
41
|
+
it('list-table includes status, numeric align, and domain empty copy', () => {
|
|
42
|
+
const table = findPageSnippet('list-table');
|
|
43
|
+
expect(table.template).toContain('MStatus');
|
|
44
|
+
expect(table.scriptSetup).toContain("align: 'end'");
|
|
45
|
+
expect(table.template).toContain('tabular-nums');
|
|
46
|
+
expect(table.template).toContain('还没有成员');
|
|
47
|
+
expect(table.imports).toContain('MStatus');
|
|
48
|
+
});
|
|
49
|
+
it('form-body sets label-width and domain placeholders', () => {
|
|
50
|
+
const form = findPageSnippet('form-body');
|
|
51
|
+
expect(form.template).toContain('label-width');
|
|
52
|
+
expect(form.template).toContain('MSelect');
|
|
45
53
|
});
|
|
46
54
|
});
|
|
@@ -315,26 +315,26 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
315
315
|
expect(result.ok).toBe(true);
|
|
316
316
|
expect(result.suggestions.some((item) => item.type === 'menu-missing-icons')).toBe(false);
|
|
317
317
|
});
|
|
318
|
-
it('flags MTable data prop as contract
|
|
318
|
+
it('flags MTable data prop as contract failure', () => {
|
|
319
319
|
const result = read(handlers.validatePage({
|
|
320
320
|
code: '<MTable :columns="columns" :data="rows" />',
|
|
321
321
|
}));
|
|
322
|
-
expect(result.ok).toBe(
|
|
323
|
-
expect(result.suggestions.some((item) => item.type === 'table-data-prop')).toBe(true);
|
|
322
|
+
expect(result.ok).toBe(false);
|
|
323
|
+
expect(result.suggestions.some((item) => item.type === 'table-data-prop' && item.severity === 'contract')).toBe(true);
|
|
324
324
|
});
|
|
325
325
|
it('flags MMessage severity as inline-alert misuse', () => {
|
|
326
326
|
const result = read(handlers.validatePage({
|
|
327
327
|
code: '<MMessage severity="error">登录失败</MMessage>',
|
|
328
328
|
}));
|
|
329
|
-
expect(result.ok).toBe(
|
|
330
|
-
expect(result.suggestions.some((item) => item.type === 'mmessage-as-alert')).toBe(true);
|
|
329
|
+
expect(result.ok).toBe(false);
|
|
330
|
+
expect(result.suggestions.some((item) => item.type === 'mmessage-as-alert' && item.severity === 'contract')).toBe(true);
|
|
331
331
|
});
|
|
332
332
|
it('flags one-line toast string as message preference', () => {
|
|
333
333
|
const result = read(handlers.validatePage({
|
|
334
334
|
code: `toast.success('已保存')`,
|
|
335
335
|
}));
|
|
336
|
-
expect(result.ok).toBe(
|
|
337
|
-
expect(result.suggestions.some((item) => item.type === 'toast-one-liner')).toBe(true);
|
|
336
|
+
expect(result.ok).toBe(false);
|
|
337
|
+
expect(result.suggestions.some((item) => item.type === 'toast-one-liner' && item.severity === 'contract')).toBe(true);
|
|
338
338
|
});
|
|
339
339
|
it('flags MTag in cell-status when MStatus is missing', () => {
|
|
340
340
|
const result = read(handlers.validatePage({
|
|
@@ -407,10 +407,12 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
407
407
|
expect(result.counts.decisions).toBeGreaterThan(0);
|
|
408
408
|
expect(result.counts.resources).toBeGreaterThan(100);
|
|
409
409
|
expect(result.counts.resourceTemplates).toBe(3);
|
|
410
|
-
expect(result.tools).toHaveLength(
|
|
410
|
+
expect(result.tools).toHaveLength(21);
|
|
411
411
|
expect(result.tools).toContain('get_golden_page');
|
|
412
412
|
expect(result.tools).toContain('get_page_snippet');
|
|
413
413
|
expect(result.tools).toContain('get_style_direction');
|
|
414
|
+
expect(result.tools).toContain('map_reference');
|
|
415
|
+
expect(result.tools).toContain('get_style_shells');
|
|
414
416
|
expect(result.tools).not.toContain('list_style_presets');
|
|
415
417
|
expect(result.tools).not.toContain('get_style_preset');
|
|
416
418
|
expect(result.tools).toContain('validate_page');
|
package/dist/index.js
CHANGED
|
@@ -86,20 +86,49 @@ register('get_pattern', 'Read the structure, component composition, layout, and
|
|
|
86
86
|
pattern: z.string().min(1),
|
|
87
87
|
mode: z.string().optional(),
|
|
88
88
|
}, async (args) => handlers.getPattern(args));
|
|
89
|
-
register('recommend_page', 'Recommend a page pattern and component composition from product intent. Pass style as free-text
|
|
89
|
+
register('recommend_page', 'Recommend a page pattern and component composition from product intent. Pass style as free-text look; density=compact|default|spacious; brief for reference fidelity mapping; includeScaffold for optional golden structure.', {
|
|
90
90
|
intent: z.string().min(1),
|
|
91
91
|
pageType: z.string().optional(),
|
|
92
92
|
features: z.array(z.string()).max(20).optional(),
|
|
93
93
|
style: z.string().optional().describe('Optional free-text style description from the user (not a preset id).'),
|
|
94
|
+
density: z.string().optional().describe('compact | default | spacious'),
|
|
95
|
+
brief: z
|
|
96
|
+
.object({
|
|
97
|
+
description: z.string().optional(),
|
|
98
|
+
intent: z.string().optional(),
|
|
99
|
+
surface: z.string().optional(),
|
|
100
|
+
density: z.string().optional(),
|
|
101
|
+
requiredBlocks: z.array(z.string()).max(30).optional(),
|
|
102
|
+
requiredComponents: z.array(z.string()).max(30).optional(),
|
|
103
|
+
primaryAction: z.string().optional(),
|
|
104
|
+
style: z.string().optional(),
|
|
105
|
+
})
|
|
106
|
+
.optional(),
|
|
94
107
|
mode: z.string().optional(),
|
|
95
108
|
includeScaffold: z.boolean().optional(),
|
|
96
109
|
}, async (args) => handlers.recommendPage(args));
|
|
97
|
-
register('
|
|
110
|
+
register('map_reference', 'Map a reference brief (screenshot/mock description, required blocks) to snippets, token roles, and a signature shell. Pass the same brief to validate_page.', {
|
|
111
|
+
description: z.string().optional(),
|
|
112
|
+
intent: z.string().optional(),
|
|
113
|
+
surface: z.string().optional(),
|
|
114
|
+
density: z.string().optional(),
|
|
115
|
+
requiredBlocks: z.array(z.string()).max(30).optional(),
|
|
116
|
+
requiredComponents: z.array(z.string()).max(30).optional(),
|
|
117
|
+
primaryAction: z.string().optional(),
|
|
118
|
+
style: z.string().optional(),
|
|
119
|
+
mode: z.string().optional(),
|
|
120
|
+
}, async (args) => handlers.mapReference(args));
|
|
121
|
+
register('get_design_rules', 'Return design-token, style-direction rules, signature shell catalog, semantic-action, accessibility, and composition rules for generated pages.', { mode: z.string().optional() }, async (args) => handlers.getDesignRules(args));
|
|
98
122
|
register('get_style_direction', 'Resolve visual style direction: follow user reference/description, infer from clear cues, or ask when uncertain. No named preset catalog.', {
|
|
99
123
|
intent: z.string().optional(),
|
|
100
124
|
style: z.string().optional().describe('Free-text style description from the user.'),
|
|
101
125
|
mode: z.string().optional(),
|
|
102
126
|
}, async (args) => handlers.getStyleDirection(args));
|
|
127
|
+
register('get_style_shells', 'List or read token-only signature shells (ops-quiet, account-split, express-hero, …) with pasteable CSS.', {
|
|
128
|
+
shell: z.string().optional().describe('Shell id for full CSS.'),
|
|
129
|
+
surface: z.string().optional().describe('Filter: ops | account | flow | express'),
|
|
130
|
+
mode: z.string().optional(),
|
|
131
|
+
}, async (args) => handlers.getStyleShells(args));
|
|
103
132
|
register('recommend_component', 'List, read, or recommend component selection guides. Omit query/decision to list; pass decision only to read; pass query to recommend.', {
|
|
104
133
|
query: z.string().optional(),
|
|
105
134
|
decision: z.string().optional(),
|
|
@@ -112,9 +141,23 @@ register('get_golden_page', 'Read a golden page Vue source sample (list-page, li
|
|
|
112
141
|
page: z.string().min(1),
|
|
113
142
|
mode: z.string().optional(),
|
|
114
143
|
}, async (args) => handlers.getGoldenPage(args));
|
|
115
|
-
register('validate_page', 'Validate
|
|
144
|
+
register('validate_page', 'Validate generated page code: contract issues (ok=false) vs craft suggestions. Pass brief/requiredBlocks for fidelity checks against a reference map.', {
|
|
116
145
|
code: z.string().min(1),
|
|
117
146
|
mode: z.string().optional(),
|
|
147
|
+
requiredBlocks: z.array(z.string()).max(30).optional(),
|
|
148
|
+
requiredComponents: z.array(z.string()).max(30).optional(),
|
|
149
|
+
brief: z
|
|
150
|
+
.object({
|
|
151
|
+
description: z.string().optional(),
|
|
152
|
+
intent: z.string().optional(),
|
|
153
|
+
surface: z.string().optional(),
|
|
154
|
+
density: z.string().optional(),
|
|
155
|
+
requiredBlocks: z.array(z.string()).max(30).optional(),
|
|
156
|
+
requiredComponents: z.array(z.string()).max(30).optional(),
|
|
157
|
+
primaryAction: z.string().optional(),
|
|
158
|
+
style: z.string().optional(),
|
|
159
|
+
})
|
|
160
|
+
.optional(),
|
|
118
161
|
}, async (args) => handlers.validatePage(args));
|
|
119
162
|
register('list_page_snippets', 'List reusable page section snippets for local edits (filters, toolbar, form actions, KPI grid, etc.).', {
|
|
120
163
|
query: z.string().optional(),
|