@morya-ui/mcp 0.2.1 → 0.2.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.
@@ -0,0 +1,33 @@
1
+ export type PageSnippetPageType = 'list' | 'form' | 'dashboard' | 'detail' | 'common';
2
+ export interface PageSnippet {
3
+ id: string;
4
+ title: string;
5
+ titleEn: string;
6
+ description: string;
7
+ descriptionEn: string;
8
+ pageTypes: PageSnippetPageType[];
9
+ keywords: string[];
10
+ imports: string[];
11
+ scriptSetup?: string;
12
+ template: string;
13
+ rules: string[];
14
+ rulesEn: string[];
15
+ avoid: string[];
16
+ avoidEn: string[];
17
+ }
18
+ export declare const pageSnippets: PageSnippet[];
19
+ export declare function findPageSnippet(id: string): PageSnippet | undefined;
20
+ export declare function scorePageSnippet(snippet: PageSnippet, query: string): number;
21
+ export declare function filterPageSnippets(args: {
22
+ query?: string;
23
+ pageType?: string;
24
+ limit?: number;
25
+ offset?: number;
26
+ }): {
27
+ total: number;
28
+ items: PageSnippet[];
29
+ offset: number;
30
+ limit: number;
31
+ hasMore: boolean;
32
+ nextOffset: number | undefined;
33
+ };
@@ -0,0 +1,405 @@
1
+ export const pageSnippets = [
2
+ {
3
+ id: 'list-filters',
4
+ title: '列表页筛选区',
5
+ titleEn: 'List page filter bar',
6
+ description: '在 MPageContent 内追加或替换筛选区,使用 MPageFilters + MSpace 包裹控件。',
7
+ descriptionEn: 'Add or replace a filter region inside MPageContent using MPageFilters and MSpace.',
8
+ pageTypes: ['list'],
9
+ keywords: ['筛选', '搜索', '查询', 'filter', 'search', 'keyword', 'reset'],
10
+ imports: ['MPageFilters', 'MSpace', 'MInput', 'MSelect', 'MButton'],
11
+ scriptSetup: `const keyword = ref('')
12
+ const status = ref<string | undefined>()
13
+ const statusOptions = [
14
+ { label: '全部', value: '' },
15
+ { label: '启用', value: 'active' },
16
+ { label: '停用', value: 'inactive' },
17
+ ]`,
18
+ template: `<MPageFilters aria-label="筛选">
19
+ <MSpace wrap>
20
+ <MInput v-model="keyword" placeholder="搜索名称" clearable style="width: 14rem" />
21
+ <MSelect
22
+ v-model="status"
23
+ :options="statusOptions"
24
+ placeholder="状态"
25
+ clearable
26
+ style="width: 10rem"
27
+ />
28
+ <MButton severity="primary">查询</MButton>
29
+ <MButton severity="secondary">重置</MButton>
30
+ </MSpace>
31
+ </MPageFilters>`,
32
+ rules: ['放在 MPageContent 内、MPageToolbar 之前', '不要外包 MCard', '筛选项宽度用 inline style,区块样式交给 MPageFilters'],
33
+ rulesEn: ['Place inside MPageContent before MPageToolbar', 'Do not wrap with MCard', 'Use inline width on controls; let MPageFilters own the surface'],
34
+ avoid: ['不要手写 .page-filters 类', '不要用 MCard 包筛选区'],
35
+ avoidEn: ['Do not hand-write .page-filters classes', 'Do not wrap filters in MCard'],
36
+ },
37
+ {
38
+ id: 'list-toolbar',
39
+ title: '列表页工具栏',
40
+ titleEn: 'List page toolbar',
41
+ description: '标题 + 主操作按钮,放在筛选区之后、表格之前。',
42
+ descriptionEn: 'Page title and primary action between filters and the table.',
43
+ pageTypes: ['list'],
44
+ keywords: ['工具栏', '标题', '新建', 'toolbar', 'title', 'create', 'actions'],
45
+ imports: ['MPageToolbar', 'MButton'],
46
+ template: `<MPageToolbar title="用户管理">
47
+ <template #actions>
48
+ <MButton severity="primary">新建用户</MButton>
49
+ </template>
50
+ </MPageToolbar>`,
51
+ rules: ['主操作放 #actions', '标题优先用 title prop,不用裸 h1 + flex'],
52
+ rulesEn: ['Put primary actions in #actions', 'Prefer the title prop over raw h1 + flex'],
53
+ avoid: ['不要再用自定义 toolbar CSS'],
54
+ avoidEn: ['Do not add custom toolbar CSS'],
55
+ },
56
+ {
57
+ id: 'list-table',
58
+ title: '列表页表格',
59
+ titleEn: 'List page table',
60
+ description: '标准 MTable,直接放在 MPageContent 内,不额外包 Card。',
61
+ descriptionEn: 'Standard MTable placed directly in MPageContent without an extra Card wrapper.',
62
+ pageTypes: ['list'],
63
+ keywords: ['表格', 'table', 'pagination', 'paginator', 'empty', 'columns', 'rows'],
64
+ imports: ['MTable'],
65
+ scriptSetup: `const columns = [
66
+ { key: 'name', label: '名称' },
67
+ { key: 'status', label: '状态' },
68
+ { key: 'updatedAt', label: '更新时间' },
69
+ { key: 'actions', label: '操作', width: 128 },
70
+ ]
71
+ const rows = ref<Record<string, unknown>[]>([])
72
+ const loading = ref(false)`,
73
+ template: `<MTable
74
+ :columns="columns"
75
+ :rows="rows"
76
+ :loading="loading"
77
+ paginator
78
+ :rows-per-page="10"
79
+ striped
80
+ bordered
81
+ row-key="id"
82
+ aria-label="数据列表"
83
+ >
84
+ <template #empty>
85
+ <p style="margin:0;padding:var(--m-space-8);text-align:center;color:var(--m-color-text-muted)">
86
+ 暂无数据
87
+ </p>
88
+ </template>
89
+ </MTable>`,
90
+ rules: ['表格直接放在 MPageContent 内', '空态用 #empty,不要留空白区域'],
91
+ rulesEn: ['Place the table directly in MPageContent', 'Use #empty for zero-data states'],
92
+ avoid: ['不要用 MCard 包裹 bordered MTable'],
93
+ avoidEn: ['Do not wrap a bordered MTable with MCard'],
94
+ },
95
+ {
96
+ id: 'list-row-actions',
97
+ title: '表格行操作列',
98
+ titleEn: 'Table row actions cell',
99
+ description: '在 #cell-actions 插槽中放置编辑/删除等按钮组。',
100
+ descriptionEn: 'Edit/delete buttons inside the #cell-actions slot.',
101
+ pageTypes: ['list'],
102
+ keywords: ['行操作', '编辑', '删除', 'actions', 'row', 'cell'],
103
+ imports: ['MSpace', 'MButton'],
104
+ template: `<template #cell-actions>
105
+ <MSpace>
106
+ <MButton severity="secondary" size="small">编辑</MButton>
107
+ <MButton severity="danger" size="small">删除</MButton>
108
+ </MSpace>
109
+ </template>`,
110
+ rules: ['行内操作用 size="small"', '危险操作使用 severity="danger" 并配合确认弹窗'],
111
+ rulesEn: ['Use size="small" for row actions', 'Use severity="danger" with confirmation for destructive actions'],
112
+ avoid: ['不要用 Dropdown 代替明确的行内按钮组,除非操作很多'],
113
+ avoidEn: ['Do not replace explicit row buttons with Dropdown unless there are many actions'],
114
+ },
115
+ {
116
+ id: 'list-status-tag',
117
+ title: '表格状态列 Tag',
118
+ titleEn: 'Table status tag cell',
119
+ description: '在 #cell-status 中用 MTag 展示业务状态。',
120
+ descriptionEn: 'Render business status with MTag in #cell-status.',
121
+ pageTypes: ['list'],
122
+ keywords: ['状态', 'tag', 'status', 'cell-status'],
123
+ imports: ['MTag'],
124
+ template: `<template #cell-status="{ value }">
125
+ <MTag
126
+ :value="value === 'active' ? '启用' : '停用'"
127
+ :severity="value === 'active' ? 'success' : 'secondary'"
128
+ />
129
+ </template>`,
130
+ rules: ['状态用 MTag,不要用 Button 颜色表达状态'],
131
+ rulesEn: ['Use MTag for status, not Button colors'],
132
+ avoid: ['不要用裸文本颜色区分状态'],
133
+ avoidEn: ['Do not rely on raw text color for status'],
134
+ },
135
+ {
136
+ id: 'form-header',
137
+ title: '表单页标题区',
138
+ titleEn: 'Form page header',
139
+ description: '表单页顶部标题与说明,放在 MPageContent width="narrow" 内。',
140
+ descriptionEn: 'Form page title and description inside narrow MPageContent.',
141
+ pageTypes: ['form'],
142
+ keywords: ['标题', '说明', 'intro', 'header', 'description'],
143
+ imports: ['MPageHeader'],
144
+ template: `<MPageHeader title="新建用户" description="填写基本信息并分配角色。" />`,
145
+ rules: ['放在 MPageSection 之前', '说明文案用 description prop'],
146
+ rulesEn: ['Place before MPageSection', 'Use the description prop for helper copy'],
147
+ avoid: ['不要裸 h1 + p 手写 margin'],
148
+ avoidEn: ['Do not hand-write h1 + p margins'],
149
+ },
150
+ {
151
+ id: 'form-body',
152
+ title: '表单主体',
153
+ titleEn: 'Form body surface',
154
+ description: 'MPageSection variant="form" 包裹 MForm 字段。',
155
+ descriptionEn: 'Wrap MForm fields with MPageSection variant="form".',
156
+ pageTypes: ['form'],
157
+ keywords: ['表单', 'form', 'fields', 'formitem'],
158
+ imports: ['MPageSection', 'MForm', 'MFormItem', 'MInput'],
159
+ scriptSetup: `const model = reactive({ name: '' })
160
+ const submitting = ref(false)`,
161
+ template: `<MPageSection variant="form">
162
+ <MForm @submit.prevent="onSubmit">
163
+ <MFormItem label="姓名" name="name" required>
164
+ <MInput v-model="model.name" placeholder="请输入姓名" fluid />
165
+ </MFormItem>
166
+ <!-- 更多字段 -->
167
+ </MForm>
168
+ </MPageSection>`,
169
+ rules: ['字段用 MFormItem + fluid 控件', '表单表面由 MPageSection 提供,不要重复 MCard'],
170
+ rulesEn: ['Use MFormItem with fluid controls', 'Let MPageSection provide the surface; do not duplicate MCard'],
171
+ avoid: ['不要绕过 MForm 手写校验状态'],
172
+ avoidEn: ['Do not bypass MForm for validation state'],
173
+ },
174
+ {
175
+ id: 'form-actions',
176
+ title: '表单底栏操作',
177
+ titleEn: 'Form footer actions',
178
+ description: '保存/取消按钮区,放在 MForm 内底部。',
179
+ descriptionEn: 'Save/cancel actions at the bottom of MForm.',
180
+ pageTypes: ['form'],
181
+ keywords: ['保存', '取消', 'submit', 'actions', 'footer'],
182
+ imports: ['MPageSection', 'MSpace', 'MButton'],
183
+ template: `<MPageSection variant="actions">
184
+ <MSpace>
185
+ <MButton native-type="submit" severity="primary" :loading="submitting">保存</MButton>
186
+ <MButton severity="secondary">取消</MButton>
187
+ </MSpace>
188
+ </MPageSection>`,
189
+ rules: ['提交按钮在前,取消 secondary', '保存中传 loading 防重复提交'],
190
+ rulesEn: ['Submit first, cancel secondary', 'Pass loading while saving to prevent duplicate submits'],
191
+ avoid: ['不要手写 border-top + margin-top'],
192
+ avoidEn: ['Do not hand-write border-top and margin-top'],
193
+ },
194
+ {
195
+ id: 'dashboard-kpi-grid',
196
+ title: '仪表盘 KPI 栅格',
197
+ titleEn: 'Dashboard KPI grid',
198
+ description: 'MGrid + MPageStat 展示关键指标。',
199
+ descriptionEn: 'Key metrics with MGrid and MPageStat.',
200
+ pageTypes: ['dashboard'],
201
+ keywords: ['kpi', '指标', 'stat', 'grid', 'metrics'],
202
+ imports: ['MGrid', 'MGridItem', 'MPageStat'],
203
+ scriptSetup: `const metrics = ref([
204
+ { label: '总用户', value: '12,480', trend: '+8.2%', icon: 'users' },
205
+ { label: '今日活跃', value: '1,926', trend: '+3.1%', icon: 'activity' },
206
+ ])`,
207
+ template: `<MGrid :cols="4" :x-gap="16" :y-gap="16" responsive="screen">
208
+ <MGridItem v-for="metric in metrics" :key="metric.label" :span="1">
209
+ <MPageStat
210
+ :label="metric.label"
211
+ :value="metric.value"
212
+ :trend="metric.trend"
213
+ icon="activity"
214
+ />
215
+ </MGridItem>
216
+ </MGrid>`,
217
+ rules: ['KPI 用 MPageStat,不要每页自定义 stat CSS', 'responsive="screen" 便于窄屏降列'],
218
+ rulesEn: ['Use MPageStat for KPIs', 'Use responsive="screen" for narrow layouts'],
219
+ avoid: ['不要每个指标手写 font-size/颜色'],
220
+ avoidEn: ['Do not hand-write font sizes/colors per metric'],
221
+ },
222
+ {
223
+ id: 'dashboard-chart-card',
224
+ title: '仪表盘图表卡片',
225
+ titleEn: 'Dashboard chart card',
226
+ description: 'MCard 标题 + MPagePlaceholder 作为图表占位。',
227
+ descriptionEn: 'MCard title with MPagePlaceholder for chart area.',
228
+ pageTypes: ['dashboard'],
229
+ keywords: ['chart', '图表', 'placeholder', 'card', 'trend'],
230
+ imports: ['MCard', 'MPagePlaceholder', 'MSkeleton'],
231
+ template: `<MCard title="趋势概览">
232
+ <MSkeleton v-if="loading" height="8rem" />
233
+ <MPagePlaceholder
234
+ v-else
235
+ aria-label="图表占位"
236
+ description="图表区域(接入 ECharts / 业务组件)"
237
+ />
238
+ </MCard>`,
239
+ rules: ['图表区适合 MCard,因为需要 card 标题', '加载中用 MSkeleton'],
240
+ rulesEn: ['Charts belong in MCard when a card title is needed', 'Use MSkeleton while loading'],
241
+ avoid: ['不要用 MPageFilters 风格包裹图表'],
242
+ avoidEn: ['Do not wrap charts with MPageFilters styling'],
243
+ },
244
+ {
245
+ id: 'dashboard-recent-table',
246
+ title: '仪表盘最近列表',
247
+ titleEn: 'Dashboard recent list card',
248
+ description: 'MCard 内嵌小尺寸 MTable 展示最近记录。',
249
+ descriptionEn: 'Recent records in a small MTable inside MCard.',
250
+ pageTypes: ['dashboard'],
251
+ keywords: ['recent', '最近', 'table', 'card', 'list'],
252
+ imports: ['MCard', 'MTable', 'MTag'],
253
+ scriptSetup: `const recentColumns = [
254
+ { key: 'id', label: '工单号', width: 96 },
255
+ { key: 'title', label: '标题' },
256
+ { key: 'status', label: '状态', width: 96 },
257
+ ]
258
+ const recentRows = ref<Record<string, unknown>[]>([])`,
259
+ template: `<MCard title="最近工单">
260
+ <MTable :columns="recentColumns" :rows="recentRows" size="small" :paginator="false" bordered />
261
+ </MCard>`,
262
+ rules: ['明细列表放 MCard 内合理', 'size="small" 适合卡片内表格'],
263
+ rulesEn: ['Detail lists inside MCard are appropriate', 'Use size="small" for in-card tables'],
264
+ avoid: ['卡片内表格不要再外包一层 MPageContent'],
265
+ avoidEn: ['Do not wrap in-card tables with another MPageContent'],
266
+ },
267
+ {
268
+ id: 'page-content-list',
269
+ title: '列表页内容容器',
270
+ titleEn: 'List page content stack',
271
+ description: 'MLayoutContent 内的标准垂直堆叠容器。',
272
+ descriptionEn: 'Standard vertical stack container inside MLayoutContent.',
273
+ pageTypes: ['list', 'common'],
274
+ keywords: ['page content', 'stack', 'layout content', '容器'],
275
+ imports: ['MPageContent'],
276
+ template: `<MLayoutContent>
277
+ <MPageContent>
278
+ <!-- MPageFilters → MPageToolbar → MTable -->
279
+ </MPageContent>
280
+ </MLayoutContent>`,
281
+ rules: ['所有列表区块放在 MPageContent 内', '区块顺序:筛选 → 工具栏 → 表格'],
282
+ rulesEn: ['Keep all list sections inside MPageContent', 'Order: filters → toolbar → table'],
283
+ avoid: ['不要在 MLayoutContent 上手写 padding/gap'],
284
+ avoidEn: ['Do not hand-write padding/gap on MLayoutContent'],
285
+ },
286
+ {
287
+ id: 'page-content-form',
288
+ title: '表单页内容容器',
289
+ titleEn: 'Form page content stack',
290
+ description: '窄宽表单页容器。',
291
+ descriptionEn: 'Narrow form page container.',
292
+ pageTypes: ['form', 'common'],
293
+ keywords: ['narrow', 'form content', 'max-width'],
294
+ imports: ['MPageContent'],
295
+ template: `<MLayoutContent>
296
+ <MPageContent width="narrow">
297
+ <!-- MPageHeader → MPageSection form → actions -->
298
+ </MPageContent>
299
+ </MLayoutContent>`,
300
+ rules: ['表单页使用 width="narrow"', '不要手写 max-width CSS'],
301
+ rulesEn: ['Use width="narrow" on form pages', 'Do not hand-write max-width CSS'],
302
+ avoid: ['不要让表单字段铺满超宽视口'],
303
+ avoidEn: ['Do not let form fields stretch across an ultra-wide viewport'],
304
+ },
305
+ {
306
+ id: 'detail-toolbar',
307
+ title: '详情页顶栏',
308
+ titleEn: 'Detail page header actions',
309
+ description: '详情页标题与编辑操作。',
310
+ descriptionEn: 'Detail page title and edit action.',
311
+ pageTypes: ['detail'],
312
+ keywords: ['detail', '详情', 'edit', 'toolbar'],
313
+ imports: ['MPageToolbar', 'MButton', 'MTag'],
314
+ template: `<MPageToolbar title="示例资源">
315
+ <template #actions>
316
+ <MButton severity="primary" outlined>编辑</MButton>
317
+ </template>
318
+ </MPageToolbar>
319
+ <MTag value="正常" severity="success" />`,
320
+ rules: ['状态 Tag 放在标题区附近', '编辑为主操作,删除放低强调区域'],
321
+ rulesEn: ['Keep status Tag near the header area', 'Edit is primary; delete stays low emphasis'],
322
+ avoid: ['不要把操作散落到多个无关区域'],
323
+ avoidEn: ['Do not scatter actions across unrelated areas'],
324
+ },
325
+ {
326
+ id: 'layout-app-shell',
327
+ title: '后台应用骨架',
328
+ titleEn: 'Admin app shell',
329
+ description: 'MLayout fillViewport + Header + Content 的最小骨架。',
330
+ descriptionEn: 'Minimal MLayout fillViewport shell with header and content.',
331
+ pageTypes: ['common'],
332
+ keywords: ['layout', 'shell', 'breadcrumb', 'sider', 'app'],
333
+ imports: ['MLayout', 'MLayoutHeader', 'MLayoutContent', 'MBreadcrumb', 'MConfigProvider', 'zhCN'],
334
+ 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>
344
+ </MLayout>
345
+ </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'],
350
+ },
351
+ ];
352
+ export function findPageSnippet(id) {
353
+ const key = id.trim().toLowerCase().replace(/[-_\s]/g, '');
354
+ return pageSnippets.find((item) => item.id.replace(/[-_\s]/g, '') === key);
355
+ }
356
+ export function scorePageSnippet(snippet, query) {
357
+ const normalized = query.toLowerCase().trim();
358
+ if (!normalized)
359
+ return 0;
360
+ let score = 0;
361
+ if (snippet.id.replace(/[-_\s]/g, '') === normalized.replace(/[-_\s]/g, ''))
362
+ score += 100;
363
+ for (const keyword of snippet.keywords) {
364
+ const key = keyword.toLowerCase();
365
+ if (normalized === key)
366
+ score += 40;
367
+ else if (normalized.includes(key) || key.includes(normalized))
368
+ score += 15;
369
+ }
370
+ for (const type of snippet.pageTypes) {
371
+ if (normalized.includes(type))
372
+ score += 10;
373
+ }
374
+ if (snippet.title.toLowerCase().includes(normalized) || snippet.titleEn.toLowerCase().includes(normalized)) {
375
+ score += 25;
376
+ }
377
+ return score;
378
+ }
379
+ export function filterPageSnippets(args) {
380
+ const pageType = args.pageType?.trim().toLowerCase();
381
+ let items = pageSnippets.filter((item) => {
382
+ if (!pageType)
383
+ return true;
384
+ if (item.pageTypes.includes(pageType))
385
+ return true;
386
+ return item.pageTypes.length === 1 && item.pageTypes[0] === 'common';
387
+ });
388
+ if (args.query?.trim()) {
389
+ items = items
390
+ .map((item) => ({ item, score: scorePageSnippet(item, args.query) }))
391
+ .filter((entry) => entry.score > 0)
392
+ .sort((a, b) => b.score - a.score || a.item.id.localeCompare(b.item.id))
393
+ .map((entry) => entry.item);
394
+ }
395
+ const offset = Math.max(args.offset ?? 0, 0);
396
+ const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);
397
+ return {
398
+ total: items.length,
399
+ items: items.slice(offset, offset + limit),
400
+ offset,
401
+ limit,
402
+ hasMore: offset + limit < items.length,
403
+ nextOffset: offset + limit < items.length ? offset + limit : undefined,
404
+ };
405
+ }
@@ -28,6 +28,26 @@ export declare const designRules: {
28
28
  readonly typography: "--m-font-size-xs/sm/md/lg";
29
29
  readonly motion: "--m-motion-fast/normal";
30
30
  };
31
+ readonly composition: {
32
+ readonly workflow: readonly ["For full pages: recommend_page then get_golden_page before writing layout code.", "For local edits: get_page_snippet(section) for filters/toolbar/form-actions/KPI blocks instead of reading the whole golden page.", "Use MLayout fillViewport as the app shell; put MPageContent inside MLayoutContent.", "Prefer MPage* components over scoped CSS for filters, toolbars, headers, form surfaces, and KPI cards.", "Use MSpace or MFlex for control groups inside MPageFilters; use MPageToolbar for title + primary action."];
33
+ readonly snippets: readonly ["list-filters", "list-toolbar", "list-table", "list-row-actions", "form-header", "form-body", "form-actions", "dashboard-kpi-grid", "dashboard-chart-card", "layout-app-shell"];
34
+ readonly pageStack: {
35
+ readonly list: readonly ["MLayout", "MLayoutHeader", "MLayoutContent", "MPageContent", "MPageFilters", "MPageToolbar", "MTable"];
36
+ readonly form: readonly ["MLayout", "MLayoutHeader", "MLayoutContent", "MPageContent", "MPageHeader", "MPageSection", "MForm"];
37
+ readonly dashboard: readonly ["MLayout", "MLayoutHeader", "MLayoutContent", "MPageContent", "MPageHeader", "MGrid", "MPageStat", "MCard"];
38
+ };
39
+ readonly spacing: {
40
+ readonly pageContent: "MPageContent handles padding (--m-space-6) and gap (--m-space-4); spacious uses --m-space-6 gap";
41
+ readonly filters: "MPageFilters handles padded muted surface; do not wrap filters in MCard";
42
+ readonly toolbar: "MPageToolbar handles title/actions alignment with --m-space-4 gap";
43
+ readonly form: "MPageSection variant=\"form\" handles form padding/border/shadow";
44
+ };
45
+ readonly borders: {
46
+ readonly do: readonly ["MLayoutHeader bordered (default)", "MPageFilters for filter regions", "MPageSection variant=\"form\" for forms", "MTable bordered when table needs grid lines"];
47
+ 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"];
48
+ };
49
+ readonly goldenPages: readonly ["list-page", "form-page", "dashboard-page"];
50
+ };
31
51
  readonly actions: {
32
52
  readonly primary: {
33
53
  readonly component: "MButton";
package/dist/patterns.js CHANGED
@@ -32,7 +32,10 @@ export const pagePatterns = [
32
32
  { component: 'LayoutHeader', role: '顶栏面包屑' },
33
33
  { component: 'LayoutContent', role: '页面主体内容区' },
34
34
  { component: 'Breadcrumb', role: '页面层级导航' },
35
- { component: 'Space', role: '筛选区与行操作间距布局' },
35
+ { component: 'PageContent', role: '内容区垂直堆叠与间距', reason: '替代手写 gap/padding CSS' },
36
+ { component: 'PageFilters', role: '筛选区表面与边框' },
37
+ { component: 'PageToolbar', role: '标题与主操作两端对齐' },
38
+ { component: 'Space', role: '筛选控件与行操作间距' },
36
39
  { component: 'Input', role: '关键词筛选' },
37
40
  { component: 'Select', role: '枚举或状态筛选' },
38
41
  { component: 'Button', role: '查询、新增和行操作' },
@@ -47,15 +50,16 @@ export const pagePatterns = [
47
50
  ' └── MLayout',
48
51
  ' ├── MLayoutHeader → MBreadcrumb',
49
52
  ' └── MLayoutContent',
50
- ' ├── Filter section (MSpace + MInput + MSelect + MButton)',
51
- ' ├── PageHeader (title + primary action)',
52
- ' └── MTable (paginator, #cell-* slots, #empty)',
53
+ ' └── MPageContent',
54
+ ' ├── MPageFilters (MSpace + MInput + MSelect + MButton)',
55
+ ' ├── MPageToolbar (title + primary action)',
56
+ ' └── MTable (paginator, #cell-* slots, #empty)',
53
57
  ],
54
58
  layout: {
55
- page: 'MLayout has-sider 作为后台骨架;内容区 padding 使用 --m-space-6',
56
- header: 'MLayoutHeader 放面包屑;标题与主操作在内容区 toolbar 两端对齐',
57
- filters: '筛选区使用浅色背景 + border-radius;桌面端 MSpace wrap,窄屏堆叠',
58
- data: 'MTable 启用 paginator;行操作用 MButton size="small";空态用 #empty 插槽',
59
+ page: 'MLayout fillViewport + has-sider;MLayoutContent 内用 MPageContent 堆叠区块',
60
+ header: 'MLayoutHeader 放面包屑;标题与主操作用 MPageToolbar',
61
+ filters: 'MPageFilters 包裹筛选控件;内部 MSpace wrap 或 MFlex',
62
+ data: 'MTable 直接放在 MPageContent 内,不要额外 MCard;paginator + #empty',
59
63
  },
60
64
  styleRules: [
61
65
  '新增、保存等主操作使用 primary MButton',
@@ -90,7 +94,10 @@ export const pagePatterns = [
90
94
  { component: 'ConfigProvider', role: '应用根包裹' },
91
95
  { component: 'Layout', role: '页面骨架' },
92
96
  { component: 'LayoutHeader', role: '顶栏面包屑' },
93
- { component: 'LayoutContent', role: '表单主体区', reason: 'max-width 约 42rem' },
97
+ { component: 'LayoutContent', role: '页面主体滚动区' },
98
+ { component: 'PageContent', role: '表单页窄宽与垂直间距', reason: 'width="narrow"' },
99
+ { component: 'PageHeader', role: '页面标题与说明' },
100
+ { component: 'PageSection', role: '表单表面与底栏', reason: 'variant form / actions' },
94
101
  { component: 'Breadcrumb', role: '页面层级导航' },
95
102
  { component: 'Form', role: '声明式校验和布局' },
96
103
  { component: 'FormItem', role: '字段标签、帮助文本和校验状态', required: false, reason: '由 MForm 同目录导出,作为表单子结构使用' },
@@ -104,16 +111,16 @@ export const pagePatterns = [
104
111
  '└── MLayout',
105
112
  ' ├── MLayoutHeader → MBreadcrumb',
106
113
  ' └── MLayoutContent',
107
- ' ├── Page intro (title + description)',
108
- ' └── MForm',
109
- ' ├── MFormItem fields',
110
- ' └── Actions (submit + cancel)',
114
+ ' └── MPageContent (width="narrow")',
115
+ ' ├── MPageHeader',
116
+ ' └── MPageSection (variant="form")',
117
+ ' └── MForm + MPageSection (variant="actions")',
111
118
  ],
112
119
  layout: {
113
- shell: 'MLayout + MLayoutHeader + MLayoutContent;表单 max-width 约 42rem',
114
- page: '表单主体使用 max-width,避免字段横向过宽',
115
- form: '默认 label-position=top;字段按业务逻辑分组',
116
- actions: '提交按钮在前,取消使用 text secondary,操作区与表单分隔',
120
+ shell: 'MLayout fillViewport + MLayoutHeader + MLayoutContent',
121
+ page: 'MPageContent width="narrow" 限制表单宽度',
122
+ form: 'MPageSection variant="form" 包裹 MForm;label-position=top',
123
+ actions: 'MPageSection variant="actions" 放提交/取消;不要手写 border-top CSS',
117
124
  responsive: '多列字段在窄屏降为单列',
118
125
  },
119
126
  styleRules: [
@@ -148,9 +155,13 @@ export const pagePatterns = [
148
155
  { component: 'LayoutHeader', role: '顶栏与面包屑' },
149
156
  { component: 'LayoutContent', role: '仪表盘内容' },
150
157
  { component: 'Breadcrumb', role: '页面层级导航' },
158
+ { component: 'PageContent', role: '仪表盘垂直间距', reason: 'density="spacious"' },
159
+ { component: 'PageHeader', role: '页面标题' },
160
+ { component: 'PageStat', role: 'KPI 指标卡' },
161
+ { component: 'PagePlaceholder', role: '图表占位' },
151
162
  { component: 'Grid', role: 'KPI 与内容区栅格' },
152
163
  { component: 'GridItem', role: '栅格子项' },
153
- { component: 'Card', role: '指标和内容分区' },
164
+ { component: 'Card', role: '图表与列表明细分区' },
154
165
  { component: 'Icon', role: 'KPI 图标', required: false },
155
166
  { component: 'Tag', role: '状态和告警级别' },
156
167
  { component: 'Table', role: '明细数据', required: false },
@@ -321,6 +332,48 @@ export const designRules = {
321
332
  typography: '--m-font-size-xs/sm/md/lg',
322
333
  motion: '--m-motion-fast/normal',
323
334
  },
335
+ composition: {
336
+ workflow: [
337
+ 'For full pages: recommend_page then get_golden_page before writing layout code.',
338
+ 'For local edits: get_page_snippet(section) for filters/toolbar/form-actions/KPI blocks instead of reading the whole golden page.',
339
+ 'Use MLayout fillViewport as the app shell; put MPageContent inside MLayoutContent.',
340
+ 'Prefer MPage* components over scoped CSS for filters, toolbars, headers, form surfaces, and KPI cards.',
341
+ 'Use MSpace or MFlex for control groups inside MPageFilters; use MPageToolbar for title + primary action.',
342
+ ],
343
+ snippets: [
344
+ 'list-filters',
345
+ 'list-toolbar',
346
+ 'list-table',
347
+ 'list-row-actions',
348
+ 'form-header',
349
+ 'form-body',
350
+ 'form-actions',
351
+ 'dashboard-kpi-grid',
352
+ 'dashboard-chart-card',
353
+ 'layout-app-shell',
354
+ ],
355
+ pageStack: {
356
+ list: ['MLayout', 'MLayoutHeader', 'MLayoutContent', 'MPageContent', 'MPageFilters', 'MPageToolbar', 'MTable'],
357
+ form: ['MLayout', 'MLayoutHeader', 'MLayoutContent', 'MPageContent', 'MPageHeader', 'MPageSection', 'MForm'],
358
+ dashboard: ['MLayout', 'MLayoutHeader', 'MLayoutContent', 'MPageContent', 'MPageHeader', 'MGrid', 'MPageStat', 'MCard'],
359
+ },
360
+ spacing: {
361
+ pageContent: 'MPageContent handles padding (--m-space-6) and gap (--m-space-4); spacious uses --m-space-6 gap',
362
+ filters: 'MPageFilters handles padded muted surface; do not wrap filters in MCard',
363
+ toolbar: 'MPageToolbar handles title/actions alignment with --m-space-4 gap',
364
+ form: 'MPageSection variant="form" handles form padding/border/shadow',
365
+ },
366
+ borders: {
367
+ do: ['MLayoutHeader bordered (default)', 'MPageFilters for filter regions', 'MPageSection variant="form" for forms', 'MTable bordered when table needs grid lines'],
368
+ avoid: [
369
+ 'MCard wrapping MTable that is already bordered',
370
+ 'Nested MCard with bordered=true on both levels',
371
+ 'Hand-written section borders when MPageFilters or MPageSection applies',
372
+ 'Double borders on filter + table wrapper',
373
+ ],
374
+ },
375
+ goldenPages: ['list-page', 'form-page', 'dashboard-page'],
376
+ },
324
377
  actions: {
325
378
  primary: { component: 'MButton', props: ['severity omitted or primary'] },
326
379
  secondary: { component: 'MButton', props: ['severity="secondary"', 'outlined or text'] },