@morya-ui/mcp 0.3.5 → 0.3.7

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,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,5 +1,5 @@
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
4
  it('finds composition-first snippets by id', () => {
5
5
  for (const id of [
@@ -38,9 +38,17 @@ describe('page snippets catalog', () => {
38
38
  expect(scorePageSnippet(snippet, 'filters')).toBeGreaterThan(0);
39
39
  expect(scorePageSnippet(snippet, 'search')).toBeGreaterThan(0);
40
40
  });
41
- it('filters snippets by page type', () => {
42
- const result = filterPageSnippets({ pageType: 'dashboard' });
43
- expect(result.items.some((item) => item.id === 'dashboard-kpi-grid')).toBe(true);
44
- 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');
45
53
  });
46
54
  });
@@ -315,26 +315,26 @@ describe('@morya-ui/mcp handlers', () => {
315
315
  expect(result.ok).toBe(true);
316
316
  expect(result.suggestions.some((item) => item.type === 'menu-missing-icons')).toBe(false);
317
317
  });
318
- it('flags MTable data prop as contract advisory', () => {
318
+ it('flags MTable data prop as contract failure', () => {
319
319
  const result = read(handlers.validatePage({
320
320
  code: '<MTable :columns="columns" :data="rows" />',
321
321
  }));
322
- expect(result.ok).toBe(true);
323
- expect(result.suggestions.some((item) => item.type === 'table-data-prop')).toBe(true);
322
+ expect(result.ok).toBe(false);
323
+ expect(result.suggestions.some((item) => item.type === 'table-data-prop' && item.severity === 'contract')).toBe(true);
324
324
  });
325
325
  it('flags MMessage severity as inline-alert misuse', () => {
326
326
  const result = read(handlers.validatePage({
327
327
  code: '<MMessage severity="error">登录失败</MMessage>',
328
328
  }));
329
- expect(result.ok).toBe(true);
330
- expect(result.suggestions.some((item) => item.type === 'mmessage-as-alert')).toBe(true);
329
+ expect(result.ok).toBe(false);
330
+ expect(result.suggestions.some((item) => item.type === 'mmessage-as-alert' && item.severity === 'contract')).toBe(true);
331
331
  });
332
332
  it('flags one-line toast string as message preference', () => {
333
333
  const result = read(handlers.validatePage({
334
334
  code: `toast.success('已保存')`,
335
335
  }));
336
- expect(result.ok).toBe(true);
337
- expect(result.suggestions.some((item) => item.type === 'toast-one-liner')).toBe(true);
336
+ expect(result.ok).toBe(false);
337
+ expect(result.suggestions.some((item) => item.type === 'toast-one-liner' && item.severity === 'contract')).toBe(true);
338
338
  });
339
339
  it('flags MTag in cell-status when MStatus is missing', () => {
340
340
  const result = read(handlers.validatePage({
@@ -407,10 +407,12 @@ describe('@morya-ui/mcp handlers', () => {
407
407
  expect(result.counts.decisions).toBeGreaterThan(0);
408
408
  expect(result.counts.resources).toBeGreaterThan(100);
409
409
  expect(result.counts.resourceTemplates).toBe(3);
410
- expect(result.tools).toHaveLength(19);
410
+ expect(result.tools).toHaveLength(21);
411
411
  expect(result.tools).toContain('get_golden_page');
412
412
  expect(result.tools).toContain('get_page_snippet');
413
413
  expect(result.tools).toContain('get_style_direction');
414
+ expect(result.tools).toContain('map_reference');
415
+ expect(result.tools).toContain('get_style_shells');
414
416
  expect(result.tools).not.toContain('list_style_presets');
415
417
  expect(result.tools).not.toContain('get_style_preset');
416
418
  expect(result.tools).toContain('validate_page');
package/dist/decisions.js CHANGED
@@ -234,14 +234,20 @@ export const componentDecisions = [
234
234
  ['全视口主列表可 MPageContent fill + MTable fill;嵌入/短页跳过', 'Full-viewport main lists may use fill; skip for embedded/short pages'],
235
235
  ['分页:paginator + rows-per-page 或 v-model:page', 'Paging: paginator + rows-per-page or v-model:page'],
236
236
  ['行选择:selectionMode + v-model:selection', 'Row select: selectionMode + v-model:selection'],
237
+ ['大行数:virtual + maxHeight/tableHeight/fill', 'Large row sets: virtual + maxHeight/tableHeight/fill'],
238
+ ['列宽/表头筛选:resizable / filterable', 'Column resize/header filter: resizable / filterable'],
239
+ ['树表:tree-config + v-model:expanded-row-keys(children 或 transform)', 'Tree: tree-config + v-model:expanded-row-keys (children or transform)'],
237
240
  ], [
238
241
  ['#cell-{key} 自定义单元格', '#cell-{key} custom cells'],
239
242
  ['#empty 放 MEmpty', '#empty with MEmpty'],
243
+ ['多级表头 children;表尾 show-footer + footer-method', 'Grouped headers via children; footer via show-footer + footer-method'],
240
244
  ]),
241
245
  ...anti([
242
246
  [':data → :rows', ':data → :rows'],
243
247
  ['手写 <table> → MTable', 'Hand-rolled <table> → MTable'],
244
248
  ['嵌入表硬套 fill → 去掉 fill', 'Forced fill on embedded table → remove fill'],
249
+ ['无高度开 virtual → 配 maxHeight/tableHeight/fill', 'virtual without height → set maxHeight/tableHeight/fill'],
250
+ ['无层级硬套 treeConfig → 普通 Table', 'No hierarchy but treeConfig → plain Table'],
245
251
  ]),
246
252
  },
247
253
  {
@@ -261,22 +267,6 @@ export const componentDecisions = [
261
267
  ], [['#list / #grid 等项模板', '#list / #grid item templates']]),
262
268
  ...anti([['需要列对齐比较仍用 DataView → Table', 'Need column compare but used DataView → Table']]),
263
269
  },
264
- {
265
- component: 'TreeTable',
266
- when: ['数据同时具有表格列和父子层级', '用户需要展开、收起层级节点'],
267
- whenEn: [
268
- 'Data has both table columns and parent-child hierarchy',
269
- 'Users need to expand and collapse hierarchy nodes',
270
- ],
271
- avoidWhen: ['数据没有真实层级关系', '普通 Table 已能表达关系'],
272
- avoidWhenEn: ['There is no real hierarchy', 'A regular Table already expresses the relationship'],
273
- recipe: recipe([
274
- ['columns + 带 children 的行数据', 'columns + rows with children'],
275
- ['展开相关 prop 按文档(expandedKeys 等)', 'Expansion props per docs (expandedKeys etc.)'],
276
- ['row-key 稳定', 'Stable row-key'],
277
- ], [['#cell-{key}', '#cell-{key}']]),
278
- ...anti([['无层级硬用 TreeTable → Table', 'No hierarchy but TreeTable → Table']]),
279
- },
280
270
  {
281
271
  component: 'Tree',
282
272
  when: ['主要任务是浏览或选择层级节点', '节点信息不需要多列比较'],
package/dist/index.js CHANGED
@@ -86,20 +86,49 @@ register('get_pattern', 'Read the structure, component composition, layout, and
86
86
  pattern: z.string().min(1),
87
87
  mode: z.string().optional(),
88
88
  }, async (args) => handlers.getPattern(args));
89
- register('recommend_page', 'Recommend a page pattern and component composition from product intent. Pass style as free-text user description when they specified a look (no preset catalog). Pass includeScaffold: true for starter Vue code.', {
89
+ register('recommend_page', 'Recommend a page pattern and component composition from product intent. Pass style as free-text look; density=compact|default|spacious; brief for reference fidelity mapping; includeScaffold for optional golden structure.', {
90
90
  intent: z.string().min(1),
91
91
  pageType: z.string().optional(),
92
92
  features: z.array(z.string()).max(20).optional(),
93
93
  style: z.string().optional().describe('Optional free-text style description from the user (not a preset id).'),
94
+ density: z.string().optional().describe('compact | default | spacious'),
95
+ brief: z
96
+ .object({
97
+ description: z.string().optional(),
98
+ intent: z.string().optional(),
99
+ surface: z.string().optional(),
100
+ density: z.string().optional(),
101
+ requiredBlocks: z.array(z.string()).max(30).optional(),
102
+ requiredComponents: z.array(z.string()).max(30).optional(),
103
+ primaryAction: z.string().optional(),
104
+ style: z.string().optional(),
105
+ })
106
+ .optional(),
94
107
  mode: z.string().optional(),
95
108
  includeScaffold: z.boolean().optional(),
96
109
  }, async (args) => handlers.recommendPage(args));
97
- register('get_design_rules', 'Return design-token, style-direction rules, semantic-action, accessibility, and composition rules for generated pages.', { mode: z.string().optional() }, async (args) => handlers.getDesignRules(args));
110
+ register('map_reference', 'Map a reference brief (screenshot/mock description, required blocks) to snippets, token roles, and a signature shell. Pass the same brief to validate_page.', {
111
+ description: z.string().optional(),
112
+ intent: z.string().optional(),
113
+ surface: z.string().optional(),
114
+ density: z.string().optional(),
115
+ requiredBlocks: z.array(z.string()).max(30).optional(),
116
+ requiredComponents: z.array(z.string()).max(30).optional(),
117
+ primaryAction: z.string().optional(),
118
+ style: z.string().optional(),
119
+ mode: z.string().optional(),
120
+ }, async (args) => handlers.mapReference(args));
121
+ register('get_design_rules', 'Return design-token, style-direction rules, signature shell catalog, semantic-action, accessibility, and composition rules for generated pages.', { mode: z.string().optional() }, async (args) => handlers.getDesignRules(args));
98
122
  register('get_style_direction', 'Resolve visual style direction: follow user reference/description, infer from clear cues, or ask when uncertain. No named preset catalog.', {
99
123
  intent: z.string().optional(),
100
124
  style: z.string().optional().describe('Free-text style description from the user.'),
101
125
  mode: z.string().optional(),
102
126
  }, async (args) => handlers.getStyleDirection(args));
127
+ register('get_style_shells', 'List or read token-only signature shells (ops-quiet, account-split, express-hero, …) with pasteable CSS.', {
128
+ shell: z.string().optional().describe('Shell id for full CSS.'),
129
+ surface: z.string().optional().describe('Filter: ops | account | flow | express'),
130
+ mode: z.string().optional(),
131
+ }, async (args) => handlers.getStyleShells(args));
103
132
  register('recommend_component', 'List, read, or recommend component selection guides. Omit query/decision to list; pass decision only to read; pass query to recommend.', {
104
133
  query: z.string().optional(),
105
134
  decision: z.string().optional(),
@@ -112,9 +141,23 @@ register('get_golden_page', 'Read a golden page Vue source sample (list-page, li
112
141
  page: z.string().min(1),
113
142
  mode: z.string().optional(),
114
143
  }, async (args) => handlers.getGoldenPage(args));
115
- register('validate_page', 'Validate a generated page for composition, spacing, and border anti-patterns.', {
144
+ register('validate_page', 'Validate generated page code: contract issues (ok=false) vs craft suggestions. Pass brief/requiredBlocks for fidelity checks against a reference map.', {
116
145
  code: z.string().min(1),
117
146
  mode: z.string().optional(),
147
+ requiredBlocks: z.array(z.string()).max(30).optional(),
148
+ requiredComponents: z.array(z.string()).max(30).optional(),
149
+ brief: z
150
+ .object({
151
+ description: z.string().optional(),
152
+ intent: z.string().optional(),
153
+ surface: z.string().optional(),
154
+ density: z.string().optional(),
155
+ requiredBlocks: z.array(z.string()).max(30).optional(),
156
+ requiredComponents: z.array(z.string()).max(30).optional(),
157
+ primaryAction: z.string().optional(),
158
+ style: z.string().optional(),
159
+ })
160
+ .optional(),
118
161
  }, async (args) => handlers.validatePage(args));
119
162
  register('list_page_snippets', 'List reusable page section snippets for local edits (filters, toolbar, form actions, KPI grid, etc.).', {
120
163
  query: z.string().optional(),