@morya-ui/mcp 0.3.3 → 0.3.4

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>
@@ -173,6 +173,25 @@ describe('@morya-ui/mcp handlers', () => {
173
173
  expect(result.ok).toBe(true);
174
174
  expect(result.suggestions.some((item) => item.type === 'native-scroll')).toBe(true);
175
175
  });
176
+ it('suggests list fill height only for fillViewport admin lists missing fill', () => {
177
+ const result = read(handlers.validatePage({
178
+ code: '<MLayout fill-viewport><MPageContent><MTable :columns="c" :rows="r" paginator /></MPageContent></MLayout>',
179
+ }));
180
+ expect(result.ok).toBe(true);
181
+ expect(result.suggestions.some((item) => item.type === 'list-fill-height' && item.standardId === 'page-sections')).toBe(true);
182
+ });
183
+ it('does not suggest list fill when PageContent and Table already fill', () => {
184
+ const result = read(handlers.validatePage({
185
+ code: '<MLayout fill-viewport><MPageContent fill><MTable fill paginator :columns="c" :rows="r" /></MPageContent></MLayout>',
186
+ }));
187
+ expect(result.suggestions.some((item) => item.type === 'list-fill-height')).toBe(false);
188
+ });
189
+ it('does not suggest list fill for paginator tables outside fillViewport shells', () => {
190
+ const result = read(handlers.validatePage({
191
+ code: '<MPageContent><MTable :columns="c" :rows="r" paginator /></MPageContent>',
192
+ }));
193
+ expect(result.suggestions.some((item) => item.type === 'list-fill-height')).toBe(false);
194
+ });
176
195
  it('returns scrollable-panel snippet for scroll queries', () => {
177
196
  const result = read(handlers.getPageSnippet({ section: 'scrollable-panel' }));
178
197
  expect(result.id).toBe('scrollable-panel');
@@ -186,6 +205,60 @@ describe('@morya-ui/mcp handlers', () => {
186
205
  expect(result.nextStep).toContain('get_golden_page');
187
206
  expect(result.pageStandards).toBeUndefined();
188
207
  expect(result.scrollStandards).toBeUndefined();
208
+ expect(result.styleDirection?.candidates?.length).toBeGreaterThan(0);
209
+ });
210
+ it('resolves an explicit style preset on recommend_page', () => {
211
+ const result = read(handlers.recommendPage({ intent: '用户列表', pageType: 'list', style: 'soft' }));
212
+ expect(result.styleDirection.resolution).toBe('preset');
213
+ expect(result.styleDirection.preset?.id).toBe('soft');
214
+ expect(result.styleDirection.guidance).toContain('soft');
215
+ expect(result.goldenPage).toContain('list-page');
216
+ });
217
+ it('routes dense/rail styles to list craft golden variants', () => {
218
+ const dense = read(handlers.recommendPage({
219
+ intent: '监控告警列表',
220
+ pageType: 'list',
221
+ style: 'dense',
222
+ includeScaffold: true,
223
+ }));
224
+ expect(dense.goldenPage).toContain('list-page-dense');
225
+ expect(dense.scaffold.goldenPage).toBe('list-page-dense');
226
+ expect(dense.scaffold.files.component).toContain('density="compact"');
227
+ expect(dense.nextStep).toContain('list-page-dense');
228
+ const rail = read(handlers.recommendPage({
229
+ intent: '品牌色后台列表',
230
+ pageType: 'list',
231
+ style: 'rail',
232
+ includeScaffold: true,
233
+ }));
234
+ expect(rail.goldenPage).toContain('list-page-rail');
235
+ expect(rail.scaffold.goldenPage).toBe('list-page-rail');
236
+ expect(rail.scaffold.files.component).toContain('gp-rail-sider');
237
+ });
238
+ it('returns dense and rail golden page sources', () => {
239
+ const dense = read(handlers.getGoldenPage({ page: 'list-page-dense' }));
240
+ expect(dense.id).toBe('list-page-dense');
241
+ expect(dense.style).toBe('dense');
242
+ expect(dense.variantOf).toBe('list-page');
243
+ expect(dense.source).toContain('size="small"');
244
+ const rail = read(handlers.getGoldenPage({ page: 'list-page-rail' }));
245
+ expect(rail.id).toBe('list-page-rail');
246
+ expect(rail.style).toBe('rail');
247
+ expect(rail.source).toContain('color-mix');
248
+ });
249
+ it('lists style presets with resolution priority', () => {
250
+ const result = read(handlers.listStylePresets({ mode: 'zh-CN' }));
251
+ expect(result.presets.map((item) => item.id)).toEqual(expect.arrayContaining(['quiet', 'soft', 'dense', 'rail', 'studio', 'ink']));
252
+ expect(result.resolution[0]).toContain('参考');
253
+ });
254
+ it('reads a style preset by alias', () => {
255
+ const result = read(handlers.getStylePreset({ style: '柔和留白', mode: 'zh-CN' }));
256
+ expect(result.id).toBe('soft');
257
+ expect(result.apply.length).toBeGreaterThan(0);
258
+ });
259
+ it('exposes style presets from get_design_rules', () => {
260
+ const result = read(handlers.getDesignRules({ mode: 'zh-CN' }));
261
+ expect(result.styles.presets.some((item) => item.id === 'dense')).toBe(true);
189
262
  });
190
263
  it('keeps get_golden_page focused on source code', () => {
191
264
  const result = read(handlers.getGoldenPage({ page: 'list-page' }));
@@ -317,9 +390,11 @@ describe('@morya-ui/mcp handlers', () => {
317
390
  expect(result.counts.decisions).toBeGreaterThan(0);
318
391
  expect(result.counts.resources).toBeGreaterThan(100);
319
392
  expect(result.counts.resourceTemplates).toBe(3);
320
- expect(result.tools).toHaveLength(18);
393
+ expect(result.tools).toHaveLength(20);
321
394
  expect(result.tools).toContain('get_golden_page');
322
395
  expect(result.tools).toContain('get_page_snippet');
396
+ expect(result.tools).toContain('list_style_presets');
397
+ expect(result.tools).toContain('get_style_preset');
323
398
  expect(result.tools).toContain('validate_page');
324
399
  expect(result.tools).not.toContain('create_page');
325
400
  });
package/dist/decisions.js CHANGED
@@ -226,6 +226,7 @@ export const componentDecisions = [
226
226
  recipe: recipe([
227
227
  ['columns + rows(没有 data prop)', 'columns + rows (no data prop)'],
228
228
  ['row-key 默认 id;不稳定时显式指定', 'row-key defaults to id; set explicitly when needed'],
229
+ ['全视口主列表可 MPageContent fill + MTable fill;嵌入/短页跳过', 'Full-viewport main lists may use fill; skip for embedded/short pages'],
229
230
  ['分页:paginator + rows-per-page 或 v-model:page', 'Paging: paginator + rows-per-page or v-model:page'],
230
231
  ['行选择:selectionMode + v-model:selection', 'Row select: selectionMode + v-model:selection'],
231
232
  ], [
@@ -235,6 +236,7 @@ export const componentDecisions = [
235
236
  ...anti([
236
237
  [':data → :rows', ':data → :rows'],
237
238
  ['手写 <table> → MTable', 'Hand-rolled <table> → MTable'],
239
+ ['嵌入表硬套 fill → 去掉 fill', 'Forced fill on embedded table → remove fill'],
238
240
  ]),
239
241
  },
240
242
  {
@@ -725,10 +727,45 @@ export const componentDecisions = [
725
727
  recipe: recipe([
726
728
  ['MLayout fill-viewport(或 :fill-viewport="true")', 'MLayout fill-viewport (or :fill-viewport="true")'],
727
729
  ['内容放 MLayoutContent', 'Content in MLayoutContent'],
730
+ ['是否再 fill 表格:见下方「MPageContent fill + MTable fill」判断', 'Whether to fill the table: see “MPageContent fill + MTable fill” below'],
728
731
  ['侧栏用 MLayoutSider + MMenu,不是随便一个 Drawer', 'Sider: MLayoutSider + MMenu, not a random Drawer'],
729
732
  ]),
730
733
  ...anti([['在 Layout 外再包一层 100vh 滚动 → 去掉', 'Extra 100vh scroll wrapper outside Layout → remove']]),
731
734
  },
735
+ {
736
+ component: 'MPageContent fill + MTable fill',
737
+ when: [
738
+ '全视口后台列表,页面主任务就是浏览一张表',
739
+ '内容高度表格会留下大块空白、分页悬在中间不好看',
740
+ '希望只有表体滚动、分页贴在页面最下方',
741
+ ],
742
+ whenEn: [
743
+ 'Full-viewport admin list whose main job is browsing one table',
744
+ 'A content-sized table would leave a large empty band with mid-page pagination',
745
+ 'Only the table body should scroll; pagination should stay at the page bottom',
746
+ ],
747
+ avoidWhen: [
748
+ '仪表盘/详情里的嵌入小表',
749
+ '内容本身很短、内容高度即可',
750
+ '整页应作为文档滚动(长筛选+说明+表格)',
751
+ 'Dialog / Drawer 内表格',
752
+ ],
753
+ avoidWhenEn: [
754
+ 'Embedded tables on dashboards or detail pages',
755
+ 'Short content where a content-sized table is fine',
756
+ 'Whole page should scroll as a document',
757
+ 'Tables inside Dialog / Drawer',
758
+ ],
759
+ recipe: recipe([
760
+ ['先判断是否适合 fill,再写 MPageContent fill + MTable fill', 'Decide fill first, then MPageContent fill + MTable fill'],
761
+ ['paginator 或同级 MPagination', 'paginator or sibling MPagination'],
762
+ ['适合时不要手写 min-height / calc', 'When fill fits, do not hand-write min-height / calc'],
763
+ ]),
764
+ ...anti([
765
+ ['嵌入/短页硬套 fill → 去掉', 'Forced fill on embedded/short page → remove'],
766
+ ['适合 fill 却手写 calc → 改用 fill', 'Hand-written calc when fill fits → use fill'],
767
+ ]),
768
+ },
732
769
  {
733
770
  component: 'MScrollbar',
734
771
  when: [
@@ -796,9 +833,13 @@ export const componentDecisions = [
796
833
  recipe: recipe([
797
834
  ['MPageContent > MPageFilters + MTable', 'MPageContent > MPageFilters + MTable'],
798
835
  ['表格直接放 PageContent,不套 Card', 'Table directly under PageContent, no Card'],
836
+ ['高度:全视口主列表再考虑 fill;嵌入/短页跳过', 'Height: consider fill only for full-viewport main lists'],
799
837
  ['空态用 Table #empty + MEmpty', 'Empty: Table #empty + MEmpty'],
800
838
  ]),
801
- ...anti([['Table 外包 MCard → 去掉 Card', 'MCard around Table → remove Card']]),
839
+ ...anti([
840
+ ['Table 外包 MCard → 去掉 Card', 'MCard around Table → remove Card'],
841
+ ['嵌入表硬套 fill → 去掉 fill', 'Forced fill on embedded table → remove fill'],
842
+ ]),
802
843
  },
803
844
  {
804
845
  component: 'MCard',
@@ -4,10 +4,23 @@ export interface GoldenPageRecord {
4
4
  title: string;
5
5
  titleEn: string;
6
6
  patternId: string;
7
+ /** Visual style preset this sample demonstrates (structure may be shared). */
8
+ style?: string;
9
+ /** Canonical structure id when this is a craft variant. */
10
+ variantOf?: string;
7
11
  }
8
12
  export declare const goldenPageCatalog: GoldenPageRecord[];
9
13
  export declare function listGoldenPages(): GoldenPageRecord[];
10
14
  export declare function findGoldenPage(id: string): GoldenPageRecord | undefined;
15
+ /**
16
+ * Pick the best golden for a pattern + optional style preset.
17
+ * Falls back to the pattern's canonical sample when no style variant exists.
18
+ */
19
+ export declare function resolveGoldenPageId(args: {
20
+ patternId?: string | null;
21
+ canonicalGoldenId?: string | null;
22
+ styleId?: string | null;
23
+ }): string | null;
11
24
  export declare function readGoldenPageSource(id: string): {
12
25
  record: GoldenPageRecord;
13
26
  source: string;
@@ -5,9 +5,28 @@ export const goldenPageCatalog = [
5
5
  {
6
6
  id: 'list-page',
7
7
  file: 'list-page.vue',
8
- title: '列表页黄金样例',
9
- titleEn: 'List page golden sample',
8
+ title: '列表页黄金样例(soft)',
9
+ titleEn: 'List page golden sample (soft)',
10
10
  patternId: 'admin-list',
11
+ style: 'soft',
12
+ },
13
+ {
14
+ id: 'list-page-dense',
15
+ file: 'list-page-dense.vue',
16
+ title: '列表页黄金样例(dense)',
17
+ titleEn: 'List page golden sample (dense)',
18
+ patternId: 'admin-list',
19
+ style: 'dense',
20
+ variantOf: 'list-page',
21
+ },
22
+ {
23
+ id: 'list-page-rail',
24
+ file: 'list-page-rail.vue',
25
+ title: '列表页黄金样例(rail)',
26
+ titleEn: 'List page golden sample (rail)',
27
+ patternId: 'admin-list',
28
+ style: 'rail',
29
+ variantOf: 'list-page',
11
30
  },
12
31
  {
13
32
  id: 'form-page',
@@ -80,6 +99,15 @@ export const goldenPageCatalog = [
80
99
  patternId: 'wizard-form',
81
100
  },
82
101
  ];
102
+ /** Map style preset → list golden craft variant (same structure). */
103
+ const listStyleGolden = {
104
+ soft: 'list-page',
105
+ quiet: 'list-page',
106
+ studio: 'list-page',
107
+ ink: 'list-page',
108
+ dense: 'list-page-dense',
109
+ rail: 'list-page-rail',
110
+ };
83
111
  const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
84
112
  const bundledDir = join(pkgRoot, 'data/golden-pages');
85
113
  const repoDir = join(pkgRoot, '../../design-kit/docs/golden-pages');
@@ -97,6 +125,31 @@ export function findGoldenPage(id) {
97
125
  const key = id.trim().toLowerCase().replace(/\.vue$/i, '').replace(/[-_\s]/g, '');
98
126
  return goldenPageCatalog.find((item) => item.id.replace(/[-_\s]/g, '') === key);
99
127
  }
128
+ /**
129
+ * Pick the best golden for a pattern + optional style preset.
130
+ * Falls back to the pattern's canonical sample when no style variant exists.
131
+ */
132
+ export function resolveGoldenPageId(args) {
133
+ const style = args.styleId?.trim().toLowerCase() || '';
134
+ const canonical = args.canonicalGoldenId?.split('/').pop()?.replace(/\.vue$/i, '') ||
135
+ (args.patternId
136
+ ? goldenPageCatalog.find((item) => item.patternId === args.patternId && !item.variantOf)?.id
137
+ : null) ||
138
+ null;
139
+ if (args.patternId === 'admin-list' || canonical === 'list-page' || canonical?.startsWith('list-page')) {
140
+ const styled = style ? listStyleGolden[style] : null;
141
+ if (styled && findGoldenPage(styled))
142
+ return styled;
143
+ return 'list-page';
144
+ }
145
+ if (canonical && findGoldenPage(canonical))
146
+ return canonical;
147
+ if (args.patternId) {
148
+ const match = goldenPageCatalog.find((item) => item.patternId === args.patternId && !item.variantOf);
149
+ return match?.id ?? null;
150
+ }
151
+ return null;
152
+ }
100
153
  export function readGoldenPageSource(id) {
101
154
  const record = findGoldenPage(id);
102
155
  if (!record)