@morya-ui/mcp 0.2.4 → 0.2.5
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 +1 -1
- package/data/catalog.json +1831 -1017
- package/data/example-coverage.json +98 -5
- package/data/golden-pages/dashboard-page.vue +1 -1
- package/data/golden-pages/empty-state.vue +66 -0
- package/data/golden-pages/form-page.vue +7 -3
- package/data/golden-pages/landing-page.vue +328 -0
- package/data/golden-pages/list-page.vue +16 -6
- package/data/golden-pages/login-page.vue +191 -0
- package/dist/__tests__/resources.test.js +1 -1
- package/dist/__tests__/tools.test.js +7 -0
- package/dist/golden-pages.js +22 -1
- package/dist/index.js +1 -1
- package/dist/page-snippets.js +66 -2
- package/dist/patterns.d.ts +17 -5
- package/dist/patterns.js +191 -29
- package/dist/resources.d.ts +1 -1
- package/dist/tools.js +59 -17
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* 黄金样例:登录页(Account + 轻量品牌)
|
|
4
|
+
* @see DESIGN.md §3 · morya-ui-pages surfaces § Account
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
MButton,
|
|
8
|
+
MConfigProvider,
|
|
9
|
+
MForm,
|
|
10
|
+
MFormItem,
|
|
11
|
+
MInput,
|
|
12
|
+
MInputPassword,
|
|
13
|
+
MSpace,
|
|
14
|
+
zhCN,
|
|
15
|
+
} from 'morya-ui'
|
|
16
|
+
import { reactive, ref } from 'vue'
|
|
17
|
+
|
|
18
|
+
const submitting = ref(false)
|
|
19
|
+
const formError = ref('')
|
|
20
|
+
|
|
21
|
+
const model = reactive({
|
|
22
|
+
email: '',
|
|
23
|
+
password: '',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
async function onSubmit() {
|
|
27
|
+
formError.value = ''
|
|
28
|
+
submitting.value = true
|
|
29
|
+
try {
|
|
30
|
+
// await api.login(model)
|
|
31
|
+
if (!model.email || !model.password) {
|
|
32
|
+
formError.value = '请输入邮箱和密码后再试。'
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
} finally {
|
|
36
|
+
submitting.value = false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<template>
|
|
42
|
+
<MConfigProvider :locale="zhCN">
|
|
43
|
+
<div class="login-shell">
|
|
44
|
+
<aside class="login-brand" aria-label="品牌">
|
|
45
|
+
<p class="login-brand__mark">青禾书房</p>
|
|
46
|
+
<h1 class="login-brand__title">把好书留在手边</h1>
|
|
47
|
+
<p class="login-brand__lead">
|
|
48
|
+
店员与会员共用同一套账户。登录后继续进货、上架与会员服务。
|
|
49
|
+
</p>
|
|
50
|
+
</aside>
|
|
51
|
+
|
|
52
|
+
<main class="login-main">
|
|
53
|
+
<div class="login-panel">
|
|
54
|
+
<header class="login-panel__header">
|
|
55
|
+
<h2>登录</h2>
|
|
56
|
+
<p>使用工作邮箱进入后台。</p>
|
|
57
|
+
</header>
|
|
58
|
+
|
|
59
|
+
<!-- 表单级常驻错误:token 样式告警条(字段错误优先用 errorMessage) -->
|
|
60
|
+
<p v-if="formError" class="login-alert" role="alert">
|
|
61
|
+
{{ formError }}
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
<MForm @submit="onSubmit">
|
|
65
|
+
<MFormItem label="邮箱" name="email" required>
|
|
66
|
+
<MInput
|
|
67
|
+
v-model="model.email"
|
|
68
|
+
type="email"
|
|
69
|
+
placeholder="name@qinghe.example"
|
|
70
|
+
autocomplete="username"
|
|
71
|
+
fluid
|
|
72
|
+
/>
|
|
73
|
+
</MFormItem>
|
|
74
|
+
|
|
75
|
+
<MFormItem label="密码" name="password" required>
|
|
76
|
+
<MInputPassword
|
|
77
|
+
v-model="model.password"
|
|
78
|
+
placeholder="请输入密码"
|
|
79
|
+
autocomplete="current-password"
|
|
80
|
+
fluid
|
|
81
|
+
/>
|
|
82
|
+
</MFormItem>
|
|
83
|
+
|
|
84
|
+
<MSpace style="margin-top: var(--m-space-2)" alignment="center">
|
|
85
|
+
<MButton type="submit" label="登录" :loading="submitting" />
|
|
86
|
+
<MButton type="button" label="忘记密码" severity="secondary" text />
|
|
87
|
+
</MSpace>
|
|
88
|
+
</MForm>
|
|
89
|
+
</div>
|
|
90
|
+
</main>
|
|
91
|
+
</div>
|
|
92
|
+
</MConfigProvider>
|
|
93
|
+
</template>
|
|
94
|
+
|
|
95
|
+
<style scoped>
|
|
96
|
+
.login-shell {
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
|
99
|
+
min-height: 100vh;
|
|
100
|
+
background: var(--m-color-surface);
|
|
101
|
+
color: var(--m-color-text);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.login-brand {
|
|
105
|
+
display: flex;
|
|
106
|
+
flex-direction: column;
|
|
107
|
+
justify-content: flex-end;
|
|
108
|
+
gap: var(--m-space-4);
|
|
109
|
+
padding: clamp(2rem, 6vw, 4.5rem);
|
|
110
|
+
background:
|
|
111
|
+
linear-gradient(
|
|
112
|
+
165deg,
|
|
113
|
+
color-mix(in srgb, var(--m-color-primary) 18%, var(--m-color-surface)) 0%,
|
|
114
|
+
var(--m-color-surface) 55%,
|
|
115
|
+
color-mix(in srgb, var(--m-color-border) 35%, var(--m-color-surface)) 100%
|
|
116
|
+
);
|
|
117
|
+
border-right: 1px solid var(--m-color-border);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.login-brand__mark {
|
|
121
|
+
margin: 0;
|
|
122
|
+
font-size: 0.8125rem;
|
|
123
|
+
letter-spacing: 0.14em;
|
|
124
|
+
text-transform: uppercase;
|
|
125
|
+
color: var(--m-color-text-muted);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.login-brand__title {
|
|
129
|
+
margin: 0;
|
|
130
|
+
max-width: 12em;
|
|
131
|
+
font-size: clamp(2rem, 4vw, 3rem);
|
|
132
|
+
font-weight: 650;
|
|
133
|
+
line-height: 1.15;
|
|
134
|
+
letter-spacing: -0.03em;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.login-brand__lead {
|
|
138
|
+
margin: 0;
|
|
139
|
+
max-width: 28rem;
|
|
140
|
+
color: var(--m-color-text-muted);
|
|
141
|
+
line-height: 1.6;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.login-main {
|
|
145
|
+
display: grid;
|
|
146
|
+
place-items: center;
|
|
147
|
+
padding: var(--m-space-6);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.login-panel {
|
|
151
|
+
width: min(100%, 22rem);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.login-panel__header {
|
|
155
|
+
margin-bottom: var(--m-space-5);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.login-panel__header h2 {
|
|
159
|
+
margin: 0 0 var(--m-space-2);
|
|
160
|
+
font-size: 1.5rem;
|
|
161
|
+
font-weight: 650;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.login-panel__header p {
|
|
165
|
+
margin: 0;
|
|
166
|
+
color: var(--m-color-text-muted);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.login-alert {
|
|
170
|
+
margin: 0 0 var(--m-space-4);
|
|
171
|
+
padding: var(--m-space-3) var(--m-space-4);
|
|
172
|
+
border: 1px solid color-mix(in srgb, var(--m-color-danger) 40%, var(--m-color-border));
|
|
173
|
+
border-radius: var(--m-radius-md);
|
|
174
|
+
background: color-mix(in srgb, var(--m-color-danger) 10%, var(--m-color-surface));
|
|
175
|
+
color: var(--m-color-danger);
|
|
176
|
+
font-size: 0.875rem;
|
|
177
|
+
line-height: 1.45;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@media (max-width: 768px) {
|
|
181
|
+
.login-shell {
|
|
182
|
+
grid-template-columns: 1fr;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.login-brand {
|
|
186
|
+
border-right: 0;
|
|
187
|
+
border-bottom: 1px solid var(--m-color-border);
|
|
188
|
+
min-height: 12rem;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
</style>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { countCatalogResources, countCatalogResourceTemplates } from '../resources.js';
|
|
2
3
|
import { createToolHandlers } from '../tools.js';
|
|
3
|
-
import { countCatalogResourceTemplates, countCatalogResources } from '../resources.js';
|
|
4
4
|
describe('@morya-ui/mcp resources', () => {
|
|
5
5
|
const handlers = createToolHandlers();
|
|
6
6
|
it('registers static resources and resource templates', () => {
|
|
@@ -160,6 +160,13 @@ describe('@morya-ui/mcp handlers', () => {
|
|
|
160
160
|
expect(result.id).toBe('page-scroll-choice');
|
|
161
161
|
expect(result.options.some((option) => option.component === 'MScrollbar')).toBe(true);
|
|
162
162
|
});
|
|
163
|
+
it('suggests redundant wrappers around MPage blocks', () => {
|
|
164
|
+
const result = read(handlers.validatePage({
|
|
165
|
+
code: '<MPageContent><div style="padding:24px"><MPageFilters /></div></MPageContent>',
|
|
166
|
+
}));
|
|
167
|
+
expect(result.ok).toBe(true);
|
|
168
|
+
expect(result.suggestions.some((item) => item.type === 'redundant-wrapper' && item.standardId === 'page-sections')).toBe(true);
|
|
169
|
+
});
|
|
163
170
|
it('lists component decision guides when query is omitted', () => {
|
|
164
171
|
const result = read(handlers.recommendComponent({ limit: 5 }));
|
|
165
172
|
expect(result.kind).toBe('decisions');
|
package/dist/golden-pages.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
export const goldenPageCatalog = [
|
|
@@ -23,6 +23,27 @@ export const goldenPageCatalog = [
|
|
|
23
23
|
titleEn: 'Dashboard golden sample',
|
|
24
24
|
patternId: 'dashboard',
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
id: 'login-page',
|
|
28
|
+
file: 'login-page.vue',
|
|
29
|
+
title: '登录页黄金样例',
|
|
30
|
+
titleEn: 'Login page golden sample',
|
|
31
|
+
patternId: 'auth-page',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'landing-page',
|
|
35
|
+
file: 'landing-page.vue',
|
|
36
|
+
title: '营销落地页黄金样例',
|
|
37
|
+
titleEn: 'Marketing landing golden sample',
|
|
38
|
+
patternId: 'marketing-landing',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'empty-state',
|
|
42
|
+
file: 'empty-state.vue',
|
|
43
|
+
title: '空状态黄金样例',
|
|
44
|
+
titleEn: 'Empty state golden sample',
|
|
45
|
+
patternId: 'empty-state',
|
|
46
|
+
},
|
|
26
47
|
];
|
|
27
48
|
const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
49
|
const bundledDir = join(pkgRoot, 'data/golden-pages');
|
package/dist/index.js
CHANGED
|
@@ -102,7 +102,7 @@ register('recommend_component', 'List, read, or recommend component selection gu
|
|
|
102
102
|
offset: z.number().int().min(0).optional(),
|
|
103
103
|
}, async (args) => handlers.recommendComponent(args));
|
|
104
104
|
register('list_golden_pages', 'List golden page samples for list, form, and dashboard layouts.', { mode: z.string().optional() }, async (args) => handlers.listGoldenPageCatalog(args));
|
|
105
|
-
register('get_golden_page', 'Read a golden page Vue source sample (list-page, form-page, dashboard-page).', {
|
|
105
|
+
register('get_golden_page', 'Read a golden page Vue source sample (list-page, form-page, dashboard-page, login-page, landing-page, empty-state).', {
|
|
106
106
|
page: z.string().min(1),
|
|
107
107
|
mode: z.string().optional(),
|
|
108
108
|
}, async (args) => handlers.getGoldenPage(args));
|
package/dist/page-snippets.js
CHANGED
|
@@ -127,11 +127,75 @@ const loading = ref(false)`,
|
|
|
127
127
|
:severity="value === 'active' ? 'success' : 'secondary'"
|
|
128
128
|
/>
|
|
129
129
|
</template>`,
|
|
130
|
-
rules: ['
|
|
131
|
-
rulesEn: ['Use MTag for status, not Button colors'],
|
|
130
|
+
rules: ['分类/强调态用 MTag;更轻的圆点+文案用 MStatus', '不要用 Button 颜色表达状态'],
|
|
131
|
+
rulesEn: ['Use MTag for chip-like status; use MStatus for lighter dot+label', 'Do not use Button colors for status'],
|
|
132
132
|
avoid: ['不要用裸文本颜色区分状态'],
|
|
133
133
|
avoidEn: ['Do not rely on raw text color for status'],
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
id: 'list-status-dot',
|
|
137
|
+
title: '表格状态列 Status',
|
|
138
|
+
titleEn: 'Table status dot cell',
|
|
139
|
+
description: '在 #cell-status 中用 MStatus 展示轻量业务状态。',
|
|
140
|
+
descriptionEn: 'Render lightweight business status with MStatus in #cell-status.',
|
|
141
|
+
pageTypes: ['list'],
|
|
142
|
+
keywords: ['状态', 'status', 'dot', 'cell-status', '在线'],
|
|
143
|
+
imports: ['MStatus'],
|
|
144
|
+
template: `<template #cell-status="{ value }">
|
|
145
|
+
<MStatus
|
|
146
|
+
:label="value === 'online' ? '在线' : '离线'"
|
|
147
|
+
:severity="value === 'online' ? 'success' : 'secondary'"
|
|
148
|
+
/>
|
|
149
|
+
</template>`,
|
|
150
|
+
rules: ['行内轻量状态优先 MStatus', '不要用 Button 颜色表达状态'],
|
|
151
|
+
rulesEn: ['Prefer MStatus for lightweight inline status', 'Do not use Button colors for status'],
|
|
152
|
+
avoid: ['不要用裸文本颜色区分状态'],
|
|
153
|
+
avoidEn: ['Do not rely on raw text color for status'],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'empty-block',
|
|
157
|
+
title: '空状态 MEmpty',
|
|
158
|
+
titleEn: 'Empty state with MEmpty',
|
|
159
|
+
description: '列表或内容区无数据时使用 MEmpty,操作放在 #extra。',
|
|
160
|
+
descriptionEn: 'Use MEmpty for no-data regions; put actions in #extra.',
|
|
161
|
+
pageTypes: ['list', 'common'],
|
|
162
|
+
keywords: ['空状态', '无数据', 'empty', 'zero state', 'no data'],
|
|
163
|
+
imports: ['MEmpty', 'MButton'],
|
|
164
|
+
template: `<MEmpty
|
|
165
|
+
title="还没有数据"
|
|
166
|
+
description="创建第一条记录后即可在此查看。"
|
|
167
|
+
icon="database"
|
|
168
|
+
>
|
|
169
|
+
<template #extra>
|
|
170
|
+
<MButton label="新建" />
|
|
171
|
+
<MButton label="导入" severity="secondary" text />
|
|
172
|
+
</template>
|
|
173
|
+
</MEmpty>`,
|
|
174
|
+
rules: ['正常无数据用 MEmpty,不要用错误色', '主 CTA 用 primary,次动作用 text/secondary'],
|
|
175
|
+
rulesEn: ['Use MEmpty for normal emptiness, not error colors', 'Primary CTA + secondary/text for lesser actions'],
|
|
176
|
+
avoid: ['不要用手写 div 拼空态', '不要用 MResult 表达无数据'],
|
|
177
|
+
avoidEn: ['Do not hand-roll empty markup', 'Do not use MResult for no-data'],
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'result-block',
|
|
181
|
+
title: '结果页 MResult',
|
|
182
|
+
titleEn: 'Result page with MResult',
|
|
183
|
+
description: '提交成功、失败或 403/404/500 使用 MResult。',
|
|
184
|
+
descriptionEn: 'Use MResult for submit outcomes and 403/404/500 pages.',
|
|
185
|
+
pageTypes: ['common'],
|
|
186
|
+
keywords: ['结果', '成功', '失败', '404', '403', 'result', 'success', 'error'],
|
|
187
|
+
imports: ['MResult', 'MButton'],
|
|
188
|
+
template: `<MResult status="success" description="订单已创建,可在列表中查看详情。">
|
|
189
|
+
<template #extra>
|
|
190
|
+
<MButton label="查看订单" />
|
|
191
|
+
<MButton label="返回列表" severity="secondary" text />
|
|
192
|
+
</template>
|
|
193
|
+
</MResult>`,
|
|
194
|
+
rules: ['流程终点用 MResult', '提供明确下一步操作'],
|
|
195
|
+
rulesEn: ['Use MResult for terminal outcomes', 'Provide clear next-step actions'],
|
|
196
|
+
avoid: ['不要用 MEmpty 表达 403/404/失败', '不要只靠颜色表达结果'],
|
|
197
|
+
avoidEn: ['Do not use MEmpty for 403/404/failure', 'Do not rely on color alone'],
|
|
198
|
+
},
|
|
135
199
|
{
|
|
136
200
|
id: 'form-header',
|
|
137
201
|
title: '表单页标题区',
|
package/dist/patterns.d.ts
CHANGED
|
@@ -68,7 +68,7 @@ export declare const designRules: {
|
|
|
68
68
|
readonly do: readonly ["MLayoutHeader bordered (default)", "MPageFilters for filter regions", "MPageSection variant=\"form\" for forms", "MTable bordered when table needs grid lines"];
|
|
69
69
|
readonly avoid: readonly ["MCard wrapping MTable that is already bordered", "Nested MCard with bordered=true on both levels", "Hand-written section borders when MPageFilters or MPageSection applies", "Double borders on filter + table wrapper"];
|
|
70
70
|
};
|
|
71
|
-
readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page"];
|
|
71
|
+
readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page", "login-page", "landing-page", "empty-state"];
|
|
72
72
|
};
|
|
73
73
|
readonly actions: {
|
|
74
74
|
readonly primary: {
|
|
@@ -90,13 +90,15 @@ export declare const designRules: {
|
|
|
90
90
|
};
|
|
91
91
|
};
|
|
92
92
|
readonly status: {
|
|
93
|
-
readonly
|
|
93
|
+
readonly preferred: "MStatus";
|
|
94
|
+
readonly chip: "MTag";
|
|
94
95
|
readonly mapping: {
|
|
95
96
|
readonly active: "success";
|
|
96
97
|
readonly pending: "warn";
|
|
97
98
|
readonly disabled: "secondary";
|
|
98
99
|
readonly error: "danger";
|
|
99
100
|
};
|
|
101
|
+
readonly note: "行内轻量状态用 MStatus;芯片/可关闭标签用 MTag";
|
|
100
102
|
};
|
|
101
103
|
readonly feedback: {
|
|
102
104
|
readonly default: "message";
|
|
@@ -110,13 +112,23 @@ export declare const designRules: {
|
|
|
110
112
|
readonly when: readonly ["同时需要标题与补充说明", "后台任务/批量结果含统计", "异步通知感、角落堆叠"];
|
|
111
113
|
readonly avoid: readonly ["仅有单行文案时不要使用 Toast"];
|
|
112
114
|
};
|
|
115
|
+
readonly empty: {
|
|
116
|
+
readonly component: "MEmpty";
|
|
117
|
+
readonly when: readonly ["列表无数据", "筛选无结果", "首次使用"];
|
|
118
|
+
readonly avoid: readonly ["不要用错误色表达正常空态", "不要用 MResult 表达无数据"];
|
|
119
|
+
};
|
|
120
|
+
readonly result: {
|
|
121
|
+
readonly component: "MResult";
|
|
122
|
+
readonly when: readonly ["提交成功/失败页", "403 / 404 / 500", "流程终点回执"];
|
|
123
|
+
readonly avoid: readonly ["不要用手写图标+文案替代 MResult", "不要用 MEmpty 表达阻断错误"];
|
|
124
|
+
};
|
|
113
125
|
readonly inlineMessage: {
|
|
114
|
-
readonly
|
|
115
|
-
readonly when: readonly ["
|
|
126
|
+
readonly api: "field errorMessage | token-styled role=alert";
|
|
127
|
+
readonly when: readonly ["登录/表单区常驻错误:优先字段 errorMessage", "表单级总结:使用 --m-* 样式的 role=alert 条(见 login-page 黄金样例)", "MMessage 组件当前主要为 message 服务宿主,勿臆造 severity 子节点 API"];
|
|
116
128
|
};
|
|
117
129
|
readonly doc: "docs/feedback-message-vs-toast.md";
|
|
118
130
|
};
|
|
119
|
-
readonly global: readonly ["优先使用组件库组件和 --m-* Token", "操作反馈默认 message;仅一行文案时优先于 toast", "图标按钮建议提供 aria-label;表单字段建议有可见 label", "浮层默认 Teleport 到 body;有明确布局约束时再改 appendTo", "优先使用组件 documented variant,少写深层 CSS 覆盖"];
|
|
131
|
+
readonly global: readonly ["优先使用组件库组件和 --m-* Token", "操作反馈默认 message;仅一行文案时优先于 toast", "空态用 MEmpty;结果/阻断页用 MResult;行内状态优先 MStatus", "图标按钮建议提供 aria-label;表单字段建议有可见 label", "浮层默认 Teleport 到 body;有明确布局约束时再改 appendTo", "优先使用组件 documented variant,少写深层 CSS 覆盖"];
|
|
120
132
|
};
|
|
121
133
|
export declare function findPattern(input: string): PagePattern | undefined;
|
|
122
134
|
export declare function scorePattern(pattern: PagePattern, query: string): number;
|