@morya-ui/mcp 0.3.1 → 0.3.3

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.
@@ -1,6 +1,6 @@
1
1
  {
2
- "generatedAt": "2026-09-23T09:00:43.642Z",
3
- "catalogGeneratedAt": "2026-09-23T09:00:38.573Z",
2
+ "generatedAt": "2026-09-24T08:45:50.660Z",
3
+ "catalogGeneratedAt": "2026-09-24T08:45:45.036Z",
4
4
  "components": [
5
5
  {
6
6
  "id": "Accordion",
@@ -198,12 +198,14 @@
198
198
  "exportName": "MButton",
199
199
  "examples": 20,
200
200
  "props": {
201
- "total": 24,
202
- "covered": 21,
201
+ "total": 27,
202
+ "covered": 22,
203
203
  "missing": [
204
204
  "variant",
205
- "autofocus",
206
- "nativeType"
205
+ "ripple",
206
+ "press",
207
+ "nativeType",
208
+ "autofocus"
207
209
  ]
208
210
  },
209
211
  "events": {
@@ -231,7 +233,7 @@
231
233
  {
232
234
  "id": "Card",
233
235
  "exportName": "MCard",
234
- "examples": 8,
236
+ "examples": 10,
235
237
  "props": {
236
238
  "total": 8,
237
239
  "covered": 4,
@@ -1920,13 +1922,15 @@
1920
1922
  {
1921
1923
  "id": "Page",
1922
1924
  "exportName": "MPageContent",
1923
- "examples": 6,
1925
+ "examples": 10,
1924
1926
  "props": {
1925
- "total": 13,
1926
- "covered": 10,
1927
+ "total": 24,
1928
+ "covered": 19,
1927
1929
  "missing": [
1930
+ "bands",
1931
+ "expandLabel",
1932
+ "collapseLabel",
1928
1933
  "headingLevel",
1929
- "trendSeverity",
1930
1934
  "minHeight"
1931
1935
  ]
1932
1936
  },
@@ -1936,9 +1940,15 @@
1936
1940
  "missing": []
1937
1941
  },
1938
1942
  "slots": {
1939
- "total": 2,
1940
- "covered": 2,
1941
- "missing": []
1943
+ "total": 8,
1944
+ "covered": 3,
1945
+ "missing": [
1946
+ "active",
1947
+ "breadcrumb",
1948
+ "tags",
1949
+ "icon",
1950
+ "extra"
1951
+ ]
1942
1952
  },
1943
1953
  "methods": {
1944
1954
  "total": 0,
@@ -3377,6 +3387,6 @@
3377
3387
  "summary": {
3378
3388
  "components": 93,
3379
3389
  "componentsWithMissingCoverage": 93,
3380
- "totalExamples": 811
3390
+ "totalExamples": 817
3381
3391
  }
3382
3392
  }
@@ -13,8 +13,8 @@ import {
13
13
  MLayoutContent,
14
14
  MLayoutHeader,
15
15
  MPageContent,
16
+ MEmpty,
16
17
  MPageHeader,
17
- MPagePlaceholder,
18
18
  MPageStat,
19
19
  MStatus,
20
20
  MTable,
@@ -23,9 +23,9 @@ import {
23
23
  } from 'morya-ui'
24
24
 
25
25
  const stats = [
26
- { label: '总用户', value: '12,480', trend: '+8.2%', icon: 'users' },
27
- { label: '今日活跃', value: '1,926', trend: '+3.1%', icon: 'activity' },
28
- { label: '待处理工单', value: '47', trend: '-12%', trendSeverity: 'warn' as const, icon: 'clipboard' },
26
+ { label: '总用户', value: '12,480', trend: '+8.2%', trendDirection: 'up' as const, trendLabel: '较上月', icon: 'users' },
27
+ { label: '今日活跃', value: '1,926', trend: '+3.1%', trendDirection: 'up' as const, trendLabel: '较昨日', icon: 'activity' },
28
+ { label: '待处理工单', value: '47', trend: '-12%', trendDirection: 'down' as const, trendLabel: '较上周', trendSeverity: 'warn' as const, icon: 'clipboard' },
29
29
  { label: '系统健康', value: '99.9%', trend: '稳定', trendSeverity: 'secondary' as const, icon: 'heart' },
30
30
  ]
31
31
 
@@ -88,6 +88,8 @@ function statusSeverity(s: string) {
88
88
  :value="item.value"
89
89
  :trend="item.trend"
90
90
  :trend-severity="item.trendSeverity ?? 'primary'"
91
+ :trend-direction="item.trendDirection"
92
+ :trend-label="item.trendLabel"
91
93
  :icon="item.icon"
92
94
  />
93
95
  </MGridItem>
@@ -95,15 +97,15 @@ function statusSeverity(s: string) {
95
97
 
96
98
  <MGrid :cols="2" :x-gap="16" :y-gap="16" responsive="screen">
97
99
  <MGridItem :span="1">
98
- <MCard title="趋势概览">
99
- <MPagePlaceholder
100
- aria-label="图表占位"
100
+ <MCard title="趋势概览" shadow="always">
101
+ <MEmpty
101
102
  description="接入图表组件后展示近 7 日活跃与转化。"
103
+ icon="chart-bar"
102
104
  />
103
105
  </MCard>
104
106
  </MGridItem>
105
107
  <MGridItem :span="1">
106
- <MCard title="最近工单">
108
+ <MCard title="最近工单" shadow="always">
107
109
  <MTable
108
110
  :columns="recentColumns"
109
111
  :rows="recentRows"
@@ -13,7 +13,7 @@ import {
13
13
  MLayoutContent,
14
14
  MLayoutHeader,
15
15
  MPageContent,
16
- MPageToolbar,
16
+ MPageHeader,
17
17
  zhCN,
18
18
  } from 'morya-ui'
19
19
  </script>
@@ -27,61 +27,30 @@ import {
27
27
 
28
28
  <MLayoutContent>
29
29
  <MPageContent>
30
- <MPageToolbar title="课程">
30
+ <MPageHeader title="课程" description="管理目录与可见性。">
31
31
  <template #actions>
32
- <MButton label="新建课程" />
32
+ <MButton severity="primary">
33
+ 新建课程
34
+ </MButton>
33
35
  </template>
34
- </MPageToolbar>
36
+ </MPageHeader>
35
37
 
36
- <div class="empty-state-shell">
37
- <MEmpty
38
- title="还没有课程"
39
- description="创建第一门课程后,学员就能在目录里看到它。也可以稍后从模板导入。"
40
- icon="book"
41
- >
42
- <template #extra>
43
- <MButton label="创建第一门课程" />
44
- <MButton label="从模板导入" severity="secondary" text />
45
- </template>
46
- </MEmpty>
47
- </div>
38
+ <MEmpty
39
+ title="还没有课程"
40
+ description="创建第一门课程后,学员就能在目录里看到它。也可以稍后从模板导入。"
41
+ icon="book"
42
+ >
43
+ <template #extra>
44
+ <MButton severity="primary">
45
+ 创建第一门课程
46
+ </MButton>
47
+ <MButton severity="secondary" text>
48
+ 从模板导入
49
+ </MButton>
50
+ </template>
51
+ </MEmpty>
48
52
  </MPageContent>
49
53
  </MLayoutContent>
50
54
  </MLayout>
51
55
  </MConfigProvider>
52
56
  </template>
53
-
54
- <style scoped>
55
- .empty-state-shell {
56
- display: grid;
57
- place-items: center;
58
- min-height: 18rem;
59
- padding: var(--m-space-8) var(--m-space-6);
60
- border: 1px dashed color-mix(in srgb, var(--m-color-border) 80%, var(--m-color-primary));
61
- border-radius: var(--m-radius-md);
62
- background:
63
- radial-gradient(
64
- 120% 80% at 50% 0%,
65
- color-mix(in srgb, var(--m-color-primary) 10%, transparent),
66
- transparent 55%
67
- ),
68
- var(--m-color-surface);
69
- }
70
-
71
- @media (prefers-reduced-motion: no-preference) {
72
- .empty-state-shell {
73
- animation: empty-in 400ms ease both;
74
- }
75
- }
76
-
77
- @keyframes empty-in {
78
- from {
79
- opacity: 0;
80
- transform: translateY(0.35rem);
81
- }
82
- to {
83
- opacity: 1;
84
- transform: translateY(0);
85
- }
86
- }
87
- </style>
@@ -16,7 +16,7 @@ import {
16
16
  MLayoutContent,
17
17
  MLayoutHeader,
18
18
  MPageContent,
19
- MPageToolbar,
19
+ MPageHeader,
20
20
  MSelect,
21
21
  MSpace,
22
22
  MStatus,
@@ -101,13 +101,13 @@ async function onSave() {
101
101
 
102
102
  <MLayoutContent>
103
103
  <MPageContent>
104
- <MPageToolbar title="用户管理">
104
+ <MPageHeader title="用户管理" description="短表单留在列表内弹窗;字段多时再走独立表单页。">
105
105
  <template #actions>
106
106
  <MButton severity="primary" @click="openCreate">
107
107
  新建用户
108
108
  </MButton>
109
109
  </template>
110
- </MPageToolbar>
110
+ </MPageHeader>
111
111
 
112
112
  <MTable
113
113
  :columns="columns"
@@ -15,18 +15,22 @@ import {
15
15
  MLayoutSider,
16
16
  MMenu,
17
17
  MPageContent,
18
+ MPageFilterChips,
18
19
  MPageFilters,
19
- MPageToolbar,
20
+ MPageHeader,
20
21
  MSelect,
21
22
  MSpace,
22
23
  MStatus,
23
24
  MTable,
25
+ MTag,
24
26
  zhCN,
25
27
  } from 'morya-ui'
26
- import { ref } from 'vue'
28
+ import { computed, ref } from 'vue'
27
29
 
28
30
  const keyword = ref('')
29
31
  const status = ref<string | undefined>()
32
+ const department = ref<string | null>(null)
33
+ const filtersExpanded = ref(false)
30
34
  const siderCollapsed = ref(false)
31
35
 
32
36
  const menuModel = [
@@ -41,6 +45,39 @@ const statusOptions = [
41
45
  { label: '停用', value: 'inactive' },
42
46
  ]
43
47
 
48
+ const departmentOptions = [
49
+ { label: '研发', value: '研发' },
50
+ { label: '运营', value: '运营' },
51
+ { label: '市场', value: '市场' },
52
+ ]
53
+
54
+ const activeFilters = computed(() => {
55
+ const items: Array<{ key: string; label: string }> = []
56
+ if (keyword.value.trim()) {
57
+ items.push({ key: 'keyword', label: `关键词:${keyword.value.trim()}` })
58
+ }
59
+ if (status.value) {
60
+ const label = statusOptions.find((o) => o.value === status.value)?.label ?? status.value
61
+ items.push({ key: 'status', label: `状态:${label}` })
62
+ }
63
+ if (department.value) {
64
+ items.push({ key: 'department', label: `部门:${department.value}` })
65
+ }
66
+ return items
67
+ })
68
+
69
+ function clearFilter(key: string) {
70
+ if (key === 'keyword') keyword.value = ''
71
+ if (key === 'status') status.value = undefined
72
+ if (key === 'department') department.value = null
73
+ }
74
+
75
+ function resetFilters() {
76
+ keyword.value = ''
77
+ status.value = undefined
78
+ department.value = null
79
+ }
80
+
44
81
  const columns = [
45
82
  { key: 'name', label: '名称' },
46
83
  { key: 'status', label: '状态', width: 120 },
@@ -76,7 +113,20 @@ const rows = [
76
113
 
77
114
  <MLayoutContent>
78
115
  <MPageContent>
79
- <MPageFilters aria-label="筛选">
116
+ <MPageHeader title="用户管理" description="维护账号、角色与权限。">
117
+ <template #actions>
118
+ <MButton severity="primary">
119
+ 新建用户
120
+ </MButton>
121
+ </template>
122
+ </MPageHeader>
123
+
124
+ <MPageFilters
125
+ v-model:expanded="filtersExpanded"
126
+ aria-label="筛选"
127
+ variant="filled"
128
+ collapsible
129
+ >
80
130
  <MSpace wrap>
81
131
  <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
82
132
  <MSelect
@@ -86,22 +136,37 @@ const rows = [
86
136
  clearable
87
137
  style="width: 10rem"
88
138
  />
89
- <MButton severity="primary">
139
+ <MButton severity="secondary">
90
140
  查询
91
141
  </MButton>
92
- <MButton severity="secondary">
142
+ <MButton severity="secondary" text @click="resetFilters">
93
143
  重置
94
144
  </MButton>
95
145
  </MSpace>
146
+ <template #advanced>
147
+ <MSpace wrap>
148
+ <MSelect
149
+ v-model="department"
150
+ :options="departmentOptions"
151
+ placeholder="部门"
152
+ clearable
153
+ style="width: 10rem"
154
+ />
155
+ </MSpace>
156
+ </template>
96
157
  </MPageFilters>
97
158
 
98
- <MPageToolbar title="用户管理">
99
- <template #actions>
100
- <MButton severity="primary">
101
- 新建用户
102
- </MButton>
103
- </template>
104
- </MPageToolbar>
159
+ <MPageFilterChips v-if="activeFilters.length" label="已选" aria-label="已选筛选">
160
+ <MTag
161
+ v-for="item in activeFilters"
162
+ :key="item.key"
163
+ :value="item.label"
164
+ size="small"
165
+ bordered
166
+ closable
167
+ @close="clearFilter(item.key)"
168
+ />
169
+ </MPageFilterChips>
105
170
 
106
171
  <MTable
107
172
  :columns="columns"
@@ -149,7 +149,7 @@ describe('@morya-ui/mcp handlers', () => {
149
149
  });
150
150
  it('searches page snippets scope', () => {
151
151
  const result = read(handlers.search({ query: 'toolbar', scope: 'snippets', limit: 5 }));
152
- expect(result.items.some((item) => item.type === 'snippet' && item.id === 'list-toolbar')).toBe(true);
152
+ expect(result.items.some((item) => item.type === 'snippet' && item.id === 'list-batch-toolbar')).toBe(true);
153
153
  });
154
154
  it('suggests double-border page composition as advisory standard', () => {
155
155
  const result = read(handlers.validatePage({
@@ -282,6 +282,23 @@ describe('@morya-ui/mcp handlers', () => {
282
282
  expect(result.id).toBe('overlay-choice');
283
283
  expect(result.options.some((option) => option.component === 'Drawer')).toBe(true);
284
284
  });
285
+ it('returns L2 recipes and antiPatterns on decision reads', () => {
286
+ const result = read(handlers.recommendComponent({ decision: 'data-display-choice' }));
287
+ const table = result.options.find((option) => option.component === 'Table');
288
+ expect(table?.recipe?.props?.length).toBeGreaterThanOrEqual(3);
289
+ expect(table?.recipe?.props?.some((line) => /rows/i.test(line))).toBe(true);
290
+ expect(table?.antiPatterns?.length).toBeGreaterThan(0);
291
+ });
292
+ it('covers feedback and confirm decision guides', () => {
293
+ const feedback = read(handlers.recommendComponent({ decision: 'feedback-choice' }));
294
+ expect(feedback.options.map((option) => option.component)).toEqual(expect.arrayContaining(['message', 'toast', 'field errorMessage / role="alert"']));
295
+ expect(feedback.options.every((option) => (option.recipe?.props?.length ?? 0) >= 3)).toBe(true);
296
+ const confirm = read(handlers.recommendComponent({ decision: 'confirm-choice' }));
297
+ expect(confirm.options.map((option) => option.component)).toEqual([
298
+ 'ConfirmDialog',
299
+ 'ConfirmPopup',
300
+ ]);
301
+ });
285
302
  it('covers current selection, status, empty, and menu guides', () => {
286
303
  const selection = read(handlers.recommendComponent({ decision: 'selection-choice' }));
287
304
  expect(selection.options.map((option) => option.component)).toEqual(expect.arrayContaining(['Select', 'CascadeSelect', 'Listbox', 'SelectButton', 'Radio']));
@@ -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;