@jskit-ai/ui-generator 0.1.48 → 0.1.49

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.
@@ -39,6 +39,22 @@ export { addPlacement };
39
39
  export default function getPlacements() {
40
40
  return [];
41
41
  }
42
+ `,
43
+ "utf8"
44
+ );
45
+ await writeFile(
46
+ path.join(appRoot, "src", "placementTopology.js"),
47
+ `const placements = [];
48
+
49
+ function addPlacementTopology(value = {}) {
50
+ placements.push(value);
51
+ }
52
+
53
+ export { addPlacementTopology };
54
+
55
+ export default function getPlacementTopology() {
56
+ return { placements };
57
+ }
42
58
  `,
43
59
  "utf8"
44
60
  );
@@ -99,14 +115,21 @@ test("ui-generator add-subpages derives the default target from an index-route p
99
115
  "packages/main/src/client/providers/MainClientProvider.js",
100
116
  "src/components/menus/SurfaceAwareMenuLinkItem.vue",
101
117
  "src/components/SectionContainerShell.vue",
102
- `src/pages/${targetFile}`
118
+ `src/pages/${targetFile}`,
119
+ "src/placementTopology.js"
103
120
  ]);
104
121
 
105
122
  const pageSource = await readPageFile(appRoot, targetFile);
106
123
  assert.match(
107
124
  pageSource,
108
- /<ShellOutlet target="practice:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
125
+ /<ShellOutlet target="practice:sub-pages" \/>/
109
126
  );
127
+ const topologySource = await readFile(path.join(appRoot, "src", "placementTopology.js"), "utf8");
128
+ assert.match(topologySource, /id: "page\.section-nav"/);
129
+ assert.match(topologySource, /owner: "practice"/);
130
+ assert.match(topologySource, /compact: \{/);
131
+ assert.match(topologySource, /medium: \{/);
132
+ assert.match(topologySource, /expanded: \{/);
110
133
  assert.match(pageSource, /<RouterView \/>/);
111
134
  assert.equal(
112
135
  await readFile(path.join(appRoot, "src", "components", "menus", "SurfaceAwareMenuLinkItem.vue"), "utf8"),
@@ -132,7 +155,7 @@ test("ui-generator add-subpages derives the default target from a dynamic file-r
132
155
  const pageSource = await readPageFile(appRoot, targetFile);
133
156
  assert.match(
134
157
  pageSource,
135
- /<ShellOutlet target="contacts-contact-id:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
158
+ /<ShellOutlet target="contacts-contact-id:sub-pages" \/>/
136
159
  );
137
160
  });
138
161
  });
@@ -154,7 +177,7 @@ test("ui-generator add-subpages derives the default target from a nested route p
154
177
  const pageSource = await readPageFile(appRoot, targetFile);
155
178
  assert.match(
156
179
  pageSource,
157
- /<ShellOutlet target="catalog-products:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
180
+ /<ShellOutlet target="catalog-products:sub-pages" \/>/
158
181
  );
159
182
  });
160
183
  });
@@ -199,7 +222,7 @@ test("ui-generator add-subpages supports explicit target host:position", async (
199
222
  const pageSource = await readPageFile(appRoot, targetFile);
200
223
  assert.match(
201
224
  pageSource,
202
- /<ShellOutlet target="practice-hub:secondary-tabs" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
225
+ /<ShellOutlet target="practice-hub:secondary-tabs" \/>/
203
226
  );
204
227
  });
205
228
  });
@@ -235,7 +258,8 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
235
258
 
236
259
  assert.deepEqual(result.touchedFiles, [
237
260
  "packages/main/src/client/providers/MainClientProvider.js",
238
- `src/pages/${targetFile}`
261
+ `src/pages/${targetFile}`,
262
+ "src/placementTopology.js"
239
263
  ]);
240
264
  assert.equal(
241
265
  await readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
@@ -332,7 +356,7 @@ test("ui-generator add-subpages accepts target files with a src/pages prefix", a
332
356
  const pageSource = await readFile(path.join(appRoot, targetFile), "utf8");
333
357
  assert.match(
334
358
  pageSource,
335
- /<ShellOutlet target="practice:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
359
+ /<ShellOutlet target="practice:sub-pages" \/>/
336
360
  );
337
361
  assert.match(pageSource, /<RouterView \/>/);
338
362
  });
@@ -35,10 +35,71 @@ async function writeShellLayout(appRoot, source = "") {
35
35
  <ShellOutlet
36
36
  target="shell-layout:primary-menu"
37
37
  default
38
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
39
38
  />
40
39
  </div>
41
40
  </template>
41
+ `
42
+ );
43
+ await writePlacementTopology(appRoot);
44
+ }
45
+
46
+ function renderTopologyVariant(outlet, { linkRenderer = "" } = {}) {
47
+ const rendererLines = linkRenderer
48
+ ? `,
49
+ renderers: {
50
+ link: "${linkRenderer}"
51
+ }`
52
+ : "";
53
+ return `{
54
+ outlet: "${outlet}"${rendererLines}
55
+ }`;
56
+ }
57
+
58
+ function renderTopologyEntry({
59
+ id = "",
60
+ owner = "",
61
+ surfaces = ["*"],
62
+ defaultPlacement = false,
63
+ outlet = "",
64
+ linkRenderer = ""
65
+ } = {}) {
66
+ const ownerLine = owner ? ` owner: "${owner}",\n` : "";
67
+ const defaultLine = defaultPlacement ? " default: true,\n" : "";
68
+ return ` {
69
+ id: "${id}",
70
+ ${ownerLine} surfaces: ${JSON.stringify(surfaces)},
71
+ ${defaultLine} variants: {
72
+ compact: ${renderTopologyVariant(outlet, { linkRenderer })},
73
+ medium: ${renderTopologyVariant(outlet, { linkRenderer })},
74
+ expanded: ${renderTopologyVariant(outlet, { linkRenderer })}
75
+ }
76
+ }`;
77
+ }
78
+
79
+ async function writePlacementTopology(appRoot, entries = []) {
80
+ const defaultEntries = [
81
+ renderTopologyEntry({
82
+ id: "shell.primary-nav",
83
+ surfaces: ["*"],
84
+ defaultPlacement: true,
85
+ outlet: "shell-layout:primary-menu",
86
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
87
+ }),
88
+ renderTopologyEntry({
89
+ id: "shell.status",
90
+ surfaces: ["*"],
91
+ outlet: "shell-layout:top-right",
92
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
93
+ })
94
+ ];
95
+ await writeFileInApp(
96
+ appRoot,
97
+ "src/placementTopology.js",
98
+ `export default {
99
+ placements: [
100
+ ${[...defaultEntries, ...entries].join(",\n")}
101
+ ]
102
+ };
42
103
  `
43
104
  );
44
105
  }
@@ -61,8 +122,9 @@ test("buildUiPageTemplateContext resolves link placement from default app ShellO
61
122
  targetFile: "admin/reports/index.vue",
62
123
  options: {}
63
124
  });
64
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-layout:primary-menu");
65
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
125
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.primary-nav");
126
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, "");
127
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
66
128
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
67
129
  assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
68
130
  assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, "");
@@ -116,15 +178,15 @@ test("buildUiPageTemplateContext supports explicit link placement override", asy
116
178
  appRoot,
117
179
  targetFile: "admin/reports/index.vue",
118
180
  options: {
119
- "link-placement": "shell-layout:top-right"
181
+ "link-placement": "shell.status"
120
182
  }
121
183
  });
122
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell-layout:top-right");
123
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
184
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
185
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
124
186
  });
125
187
  });
126
188
 
127
- test("buildUiPageTemplateContext supports explicit package outlet link placement", async () => {
189
+ test("buildUiPageTemplateContext supports explicit package semantic link placement", async () => {
128
190
  await withTempApp(async (appRoot) => {
129
191
  await writeConfig(
130
192
  appRoot,
@@ -136,6 +198,22 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
136
198
  `
137
199
  );
138
200
  await writeShellLayout(appRoot);
201
+ await writePlacementTopology(appRoot, [
202
+ renderTopologyEntry({
203
+ id: "page.section-nav",
204
+ owner: "catalog",
205
+ surfaces: ["admin"],
206
+ outlet: "catalog:sub-pages",
207
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
208
+ }),
209
+ renderTopologyEntry({
210
+ id: "page.section-nav",
211
+ owner: "catalog-products",
212
+ surfaces: ["admin"],
213
+ outlet: "catalog-products:sub-pages",
214
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
215
+ })
216
+ ]);
139
217
  await writeFileInApp(
140
218
  appRoot,
141
219
  ".jskit/lock.json",
@@ -164,13 +242,34 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
164
242
  metadata: {
165
243
  ui: {
166
244
  placements: {
167
- outlets: [
245
+ topology: {
246
+ placements: [
168
247
  {
169
- target: "admin-cog:primary-menu",
170
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
171
- source: "src/client/components/UsersWorkspaceToolsWidget.vue"
248
+ id: "admin.tools-menu",
249
+ surfaces: ["admin"],
250
+ variants: {
251
+ compact: {
252
+ outlet: "admin-cog:primary-menu",
253
+ renderers: {
254
+ link: "local.main.ui.surface-aware-menu-link-item"
255
+ }
256
+ },
257
+ medium: {
258
+ outlet: "admin-cog:primary-menu",
259
+ renderers: {
260
+ link: "local.main.ui.surface-aware-menu-link-item"
261
+ }
262
+ },
263
+ expanded: {
264
+ outlet: "admin-cog:primary-menu",
265
+ renderers: {
266
+ link: "local.main.ui.surface-aware-menu-link-item"
267
+ }
268
+ }
269
+ }
172
270
  }
173
271
  ]
272
+ }
174
273
  }
175
274
  }
176
275
  }
@@ -182,10 +281,10 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
182
281
  appRoot,
183
282
  targetFile: "admin/reports/index.vue",
184
283
  options: {
185
- "link-placement": "admin-cog:primary-menu"
284
+ "link-placement": "admin.tools-menu"
186
285
  }
187
286
  });
188
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin-cog:primary-menu");
287
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin.tools-menu");
189
288
  });
190
289
  });
191
290
 
@@ -200,15 +299,21 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
200
299
  };
201
300
  `
202
301
  );
302
+ await writePlacementTopology(appRoot, [
303
+ renderTopologyEntry({
304
+ id: "page.section-nav",
305
+ owner: "home-settings",
306
+ surfaces: ["home"],
307
+ outlet: "home-settings:primary-menu",
308
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
309
+ })
310
+ ]);
203
311
  await writeFileInApp(
204
312
  appRoot,
205
313
  "src/pages/home/settings.vue",
206
314
  `<template>
207
315
  <section>
208
- <ShellOutlet
209
- target="home-settings:primary-menu"
210
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
211
- />
316
+ <ShellOutlet target="home-settings:primary-menu" />
212
317
  <RouterView />
213
318
  </section>
214
319
  </template>
@@ -221,13 +326,14 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
221
326
  options: {}
222
327
  });
223
328
 
224
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "home-settings:primary-menu");
225
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
329
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
330
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"home-settings\",\n");
331
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
226
332
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
227
333
  });
228
334
  });
229
335
 
230
- test("buildUiPageTemplateContext supports explicit link component token and link-to", async () => {
336
+ test("buildUiPageTemplateContext supports explicit semantic placement and link-to", async () => {
231
337
  await withTempApp(async (appRoot) => {
232
338
  await writeConfig(
233
339
  appRoot,
@@ -244,12 +350,12 @@ test("buildUiPageTemplateContext supports explicit link component token and link
244
350
  appRoot,
245
351
  targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
246
352
  options: {
247
- "link-placement": "shell-layout:top-right",
248
- "link-component-token": "local.main.ui.tab-link-item",
353
+ "link-placement": "shell.status",
249
354
  "link-to": "./notes"
250
355
  }
251
356
  });
252
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
357
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
358
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
253
359
  assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
254
360
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
255
361
  assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
@@ -294,6 +400,15 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
294
400
  `
295
401
  );
296
402
  await writeShellLayout(appRoot);
403
+ await writePlacementTopology(appRoot, [
404
+ renderTopologyEntry({
405
+ id: "page.section-nav",
406
+ owner: "contact-view",
407
+ surfaces: ["admin"],
408
+ outlet: "contact-view:sub-pages",
409
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
410
+ })
411
+ ]);
297
412
  await writeFileInApp(
298
413
  appRoot,
299
414
  "src/pages/admin/contacts/[contactId].vue",
@@ -314,8 +429,9 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
314
429
  options: {}
315
430
  });
316
431
 
317
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "contact-view:sub-pages");
318
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
432
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
433
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"contact-view\",\n");
434
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
319
435
  assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
320
436
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
321
437
  });
@@ -333,6 +449,15 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
333
449
  `
334
450
  );
335
451
  await writeShellLayout(appRoot);
452
+ await writePlacementTopology(appRoot, [
453
+ renderTopologyEntry({
454
+ id: "page.section-nav",
455
+ owner: "contact-view",
456
+ surfaces: ["admin"],
457
+ outlet: "contact-view:sub-pages",
458
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
459
+ })
460
+ ]);
336
461
  await writeFileInApp(
337
462
  appRoot,
338
463
  "src/pages/admin/contacts/[contactId].vue",
@@ -353,8 +478,8 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
353
478
  options: {}
354
479
  });
355
480
 
356
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "contact-view:sub-pages");
357
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
481
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
482
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
358
483
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
359
484
  });
360
485
  });
@@ -371,6 +496,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
371
496
  `
372
497
  );
373
498
  await writeShellLayout(appRoot);
499
+ await writePlacementTopology(appRoot, [
500
+ renderTopologyEntry({
501
+ id: "page.section-nav",
502
+ owner: "catalog",
503
+ surfaces: ["admin"],
504
+ outlet: "catalog:sub-pages",
505
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
506
+ })
507
+ ]);
374
508
  await writeFileInApp(
375
509
  appRoot,
376
510
  "src/pages/admin/catalog/index.vue",
@@ -391,8 +525,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
391
525
  options: {}
392
526
  });
393
527
 
394
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "catalog:sub-pages");
395
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
528
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
529
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog\",\n");
530
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
396
531
  assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
397
532
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
398
533
  });
@@ -410,6 +545,22 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
410
545
  `
411
546
  );
412
547
  await writeShellLayout(appRoot);
548
+ await writePlacementTopology(appRoot, [
549
+ renderTopologyEntry({
550
+ id: "page.section-nav",
551
+ owner: "catalog",
552
+ surfaces: ["admin"],
553
+ outlet: "catalog:sub-pages",
554
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
555
+ }),
556
+ renderTopologyEntry({
557
+ id: "page.section-nav",
558
+ owner: "catalog-products",
559
+ surfaces: ["admin"],
560
+ outlet: "catalog-products:sub-pages",
561
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
562
+ })
563
+ ]);
413
564
  await writeFileInApp(
414
565
  appRoot,
415
566
  "src/pages/admin/catalog/index.vue",
@@ -443,8 +594,9 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
443
594
  options: {}
444
595
  });
445
596
 
446
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "catalog-products:sub-pages");
447
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
597
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
598
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog-products\",\n");
599
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
448
600
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
449
601
  });
450
602
  });
@@ -461,6 +613,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
461
613
  `
462
614
  );
463
615
  await writeShellLayout(appRoot);
616
+ await writePlacementTopology(appRoot, [
617
+ renderTopologyEntry({
618
+ id: "page.section-nav",
619
+ owner: "customer-view",
620
+ surfaces: ["admin"],
621
+ outlet: "customer-view:sub-pages",
622
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
623
+ })
624
+ ]);
464
625
  await writeFileInApp(
465
626
  appRoot,
466
627
  "src/pages/admin/customers/[customerId]/index.vue",
@@ -481,8 +642,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
481
642
  options: {}
482
643
  });
483
644
 
484
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "customer-view:sub-pages");
485
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.surface-aware-menu-link-item");
645
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
646
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"customer-view\",\n");
647
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
486
648
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
487
649
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
488
650
  });
@@ -641,7 +803,7 @@ test("buildUiPageTemplateContext validates link placement format", async () => {
641
803
  "link-placement": "invalid-placement"
642
804
  }
643
805
  }),
644
- /option "placement" must be a target in "host:position" format/
806
+ /option "placement" must be a semantic target in "area.slot" format/
645
807
  );
646
808
  });
647
809
  });
@@ -14,6 +14,39 @@ async function withTempApp(run) {
14
14
  }
15
15
  }
16
16
 
17
+ function renderTopologyEntry({
18
+ id = "",
19
+ owner = "",
20
+ surfaces = ["*"],
21
+ defaultPlacement = false,
22
+ outlet = "",
23
+ linkRenderer = ""
24
+ } = {}) {
25
+ const ownerLine = owner ? ` owner: "${owner}",\n` : "";
26
+ const defaultLine = defaultPlacement ? " default: true,\n" : "";
27
+ const rendererLines = linkRenderer
28
+ ? `,
29
+ renderers: {
30
+ link: "${linkRenderer}"
31
+ }`
32
+ : "";
33
+ return ` {
34
+ id: "${id}",
35
+ ${ownerLine} surfaces: ${JSON.stringify(surfaces)},
36
+ ${defaultLine} variants: {
37
+ compact: {
38
+ outlet: "${outlet}"${rendererLines}
39
+ },
40
+ medium: {
41
+ outlet: "${outlet}"${rendererLines}
42
+ },
43
+ expanded: {
44
+ outlet: "${outlet}"${rendererLines}
45
+ }
46
+ }
47
+ }`;
48
+ }
49
+
17
50
  async function writeAppFixture(appRoot) {
18
51
  await mkdir(path.join(appRoot, "config"), { recursive: true });
19
52
  await mkdir(path.join(appRoot, "src", "components"), { recursive: true });
@@ -43,11 +76,39 @@ async function writeAppFixture(appRoot) {
43
76
  <ShellOutlet
44
77
  target="shell-layout:primary-menu"
45
78
  default
46
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
47
79
  />
48
80
  <ShellOutlet target="shell-layout:top-right" />
49
81
  </div>
50
82
  </template>
83
+ `,
84
+ "utf8"
85
+ );
86
+ await writeFile(
87
+ path.join(appRoot, "src", "placementTopology.js"),
88
+ `export default {
89
+ placements: [
90
+ ${[
91
+ renderTopologyEntry({
92
+ id: "shell.primary-nav",
93
+ surfaces: ["*"],
94
+ defaultPlacement: true,
95
+ outlet: "shell-layout:primary-menu",
96
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
97
+ }),
98
+ renderTopologyEntry({
99
+ id: "shell.status",
100
+ surfaces: ["*"],
101
+ outlet: "shell-layout:top-right"
102
+ }),
103
+ renderTopologyEntry({
104
+ id: "settings.sections",
105
+ owner: "admin-settings",
106
+ surfaces: ["admin"],
107
+ outlet: "admin-settings:forms"
108
+ })
109
+ ].join(",\n")}
110
+ ]
111
+ };
51
112
  `,
52
113
  "utf8"
53
114
  );
@@ -116,7 +177,8 @@ test("ui-generator placed-element subcommand creates component and outlet placem
116
177
 
117
178
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
118
179
  assert.match(placementSource, /id: "ui-generator\.element\.ops-panel"/);
119
- assert.match(placementSource, /target: "shell-layout:top-right"/);
180
+ assert.match(placementSource, /target: "shell\.status"/);
181
+ assert.match(placementSource, /kind: "component"/);
120
182
  assert.match(placementSource, /componentToken: "local\.main\.ui\.element\.ops-panel"/);
121
183
  });
122
184
  });
@@ -131,16 +193,16 @@ test("ui-generator placed-element subcommand supports explicit placement overrid
131
193
  options: {
132
194
  name: "Ops Panel",
133
195
  surface: "admin",
134
- placement: "shell-layout:primary-menu"
196
+ placement: "shell.primary-nav"
135
197
  }
136
198
  });
137
199
 
138
200
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
139
- assert.match(placementSource, /target: "shell-layout:primary-menu"/);
201
+ assert.match(placementSource, /target: "shell\.primary-nav"/);
140
202
  });
141
203
  });
142
204
 
143
- test("ui-generator placed-element infers surface from a page-owned placement target", async () => {
205
+ test("ui-generator placed-element infers surface from an owner-scoped semantic placement", async () => {
144
206
  await withTempApp(async (appRoot) => {
145
207
  await writeAppFixture(appRoot);
146
208
 
@@ -149,12 +211,14 @@ test("ui-generator placed-element infers surface from a page-owned placement tar
149
211
  subcommand: "placed-element",
150
212
  options: {
151
213
  name: "Ops Panel",
152
- placement: "admin-settings:forms"
214
+ placement: "settings.sections",
215
+ owner: "admin-settings"
153
216
  }
154
217
  });
155
218
 
156
219
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
157
- assert.match(placementSource, /target: "admin-settings:forms"/);
220
+ assert.match(placementSource, /target: "settings\.sections"/);
221
+ assert.match(placementSource, /owner: "admin-settings"/);
158
222
  assert.match(placementSource, /surfaces: \["admin"\]/);
159
223
  });
160
224
  });
@@ -172,7 +236,7 @@ test("ui-generator placed-element infers the only enabled surface for shared she
172
236
  });
173
237
 
174
238
  const placementSource = await readFile(path.join(appRoot, "src", "placement.js"), "utf8");
175
- assert.match(placementSource, /target: "shell-layout:top-right"/);
239
+ assert.match(placementSource, /target: "shell\.status"/);
176
240
  assert.match(placementSource, /surfaces: \["admin"\]/);
177
241
  });
178
242
  });
@@ -210,7 +274,7 @@ test("ui-generator placed-element requires explicit surface when a shared shell
210
274
  name: "Ops Panel"
211
275
  }
212
276
  }),
213
- /could not infer a surface for placement target "shell-layout:top-right". Pass --surface explicitly/
277
+ /could not infer a surface for placement target "shell.status". Pass --surface explicitly/
214
278
  );
215
279
  });
216
280
  });
@@ -226,11 +290,12 @@ test("ui-generator placed-element rejects explicit surfaces that conflict with p
226
290
  subcommand: "placed-element",
227
291
  options: {
228
292
  name: "Ops Panel",
229
- placement: "admin-settings:forms",
293
+ placement: "settings.sections",
294
+ owner: "admin-settings",
230
295
  surface: "console"
231
296
  }
232
297
  }),
233
- /target "admin-settings:forms" belongs to surface "admin", so --surface console is invalid/
298
+ /target "settings.sections" is not available on surface "console"/
234
299
  );
235
300
  });
236
301
  });