@morya-ui/mcp 0.2.4 → 0.2.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.
@@ -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,34 @@ 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
+ });
170
+ it('suggests height-chain when MLayout is used without shell CSS', () => {
171
+ const result = read(handlers.validatePage({
172
+ code: '<MLayout fill-viewport has-sider><MLayoutContent /></MLayout>',
173
+ }));
174
+ expect(result.ok).toBe(true);
175
+ expect(result.suggestions.some((item) => item.type === 'height-chain')).toBe(true);
176
+ });
177
+ it('suggests menu icons when MMenu model has no icon fields', () => {
178
+ const result = read(handlers.validatePage({
179
+ code: `<MMenu :model="[{ key: 'users', label: 'Users', to: '/users' }]" />`,
180
+ }));
181
+ expect(result.ok).toBe(true);
182
+ expect(result.suggestions.some((item) => item.type === 'menu-missing-icons')).toBe(true);
183
+ });
184
+ it('does not suggest menu icons when model items include icon', () => {
185
+ const result = read(handlers.validatePage({
186
+ code: `<MMenu :model="[{ key: 'users', label: 'Users', icon: 'user', to: '/users' }]" />`,
187
+ }));
188
+ expect(result.ok).toBe(true);
189
+ expect(result.suggestions.some((item) => item.type === 'menu-missing-icons')).toBe(false);
190
+ });
163
191
  it('lists component decision guides when query is omitted', () => {
164
192
  const result = read(handlers.recommendComponent({ limit: 5 }));
165
193
  expect(result.kind).toBe('decisions');
package/dist/decisions.js CHANGED
@@ -1,4 +1,46 @@
1
1
  export const componentDecisions = [
2
+ {
3
+ id: 'form-surface-choice',
4
+ title: '新建/编辑用弹窗还是独立表单页',
5
+ titleEn: 'Dialog vs dedicated form page for create/edit',
6
+ question: '表单是从列表触发的短录入,还是多分组/长流程的独立配置?',
7
+ questionEn: 'Is this a short create/edit from a list, or a multi-section dedicated form flow?',
8
+ keywords: ['新建', '编辑', '表单', '弹窗', '路由', 'create', 'edit', 'form', 'dialog', 'page'],
9
+ options: [
10
+ {
11
+ component: 'Dialog',
12
+ when: [
13
+ '从列表/工具栏打开新建或编辑',
14
+ '字段大约 ≤8 个、单段表单',
15
+ '希望保存后仍停留在列表上下文',
16
+ ],
17
+ whenEn: [
18
+ 'Create/edit is launched from a list or toolbar',
19
+ 'About ≤8 fields in a single section',
20
+ 'User should stay in the list context after save',
21
+ ],
22
+ avoidWhen: ['多步骤向导', '多分组长配置页'],
23
+ avoidWhenEn: ['Multi-step wizards', 'Long multi-section configuration pages'],
24
+ },
25
+ {
26
+ component: 'Drawer',
27
+ when: ['表单比 Dialog 更长但仍需对照列表', '详情+编辑侧滑'],
28
+ whenEn: ['Form is longer than a dialog but list context matters', 'Detail plus edit in a side panel'],
29
+ avoidWhen: ['字段极少的确认式录入'],
30
+ avoidWhenEn: ['Tiny confirmation-style forms'],
31
+ },
32
+ {
33
+ component: 'Form',
34
+ when: ['用户明确要求独立表单页', '多分组、长校验、离开需确认的配置流'],
35
+ whenEn: [
36
+ 'The user explicitly asked for a dedicated form page',
37
+ 'Multi-section, heavy validation, or leave-confirm configuration flows',
38
+ ],
39
+ avoidWhen: ['列表上的常规短 CRUD'],
40
+ avoidWhenEn: ['Routine short CRUD from a list'],
41
+ },
42
+ ],
43
+ },
2
44
  {
3
45
  id: 'overlay-choice',
4
46
  title: '如何选择浮层组件',
@@ -9,10 +51,23 @@ export const componentDecisions = [
9
51
  options: [
10
52
  {
11
53
  component: 'Dialog',
12
- when: ['需要用户聚焦完成一项短到中等任务', '需要确认危险操作', '内容不适合直接放在页面流中'],
13
- whenEn: ['The user should focus on a short or medium-sized task', 'A destructive action needs confirmation', 'Content should not interrupt the page flow'],
14
- avoidWhen: ['内容接近完整页面', '用户需要持续查看底层页面上下文'],
15
- avoidWhenEn: ['The content is effectively a full page', 'The user must continuously reference the underlying page'],
54
+ when: [
55
+ '需要用户聚焦完成一项短到中等任务',
56
+ '列表页上的新建/编辑短表单(约 ≤8 个字段)',
57
+ '需要确认危险操作',
58
+ '内容不适合直接放在页面流中',
59
+ ],
60
+ whenEn: [
61
+ 'The user should focus on a short or medium-sized task',
62
+ 'Create/edit short forms launched from a list (~≤8 fields)',
63
+ 'A destructive action needs confirmation',
64
+ 'Content should not interrupt the page flow',
65
+ ],
66
+ avoidWhen: ['内容接近完整多分组页面', '用户需要持续查看底层页面上下文且表单很长'],
67
+ avoidWhenEn: [
68
+ 'The content is effectively a multi-section full page',
69
+ 'The user must continuously reference the underlying page and the form is long',
70
+ ],
16
71
  },
17
72
  {
18
73
  component: 'Drawer',
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
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));
@@ -127,11 +127,75 @@ const loading = ref(false)`,
127
127
  :severity="value === 'active' ? 'success' : 'secondary'"
128
128
  />
129
129
  </template>`,
130
- rules: ['状态用 MTag,不要用 Button 颜色表达状态'],
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: '表单页标题区',
@@ -330,23 +394,51 @@ const recentRows = ref<Record<string, unknown>[]>([])`,
330
394
  descriptionEn: 'Minimal MLayout fillViewport shell with header and content.',
331
395
  pageTypes: ['common'],
332
396
  keywords: ['layout', 'shell', 'breadcrumb', 'sider', 'app'],
333
- imports: ['MLayout', 'MLayoutHeader', 'MLayoutContent', 'MBreadcrumb', 'MConfigProvider', 'zhCN'],
397
+ imports: [
398
+ 'MLayout',
399
+ 'MLayoutHeader',
400
+ 'MLayoutContent',
401
+ 'MLayoutSider',
402
+ 'MMenu',
403
+ 'MBreadcrumb',
404
+ 'MPageContent',
405
+ 'MConfigProvider',
406
+ 'zhCN',
407
+ ],
334
408
  template: `<MConfigProvider :locale="zhCN">
335
- <MLayout fill-viewport>
336
- <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
337
- <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
338
- </MLayoutHeader>
339
- <MLayoutContent>
340
- <MPageContent>
341
- <!-- page sections -->
342
- </MPageContent>
343
- </MLayoutContent>
409
+ <MLayout fill-viewport has-sider>
410
+ <MLayoutSider v-model:collapsed="siderCollapsed" bordered :collapsed-width="72">
411
+ <MMenu
412
+ :model="[
413
+ { key: 'users', label: '用户管理', icon: 'user', to: '/users' },
414
+ { key: 'roles', label: '角色管理', icon: 'shield', to: '/roles' },
415
+ ]"
416
+ :collapsed="siderCollapsed"
417
+ :collapsed-width="72"
418
+ />
419
+ </MLayoutSider>
420
+ <MLayout>
421
+ <MLayoutHeader :padding="'var(--m-space-4) var(--m-space-6)'">
422
+ <MBreadcrumb :model="[{ label: '首页', to: '/' }, { label: '当前页' }]" />
423
+ </MLayoutHeader>
424
+ <MLayoutContent>
425
+ <MPageContent>
426
+ <!-- page sections -->
427
+ </MPageContent>
428
+ </MLayoutContent>
429
+ </MLayout>
344
430
  </MLayout>
345
431
  </MConfigProvider>`,
346
- rules: ['根布局用 fillViewport', '面包屑放 MLayoutHeader'],
347
- rulesEn: ['Use fillViewport on the root layout', 'Place breadcrumb in MLayoutHeader'],
348
- avoid: ['不要手写 min-height: 100vh'],
349
- avoidEn: ['Do not hand-write min-height: 100vh'],
432
+ rules: [
433
+ '根布局用 fillViewport',
434
+ '面包屑放 MLayoutHeader',
435
+ ],
436
+ rulesEn: [
437
+ 'Use fillViewport on the root layout',
438
+ 'Place breadcrumb in MLayoutHeader',
439
+ ],
440
+ avoid: ['不要在 MLayoutContent 上写 padding'],
441
+ avoidEn: ['Do not put padding on MLayoutContent'],
350
442
  },
351
443
  {
352
444
  id: 'scrollable-panel',
@@ -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 component: "MTag";
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 component: "MMessage";
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;