@morya-ui/mcp 0.3.4 → 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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +154 -151
  3. package/bin/morya-ui-mcp.js +2 -2
  4. package/data/catalog.json +40 -40
  5. package/data/example-coverage.json +2 -2
  6. package/data/golden-pages/dashboard-page.vue +138 -138
  7. package/data/golden-pages/detail-page.vue +178 -178
  8. package/data/golden-pages/empty-state.vue +56 -56
  9. package/data/golden-pages/form-in-dialog.vue +166 -166
  10. package/data/golden-pages/form-page.vue +107 -107
  11. package/data/golden-pages/landing-page.vue +358 -358
  12. package/data/golden-pages/list-page-dense.vue +168 -166
  13. package/data/golden-pages/list-page-rail.vue +272 -270
  14. package/data/golden-pages/list-page.vue +222 -220
  15. package/data/golden-pages/login-page.vue +231 -231
  16. package/data/golden-pages/result-page.vue +84 -84
  17. package/data/golden-pages/settings-page.vue +163 -163
  18. package/data/golden-pages/wizard-form.vue +182 -182
  19. package/dist/__tests__/fidelity.test.d.ts +1 -0
  20. package/dist/__tests__/fidelity.test.js +88 -0
  21. package/dist/__tests__/page-snippets.test.js +42 -5
  22. package/dist/__tests__/tools.test.js +49 -29
  23. package/dist/decisions.d.ts +2 -0
  24. package/dist/decisions.js +34 -6
  25. package/dist/golden-pages.d.ts +6 -1
  26. package/dist/golden-pages.js +34 -33
  27. package/dist/index.js +52 -9
  28. package/dist/page-snippets.js +1041 -280
  29. package/dist/patterns.d.ts +4 -2
  30. package/dist/patterns.js +35 -6
  31. package/dist/reference-brief.d.ts +55 -0
  32. package/dist/reference-brief.js +468 -0
  33. package/dist/style-presets.d.ts +13 -32
  34. package/dist/style-presets.js +100 -291
  35. package/dist/style-shells.d.ts +20 -0
  36. package/dist/style-shells.js +249 -0
  37. package/dist/tools.d.ts +21 -3
  38. package/dist/tools.js +496 -424
  39. package/package.json +11 -13
@@ -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,6 +1,35 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { filterPageSnippets, findPageSnippet, scorePageSnippet } from '../page-snippets.js';
2
+ import { findPageSnippet, scorePageSnippet } from '../page-snippets.js';
3
3
  describe('page snippets catalog', () => {
4
+ it('finds composition-first snippets by id', () => {
5
+ for (const id of [
6
+ 'form-in-dialog',
7
+ 'form-in-drawer',
8
+ 'confirm-delete',
9
+ 'row-actions-menu',
10
+ 'wizard-steps',
11
+ 'auth-split-shell',
12
+ 'page-header-actions',
13
+ 'list-filters-stack',
14
+ 'list-filters-dense',
15
+ ]) {
16
+ expect(findPageSnippet(id)?.id).toBe(id);
17
+ }
18
+ });
19
+ it('list-filters-stack includes chips, advanced, and #actions', () => {
20
+ const stack = findPageSnippet('list-filters-stack');
21
+ expect(stack.template).toContain('MPageFilterChips');
22
+ expect(stack.template).toContain('#advanced');
23
+ expect(stack.template).toContain('#actions');
24
+ expect(stack.template).toContain('collapsible');
25
+ expect(stack.scriptSetup).toContain('activeFilters');
26
+ expect(stack.scriptSetup).toContain('resetFilters');
27
+ });
28
+ it('scores filter stack keywords', () => {
29
+ const stack = findPageSnippet('list-filters-stack');
30
+ expect(scorePageSnippet(stack, '筛选组合')).toBeGreaterThan(0);
31
+ expect(scorePageSnippet(stack, 'filter stack')).toBeGreaterThan(0);
32
+ });
4
33
  it('finds snippet by exact id', () => {
5
34
  expect(findPageSnippet('form-actions')?.id).toBe('form-actions');
6
35
  });
@@ -9,9 +38,17 @@ describe('page snippets catalog', () => {
9
38
  expect(scorePageSnippet(snippet, 'filters')).toBeGreaterThan(0);
10
39
  expect(scorePageSnippet(snippet, 'search')).toBeGreaterThan(0);
11
40
  });
12
- it('filters snippets by page type', () => {
13
- const result = filterPageSnippets({ pageType: 'dashboard' });
14
- expect(result.items.some((item) => item.id === 'dashboard-kpi-grid')).toBe(true);
15
- expect(result.items.some((item) => item.id === 'list-filters')).toBe(false);
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');
16
53
  });
17
54
  });