@morya-ui/mcp 0.3.2 → 0.3.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 +151 -149
- package/bin/morya-ui-mcp.js +2 -2
- package/data/catalog.json +70 -53
- package/data/example-coverage.json +8 -6
- 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 +166 -0
- package/data/golden-pages/list-page-rail.vue +270 -0
- package/data/golden-pages/list-page.vue +220 -216
- 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__/tools.test.js +93 -1
- package/dist/decisions.d.ts +14 -0
- package/dist/decisions.js +818 -41
- package/dist/golden-pages.d.ts +13 -0
- package/dist/golden-pages.js +55 -2
- package/dist/index.js +9 -3
- package/dist/page-snippets.js +254 -225
- package/dist/patterns.d.ts +2 -2
- package/dist/patterns.js +20 -9
- package/dist/style-presets.d.ts +52 -0
- package/dist/style-presets.js +322 -0
- package/dist/tools.d.ts +20 -0
- package/dist/tools.js +336 -216
- package/package.json +16 -12
- package/LICENSE +0 -21
|
@@ -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>
|
|
@@ -173,6 +173,25 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
173
173
|
expect(result.ok).toBe(true);
|
|
174
174
|
expect(result.suggestions.some((item) => item.type === 'native-scroll')).toBe(true);
|
|
175
175
|
});
|
|
176
|
+
it('suggests list fill height only for fillViewport admin lists missing fill', () => {
|
|
177
|
+
const result = read(handlers.validatePage({
|
|
178
|
+
code: '<MLayout fill-viewport><MPageContent><MTable :columns="c" :rows="r" paginator /></MPageContent></MLayout>',
|
|
179
|
+
}));
|
|
180
|
+
expect(result.ok).toBe(true);
|
|
181
|
+
expect(result.suggestions.some((item) => item.type === 'list-fill-height' && item.standardId === 'page-sections')).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
it('does not suggest list fill when PageContent and Table already fill', () => {
|
|
184
|
+
const result = read(handlers.validatePage({
|
|
185
|
+
code: '<MLayout fill-viewport><MPageContent fill><MTable fill paginator :columns="c" :rows="r" /></MPageContent></MLayout>',
|
|
186
|
+
}));
|
|
187
|
+
expect(result.suggestions.some((item) => item.type === 'list-fill-height')).toBe(false);
|
|
188
|
+
});
|
|
189
|
+
it('does not suggest list fill for paginator tables outside fillViewport shells', () => {
|
|
190
|
+
const result = read(handlers.validatePage({
|
|
191
|
+
code: '<MPageContent><MTable :columns="c" :rows="r" paginator /></MPageContent>',
|
|
192
|
+
}));
|
|
193
|
+
expect(result.suggestions.some((item) => item.type === 'list-fill-height')).toBe(false);
|
|
194
|
+
});
|
|
176
195
|
it('returns scrollable-panel snippet for scroll queries', () => {
|
|
177
196
|
const result = read(handlers.getPageSnippet({ section: 'scrollable-panel' }));
|
|
178
197
|
expect(result.id).toBe('scrollable-panel');
|
|
@@ -186,6 +205,60 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
186
205
|
expect(result.nextStep).toContain('get_golden_page');
|
|
187
206
|
expect(result.pageStandards).toBeUndefined();
|
|
188
207
|
expect(result.scrollStandards).toBeUndefined();
|
|
208
|
+
expect(result.styleDirection?.candidates?.length).toBeGreaterThan(0);
|
|
209
|
+
});
|
|
210
|
+
it('resolves an explicit style preset on recommend_page', () => {
|
|
211
|
+
const result = read(handlers.recommendPage({ intent: '用户列表', pageType: 'list', style: 'soft' }));
|
|
212
|
+
expect(result.styleDirection.resolution).toBe('preset');
|
|
213
|
+
expect(result.styleDirection.preset?.id).toBe('soft');
|
|
214
|
+
expect(result.styleDirection.guidance).toContain('soft');
|
|
215
|
+
expect(result.goldenPage).toContain('list-page');
|
|
216
|
+
});
|
|
217
|
+
it('routes dense/rail styles to list craft golden variants', () => {
|
|
218
|
+
const dense = read(handlers.recommendPage({
|
|
219
|
+
intent: '监控告警列表',
|
|
220
|
+
pageType: 'list',
|
|
221
|
+
style: 'dense',
|
|
222
|
+
includeScaffold: true,
|
|
223
|
+
}));
|
|
224
|
+
expect(dense.goldenPage).toContain('list-page-dense');
|
|
225
|
+
expect(dense.scaffold.goldenPage).toBe('list-page-dense');
|
|
226
|
+
expect(dense.scaffold.files.component).toContain('density="compact"');
|
|
227
|
+
expect(dense.nextStep).toContain('list-page-dense');
|
|
228
|
+
const rail = read(handlers.recommendPage({
|
|
229
|
+
intent: '品牌色后台列表',
|
|
230
|
+
pageType: 'list',
|
|
231
|
+
style: 'rail',
|
|
232
|
+
includeScaffold: true,
|
|
233
|
+
}));
|
|
234
|
+
expect(rail.goldenPage).toContain('list-page-rail');
|
|
235
|
+
expect(rail.scaffold.goldenPage).toBe('list-page-rail');
|
|
236
|
+
expect(rail.scaffold.files.component).toContain('gp-rail-sider');
|
|
237
|
+
});
|
|
238
|
+
it('returns dense and rail golden page sources', () => {
|
|
239
|
+
const dense = read(handlers.getGoldenPage({ page: 'list-page-dense' }));
|
|
240
|
+
expect(dense.id).toBe('list-page-dense');
|
|
241
|
+
expect(dense.style).toBe('dense');
|
|
242
|
+
expect(dense.variantOf).toBe('list-page');
|
|
243
|
+
expect(dense.source).toContain('size="small"');
|
|
244
|
+
const rail = read(handlers.getGoldenPage({ page: 'list-page-rail' }));
|
|
245
|
+
expect(rail.id).toBe('list-page-rail');
|
|
246
|
+
expect(rail.style).toBe('rail');
|
|
247
|
+
expect(rail.source).toContain('color-mix');
|
|
248
|
+
});
|
|
249
|
+
it('lists style presets with resolution priority', () => {
|
|
250
|
+
const result = read(handlers.listStylePresets({ mode: 'zh-CN' }));
|
|
251
|
+
expect(result.presets.map((item) => item.id)).toEqual(expect.arrayContaining(['quiet', 'soft', 'dense', 'rail', 'studio', 'ink']));
|
|
252
|
+
expect(result.resolution[0]).toContain('参考');
|
|
253
|
+
});
|
|
254
|
+
it('reads a style preset by alias', () => {
|
|
255
|
+
const result = read(handlers.getStylePreset({ style: '柔和留白', mode: 'zh-CN' }));
|
|
256
|
+
expect(result.id).toBe('soft');
|
|
257
|
+
expect(result.apply.length).toBeGreaterThan(0);
|
|
258
|
+
});
|
|
259
|
+
it('exposes style presets from get_design_rules', () => {
|
|
260
|
+
const result = read(handlers.getDesignRules({ mode: 'zh-CN' }));
|
|
261
|
+
expect(result.styles.presets.some((item) => item.id === 'dense')).toBe(true);
|
|
189
262
|
});
|
|
190
263
|
it('keeps get_golden_page focused on source code', () => {
|
|
191
264
|
const result = read(handlers.getGoldenPage({ page: 'list-page' }));
|
|
@@ -282,6 +355,23 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
282
355
|
expect(result.id).toBe('overlay-choice');
|
|
283
356
|
expect(result.options.some((option) => option.component === 'Drawer')).toBe(true);
|
|
284
357
|
});
|
|
358
|
+
it('returns L2 recipes and antiPatterns on decision reads', () => {
|
|
359
|
+
const result = read(handlers.recommendComponent({ decision: 'data-display-choice' }));
|
|
360
|
+
const table = result.options.find((option) => option.component === 'Table');
|
|
361
|
+
expect(table?.recipe?.props?.length).toBeGreaterThanOrEqual(3);
|
|
362
|
+
expect(table?.recipe?.props?.some((line) => /rows/i.test(line))).toBe(true);
|
|
363
|
+
expect(table?.antiPatterns?.length).toBeGreaterThan(0);
|
|
364
|
+
});
|
|
365
|
+
it('covers feedback and confirm decision guides', () => {
|
|
366
|
+
const feedback = read(handlers.recommendComponent({ decision: 'feedback-choice' }));
|
|
367
|
+
expect(feedback.options.map((option) => option.component)).toEqual(expect.arrayContaining(['message', 'toast', 'field errorMessage / role="alert"']));
|
|
368
|
+
expect(feedback.options.every((option) => (option.recipe?.props?.length ?? 0) >= 3)).toBe(true);
|
|
369
|
+
const confirm = read(handlers.recommendComponent({ decision: 'confirm-choice' }));
|
|
370
|
+
expect(confirm.options.map((option) => option.component)).toEqual([
|
|
371
|
+
'ConfirmDialog',
|
|
372
|
+
'ConfirmPopup',
|
|
373
|
+
]);
|
|
374
|
+
});
|
|
285
375
|
it('covers current selection, status, empty, and menu guides', () => {
|
|
286
376
|
const selection = read(handlers.recommendComponent({ decision: 'selection-choice' }));
|
|
287
377
|
expect(selection.options.map((option) => option.component)).toEqual(expect.arrayContaining(['Select', 'CascadeSelect', 'Listbox', 'SelectButton', 'Radio']));
|
|
@@ -300,9 +390,11 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
300
390
|
expect(result.counts.decisions).toBeGreaterThan(0);
|
|
301
391
|
expect(result.counts.resources).toBeGreaterThan(100);
|
|
302
392
|
expect(result.counts.resourceTemplates).toBe(3);
|
|
303
|
-
expect(result.tools).toHaveLength(
|
|
393
|
+
expect(result.tools).toHaveLength(20);
|
|
304
394
|
expect(result.tools).toContain('get_golden_page');
|
|
305
395
|
expect(result.tools).toContain('get_page_snippet');
|
|
396
|
+
expect(result.tools).toContain('list_style_presets');
|
|
397
|
+
expect(result.tools).toContain('get_style_preset');
|
|
306
398
|
expect(result.tools).toContain('validate_page');
|
|
307
399
|
expect(result.tools).not.toContain('create_page');
|
|
308
400
|
});
|
package/dist/decisions.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
+
export interface ComponentDecisionRecipe {
|
|
2
|
+
/** 3–8 critical props / usage notes (zh). Not a full API manual. */
|
|
3
|
+
props: string[];
|
|
4
|
+
propsEn: string[];
|
|
5
|
+
slots?: string[];
|
|
6
|
+
slotsEn?: string[];
|
|
7
|
+
events?: string[];
|
|
8
|
+
eventsEn?: string[];
|
|
9
|
+
}
|
|
1
10
|
export interface ComponentDecisionOption {
|
|
2
11
|
component: string;
|
|
3
12
|
when: string[];
|
|
4
13
|
whenEn: string[];
|
|
5
14
|
avoidWhen: string[];
|
|
6
15
|
avoidWhenEn: string[];
|
|
16
|
+
recipe: ComponentDecisionRecipe;
|
|
17
|
+
antiPatterns: string[];
|
|
18
|
+
antiPatternsEn: string[];
|
|
7
19
|
}
|
|
8
20
|
export interface ComponentDecision {
|
|
9
21
|
id: string;
|
|
@@ -17,3 +29,5 @@ export interface ComponentDecision {
|
|
|
17
29
|
export declare const componentDecisions: ComponentDecision[];
|
|
18
30
|
export declare function findDecision(name: string): ComponentDecision | undefined;
|
|
19
31
|
export declare function scoreDecision(decision: ComponentDecision, query: string): number;
|
|
32
|
+
/** Ensures every option carries a non-empty L2 recipe (used by tests / generate script). */
|
|
33
|
+
export declare function assertDecisionRecipes(decisions?: ComponentDecision[]): void;
|