@morya-ui/mcp 0.3.3 → 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 -151
- package/bin/morya-ui-mcp.js +2 -2
- package/data/catalog.json +39 -28
- package/data/example-coverage.json +6 -4
- 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 +76 -1
- package/dist/decisions.js +42 -1
- 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 +313 -204
- package/package.json +13 -11
- 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' }));
|
|
@@ -317,9 +390,11 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
317
390
|
expect(result.counts.decisions).toBeGreaterThan(0);
|
|
318
391
|
expect(result.counts.resources).toBeGreaterThan(100);
|
|
319
392
|
expect(result.counts.resourceTemplates).toBe(3);
|
|
320
|
-
expect(result.tools).toHaveLength(
|
|
393
|
+
expect(result.tools).toHaveLength(20);
|
|
321
394
|
expect(result.tools).toContain('get_golden_page');
|
|
322
395
|
expect(result.tools).toContain('get_page_snippet');
|
|
396
|
+
expect(result.tools).toContain('list_style_presets');
|
|
397
|
+
expect(result.tools).toContain('get_style_preset');
|
|
323
398
|
expect(result.tools).toContain('validate_page');
|
|
324
399
|
expect(result.tools).not.toContain('create_page');
|
|
325
400
|
});
|
package/dist/decisions.js
CHANGED
|
@@ -226,6 +226,7 @@ export const componentDecisions = [
|
|
|
226
226
|
recipe: recipe([
|
|
227
227
|
['columns + rows(没有 data prop)', 'columns + rows (no data prop)'],
|
|
228
228
|
['row-key 默认 id;不稳定时显式指定', 'row-key defaults to id; set explicitly when needed'],
|
|
229
|
+
['全视口主列表可 MPageContent fill + MTable fill;嵌入/短页跳过', 'Full-viewport main lists may use fill; skip for embedded/short pages'],
|
|
229
230
|
['分页:paginator + rows-per-page 或 v-model:page', 'Paging: paginator + rows-per-page or v-model:page'],
|
|
230
231
|
['行选择:selectionMode + v-model:selection', 'Row select: selectionMode + v-model:selection'],
|
|
231
232
|
], [
|
|
@@ -235,6 +236,7 @@ export const componentDecisions = [
|
|
|
235
236
|
...anti([
|
|
236
237
|
[':data → :rows', ':data → :rows'],
|
|
237
238
|
['手写 <table> → MTable', 'Hand-rolled <table> → MTable'],
|
|
239
|
+
['嵌入表硬套 fill → 去掉 fill', 'Forced fill on embedded table → remove fill'],
|
|
238
240
|
]),
|
|
239
241
|
},
|
|
240
242
|
{
|
|
@@ -725,10 +727,45 @@ export const componentDecisions = [
|
|
|
725
727
|
recipe: recipe([
|
|
726
728
|
['MLayout fill-viewport(或 :fill-viewport="true")', 'MLayout fill-viewport (or :fill-viewport="true")'],
|
|
727
729
|
['内容放 MLayoutContent', 'Content in MLayoutContent'],
|
|
730
|
+
['是否再 fill 表格:见下方「MPageContent fill + MTable fill」判断', 'Whether to fill the table: see “MPageContent fill + MTable fill” below'],
|
|
728
731
|
['侧栏用 MLayoutSider + MMenu,不是随便一个 Drawer', 'Sider: MLayoutSider + MMenu, not a random Drawer'],
|
|
729
732
|
]),
|
|
730
733
|
...anti([['在 Layout 外再包一层 100vh 滚动 → 去掉', 'Extra 100vh scroll wrapper outside Layout → remove']]),
|
|
731
734
|
},
|
|
735
|
+
{
|
|
736
|
+
component: 'MPageContent fill + MTable fill',
|
|
737
|
+
when: [
|
|
738
|
+
'全视口后台列表,页面主任务就是浏览一张表',
|
|
739
|
+
'内容高度表格会留下大块空白、分页悬在中间不好看',
|
|
740
|
+
'希望只有表体滚动、分页贴在页面最下方',
|
|
741
|
+
],
|
|
742
|
+
whenEn: [
|
|
743
|
+
'Full-viewport admin list whose main job is browsing one table',
|
|
744
|
+
'A content-sized table would leave a large empty band with mid-page pagination',
|
|
745
|
+
'Only the table body should scroll; pagination should stay at the page bottom',
|
|
746
|
+
],
|
|
747
|
+
avoidWhen: [
|
|
748
|
+
'仪表盘/详情里的嵌入小表',
|
|
749
|
+
'内容本身很短、内容高度即可',
|
|
750
|
+
'整页应作为文档滚动(长筛选+说明+表格)',
|
|
751
|
+
'Dialog / Drawer 内表格',
|
|
752
|
+
],
|
|
753
|
+
avoidWhenEn: [
|
|
754
|
+
'Embedded tables on dashboards or detail pages',
|
|
755
|
+
'Short content where a content-sized table is fine',
|
|
756
|
+
'Whole page should scroll as a document',
|
|
757
|
+
'Tables inside Dialog / Drawer',
|
|
758
|
+
],
|
|
759
|
+
recipe: recipe([
|
|
760
|
+
['先判断是否适合 fill,再写 MPageContent fill + MTable fill', 'Decide fill first, then MPageContent fill + MTable fill'],
|
|
761
|
+
['paginator 或同级 MPagination', 'paginator or sibling MPagination'],
|
|
762
|
+
['适合时不要手写 min-height / calc', 'When fill fits, do not hand-write min-height / calc'],
|
|
763
|
+
]),
|
|
764
|
+
...anti([
|
|
765
|
+
['嵌入/短页硬套 fill → 去掉', 'Forced fill on embedded/short page → remove'],
|
|
766
|
+
['适合 fill 却手写 calc → 改用 fill', 'Hand-written calc when fill fits → use fill'],
|
|
767
|
+
]),
|
|
768
|
+
},
|
|
732
769
|
{
|
|
733
770
|
component: 'MScrollbar',
|
|
734
771
|
when: [
|
|
@@ -796,9 +833,13 @@ export const componentDecisions = [
|
|
|
796
833
|
recipe: recipe([
|
|
797
834
|
['MPageContent > MPageFilters + MTable', 'MPageContent > MPageFilters + MTable'],
|
|
798
835
|
['表格直接放 PageContent,不套 Card', 'Table directly under PageContent, no Card'],
|
|
836
|
+
['高度:全视口主列表再考虑 fill;嵌入/短页跳过', 'Height: consider fill only for full-viewport main lists'],
|
|
799
837
|
['空态用 Table #empty + MEmpty', 'Empty: Table #empty + MEmpty'],
|
|
800
838
|
]),
|
|
801
|
-
...anti([
|
|
839
|
+
...anti([
|
|
840
|
+
['Table 外包 MCard → 去掉 Card', 'MCard around Table → remove Card'],
|
|
841
|
+
['嵌入表硬套 fill → 去掉 fill', 'Forced fill on embedded table → remove fill'],
|
|
842
|
+
]),
|
|
802
843
|
},
|
|
803
844
|
{
|
|
804
845
|
component: 'MCard',
|
package/dist/golden-pages.d.ts
CHANGED
|
@@ -4,10 +4,23 @@ export interface GoldenPageRecord {
|
|
|
4
4
|
title: string;
|
|
5
5
|
titleEn: string;
|
|
6
6
|
patternId: string;
|
|
7
|
+
/** Visual style preset this sample demonstrates (structure may be shared). */
|
|
8
|
+
style?: string;
|
|
9
|
+
/** Canonical structure id when this is a craft variant. */
|
|
10
|
+
variantOf?: string;
|
|
7
11
|
}
|
|
8
12
|
export declare const goldenPageCatalog: GoldenPageRecord[];
|
|
9
13
|
export declare function listGoldenPages(): GoldenPageRecord[];
|
|
10
14
|
export declare function findGoldenPage(id: string): GoldenPageRecord | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* Pick the best golden for a pattern + optional style preset.
|
|
17
|
+
* Falls back to the pattern's canonical sample when no style variant exists.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveGoldenPageId(args: {
|
|
20
|
+
patternId?: string | null;
|
|
21
|
+
canonicalGoldenId?: string | null;
|
|
22
|
+
styleId?: string | null;
|
|
23
|
+
}): string | null;
|
|
11
24
|
export declare function readGoldenPageSource(id: string): {
|
|
12
25
|
record: GoldenPageRecord;
|
|
13
26
|
source: string;
|
package/dist/golden-pages.js
CHANGED
|
@@ -5,9 +5,28 @@ export const goldenPageCatalog = [
|
|
|
5
5
|
{
|
|
6
6
|
id: 'list-page',
|
|
7
7
|
file: 'list-page.vue',
|
|
8
|
-
title: '
|
|
9
|
-
titleEn: 'List page golden sample',
|
|
8
|
+
title: '列表页黄金样例(soft)',
|
|
9
|
+
titleEn: 'List page golden sample (soft)',
|
|
10
10
|
patternId: 'admin-list',
|
|
11
|
+
style: 'soft',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'list-page-dense',
|
|
15
|
+
file: 'list-page-dense.vue',
|
|
16
|
+
title: '列表页黄金样例(dense)',
|
|
17
|
+
titleEn: 'List page golden sample (dense)',
|
|
18
|
+
patternId: 'admin-list',
|
|
19
|
+
style: 'dense',
|
|
20
|
+
variantOf: 'list-page',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'list-page-rail',
|
|
24
|
+
file: 'list-page-rail.vue',
|
|
25
|
+
title: '列表页黄金样例(rail)',
|
|
26
|
+
titleEn: 'List page golden sample (rail)',
|
|
27
|
+
patternId: 'admin-list',
|
|
28
|
+
style: 'rail',
|
|
29
|
+
variantOf: 'list-page',
|
|
11
30
|
},
|
|
12
31
|
{
|
|
13
32
|
id: 'form-page',
|
|
@@ -80,6 +99,15 @@ export const goldenPageCatalog = [
|
|
|
80
99
|
patternId: 'wizard-form',
|
|
81
100
|
},
|
|
82
101
|
];
|
|
102
|
+
/** Map style preset → list golden craft variant (same structure). */
|
|
103
|
+
const listStyleGolden = {
|
|
104
|
+
soft: 'list-page',
|
|
105
|
+
quiet: 'list-page',
|
|
106
|
+
studio: 'list-page',
|
|
107
|
+
ink: 'list-page',
|
|
108
|
+
dense: 'list-page-dense',
|
|
109
|
+
rail: 'list-page-rail',
|
|
110
|
+
};
|
|
83
111
|
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
84
112
|
const bundledDir = join(pkgRoot, 'data/golden-pages');
|
|
85
113
|
const repoDir = join(pkgRoot, '../../design-kit/docs/golden-pages');
|
|
@@ -97,6 +125,31 @@ export function findGoldenPage(id) {
|
|
|
97
125
|
const key = id.trim().toLowerCase().replace(/\.vue$/i, '').replace(/[-_\s]/g, '');
|
|
98
126
|
return goldenPageCatalog.find((item) => item.id.replace(/[-_\s]/g, '') === key);
|
|
99
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Pick the best golden for a pattern + optional style preset.
|
|
130
|
+
* Falls back to the pattern's canonical sample when no style variant exists.
|
|
131
|
+
*/
|
|
132
|
+
export function resolveGoldenPageId(args) {
|
|
133
|
+
const style = args.styleId?.trim().toLowerCase() || '';
|
|
134
|
+
const canonical = args.canonicalGoldenId?.split('/').pop()?.replace(/\.vue$/i, '') ||
|
|
135
|
+
(args.patternId
|
|
136
|
+
? goldenPageCatalog.find((item) => item.patternId === args.patternId && !item.variantOf)?.id
|
|
137
|
+
: null) ||
|
|
138
|
+
null;
|
|
139
|
+
if (args.patternId === 'admin-list' || canonical === 'list-page' || canonical?.startsWith('list-page')) {
|
|
140
|
+
const styled = style ? listStyleGolden[style] : null;
|
|
141
|
+
if (styled && findGoldenPage(styled))
|
|
142
|
+
return styled;
|
|
143
|
+
return 'list-page';
|
|
144
|
+
}
|
|
145
|
+
if (canonical && findGoldenPage(canonical))
|
|
146
|
+
return canonical;
|
|
147
|
+
if (args.patternId) {
|
|
148
|
+
const match = goldenPageCatalog.find((item) => item.patternId === args.patternId && !item.variantOf);
|
|
149
|
+
return match?.id ?? null;
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
100
153
|
export function readGoldenPageSource(id) {
|
|
101
154
|
const record = findGoldenPage(id);
|
|
102
155
|
if (!record)
|