@jskit-ai/ui-generator 0.1.48 → 0.1.50

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
  );
@@ -97,20 +113,28 @@ test("ui-generator add-subpages derives the default target from an index-route p
97
113
 
98
114
  assert.deepEqual(result.touchedFiles, [
99
115
  "packages/main/src/client/providers/MainClientProvider.js",
100
- "src/components/menus/SurfaceAwareMenuLinkItem.vue",
116
+ "src/components/menus/TabLinkItem.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: \{/);
133
+ assert.match(topologySource, /link: "local\.main\.ui\.tab-link-item"/);
110
134
  assert.match(pageSource, /<RouterView \/>/);
111
135
  assert.equal(
112
- await readFile(path.join(appRoot, "src", "components", "menus", "SurfaceAwareMenuLinkItem.vue"), "utf8"),
113
- await readLocalLinkItemComponentSource("local.main.ui.surface-aware-menu-link-item")
136
+ await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
137
+ await readLocalLinkItemComponentSource("local.main.ui.tab-link-item")
114
138
  );
115
139
  });
116
140
  });
@@ -132,7 +156,7 @@ test("ui-generator add-subpages derives the default target from a dynamic file-r
132
156
  const pageSource = await readPageFile(appRoot, targetFile);
133
157
  assert.match(
134
158
  pageSource,
135
- /<ShellOutlet target="contacts-contact-id:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
159
+ /<ShellOutlet target="contacts-contact-id:sub-pages" \/>/
136
160
  );
137
161
  });
138
162
  });
@@ -154,7 +178,7 @@ test("ui-generator add-subpages derives the default target from a nested route p
154
178
  const pageSource = await readPageFile(appRoot, targetFile);
155
179
  assert.match(
156
180
  pageSource,
157
- /<ShellOutlet target="catalog-products:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
181
+ /<ShellOutlet target="catalog-products:sub-pages" \/>/
158
182
  );
159
183
  });
160
184
  });
@@ -199,11 +223,46 @@ test("ui-generator add-subpages supports explicit target host:position", async (
199
223
  const pageSource = await readPageFile(appRoot, targetFile);
200
224
  assert.match(
201
225
  pageSource,
202
- /<ShellOutlet target="practice-hub:secondary-tabs" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
226
+ /<ShellOutlet target="practice-hub:secondary-tabs" \/>/
203
227
  );
204
228
  });
205
229
  });
206
230
 
231
+ test("ui-generator add-subpages creates script setup instead of adding template imports to normal script", async () => {
232
+ await withTempApp(async (appRoot) => {
233
+ await writeAppFixture(appRoot);
234
+
235
+ const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
236
+ await writePageFile(
237
+ appRoot,
238
+ targetFile,
239
+ `<script>
240
+ export default {
241
+ name: "PracticePage"
242
+ };
243
+ </script>
244
+
245
+ <template>
246
+ <section>Practice</section>
247
+ </template>
248
+ `
249
+ );
250
+
251
+ await runGeneratorSubcommand({
252
+ appRoot,
253
+ subcommand: "add-subpages",
254
+ args: [targetFile],
255
+ options: {}
256
+ });
257
+
258
+ const pageSource = await readPageFile(appRoot, targetFile);
259
+ assert.match(pageSource, /<script setup>\nimport ShellOutlet from "@jskit-ai\/shell-web\/client\/components\/ShellOutlet";/);
260
+ assert.match(pageSource, /import \{ RouterView \} from "vue-router";/);
261
+ assert.match(pageSource, /import SectionContainerShell from "\/src\/components\/SectionContainerShell\.vue";/);
262
+ assert.match(pageSource, /<script>\nexport default/);
263
+ });
264
+ });
265
+
207
266
  test("ui-generator add-subpages does not rewrite existing scaffold support components", async () => {
208
267
  await withTempApp(async (appRoot) => {
209
268
  await writeAppFixture(appRoot);
@@ -211,7 +270,7 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
211
270
  const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
212
271
  await writePageFile(appRoot, targetFile);
213
272
  const customSectionShellSource = `<template><section class="custom-shell"><slot /></section></template>\n`;
214
- const customSurfaceAwareLinkSource = `<template><button class="custom-surface-aware-link"><slot /></button></template>\n`;
273
+ const customTabLinkSource = `<template><button class="custom-tab-link"><slot /></button></template>\n`;
215
274
  await writeFile(
216
275
  path.join(appRoot, "src", "components", "SectionContainerShell.vue"),
217
276
  customSectionShellSource,
@@ -219,8 +278,8 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
219
278
  );
220
279
  await mkdir(path.join(appRoot, "src", "components", "menus"), { recursive: true });
221
280
  await writeFile(
222
- path.join(appRoot, "src", "components", "menus", "SurfaceAwareMenuLinkItem.vue"),
223
- customSurfaceAwareLinkSource,
281
+ path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"),
282
+ customTabLinkSource,
224
283
  "utf8"
225
284
  );
226
285
 
@@ -235,16 +294,105 @@ test("ui-generator add-subpages does not rewrite existing scaffold support compo
235
294
 
236
295
  assert.deepEqual(result.touchedFiles, [
237
296
  "packages/main/src/client/providers/MainClientProvider.js",
238
- `src/pages/${targetFile}`
297
+ `src/pages/${targetFile}`,
298
+ "src/placementTopology.js"
239
299
  ]);
240
300
  assert.equal(
241
301
  await readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
242
302
  customSectionShellSource
243
303
  );
244
304
  assert.equal(
245
- await readFile(path.join(appRoot, "src", "components", "menus", "SurfaceAwareMenuLinkItem.vue"), "utf8"),
246
- customSurfaceAwareLinkSource
305
+ await readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
306
+ customTabLinkSource
307
+ );
308
+ });
309
+ });
310
+
311
+ test("ui-generator add-subpages validates topology before changing page or support files", async () => {
312
+ await withTempApp(async (appRoot) => {
313
+ await writeAppFixture(appRoot);
314
+
315
+ const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
316
+ const originalPageSource = "<template><section>Practice</section></template>\n";
317
+ await writePageFile(appRoot, targetFile, originalPageSource);
318
+ const providerPath = path.join(appRoot, "packages", "main", "src", "client", "providers", "MainClientProvider.js");
319
+ const originalProviderSource = await readFile(providerPath, "utf8");
320
+ await writeFile(
321
+ path.join(appRoot, "src", "placementTopology.js"),
322
+ `export default {
323
+ placements: [
324
+ {
325
+ id: "page.section-nav",
326
+ owner: "practice",
327
+ variants: {}
328
+ }
329
+ ]
330
+ };
331
+ `,
332
+ "utf8"
247
333
  );
334
+
335
+ await assert.rejects(
336
+ runGeneratorSubcommand({
337
+ appRoot,
338
+ subcommand: "add-subpages",
339
+ args: [targetFile],
340
+ options: {}
341
+ }),
342
+ /requires compact topology variant/
343
+ );
344
+
345
+ assert.equal(await readPageFile(appRoot, targetFile), originalPageSource);
346
+ assert.equal(await readFile(providerPath, "utf8"), originalProviderSource);
347
+ await assert.rejects(
348
+ readFile(path.join(appRoot, "src", "components", "SectionContainerShell.vue"), "utf8"),
349
+ /ENOENT/
350
+ );
351
+ await assert.rejects(
352
+ readFile(path.join(appRoot, "src", "components", "menus", "TabLinkItem.vue"), "utf8"),
353
+ /ENOENT/
354
+ );
355
+ });
356
+ });
357
+
358
+ test("ui-generator add-subpages rejects existing section-nav topology for a different outlet", async () => {
359
+ await withTempApp(async (appRoot) => {
360
+ await writeAppFixture(appRoot);
361
+
362
+ const targetFile = "w/[workspaceSlug]/admin/practice/index.vue";
363
+ const originalPageSource = "<template><section>Practice</section></template>\n";
364
+ await writePageFile(appRoot, targetFile, originalPageSource);
365
+ await writeFile(
366
+ path.join(appRoot, "src", "placementTopology.js"),
367
+ `export default {
368
+ placements: [
369
+ {
370
+ id: "page.section-nav",
371
+ owner: "practice",
372
+ surfaces: ["admin"],
373
+ variants: {
374
+ compact: { outlet: "practice:existing-tabs" },
375
+ medium: { outlet: "practice:existing-tabs" },
376
+ expanded: { outlet: "practice:existing-tabs" }
377
+ }
378
+ }
379
+ ]
380
+ };
381
+ `,
382
+ "utf8"
383
+ );
384
+
385
+ await assert.rejects(
386
+ runGeneratorSubcommand({
387
+ appRoot,
388
+ subcommand: "add-subpages",
389
+ args: [targetFile],
390
+ options: {}
391
+ }),
392
+ /semantic placement "page\.section-nav" for owner "practice" already exists with different outlet mapping/
393
+ );
394
+
395
+ assert.equal(await readPageFile(appRoot, targetFile), originalPageSource);
248
396
  });
249
397
  });
250
398
 
@@ -332,7 +480,7 @@ test("ui-generator add-subpages accepts target files with a src/pages prefix", a
332
480
  const pageSource = await readFile(path.join(appRoot, targetFile), "utf8");
333
481
  assert.match(
334
482
  pageSource,
335
- /<ShellOutlet target="practice:sub-pages" default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item" \/>/
483
+ /<ShellOutlet target="practice:sub-pages" \/>/
336
484
  );
337
485
  assert.match(pageSource, /<RouterView \/>/);
338
486
  });
@@ -35,10 +35,77 @@ 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
+ renderTopologyEntry({
95
+ id: "shell.global-actions",
96
+ surfaces: ["*"],
97
+ outlet: "shell-layout:top-right",
98
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
99
+ })
100
+ ];
101
+ await writeFileInApp(
102
+ appRoot,
103
+ "src/placementTopology.js",
104
+ `export default {
105
+ placements: [
106
+ ${[...defaultEntries, ...entries].join(",\n")}
107
+ ]
108
+ };
42
109
  `
43
110
  );
44
111
  }
@@ -61,8 +128,9 @@ test("buildUiPageTemplateContext resolves link placement from default app ShellO
61
128
  targetFile: "admin/reports/index.vue",
62
129
  options: {}
63
130
  });
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");
131
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.primary-nav");
132
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, "");
133
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
66
134
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/reports");
67
135
  assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/reports");
68
136
  assert.equal(context.__JSKIT_UI_LINK_WHEN_LINE__, "");
@@ -116,15 +184,40 @@ test("buildUiPageTemplateContext supports explicit link placement override", asy
116
184
  appRoot,
117
185
  targetFile: "admin/reports/index.vue",
118
186
  options: {
119
- "link-placement": "shell-layout:top-right"
187
+ "link-placement": "shell.status"
188
+ }
189
+ });
190
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
191
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
192
+ });
193
+ });
194
+
195
+ test("buildUiPageTemplateContext maps utility navigation role to global actions", async () => {
196
+ await withTempApp(async (appRoot) => {
197
+ await writeConfig(
198
+ appRoot,
199
+ `export const config = {
200
+ surfaceDefinitions: {
201
+ admin: { id: "admin", pagesRoot: "admin", enabled: true }
202
+ }
203
+ };
204
+ `
205
+ );
206
+ await writeShellLayout(appRoot);
207
+
208
+ const context = await buildUiPageTemplateContext({
209
+ appRoot,
210
+ targetFile: "admin/help/index.vue",
211
+ options: {
212
+ "navigation-role": "utility"
120
213
  }
121
214
  });
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");
215
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.global-actions");
216
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
124
217
  });
125
218
  });
126
219
 
127
- test("buildUiPageTemplateContext supports explicit package outlet link placement", async () => {
220
+ test("buildUiPageTemplateContext supports explicit package semantic link placement", async () => {
128
221
  await withTempApp(async (appRoot) => {
129
222
  await writeConfig(
130
223
  appRoot,
@@ -136,6 +229,22 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
136
229
  `
137
230
  );
138
231
  await writeShellLayout(appRoot);
232
+ await writePlacementTopology(appRoot, [
233
+ renderTopologyEntry({
234
+ id: "page.section-nav",
235
+ owner: "catalog",
236
+ surfaces: ["admin"],
237
+ outlet: "catalog:sub-pages",
238
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
239
+ }),
240
+ renderTopologyEntry({
241
+ id: "page.section-nav",
242
+ owner: "catalog-products",
243
+ surfaces: ["admin"],
244
+ outlet: "catalog-products:sub-pages",
245
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
246
+ })
247
+ ]);
139
248
  await writeFileInApp(
140
249
  appRoot,
141
250
  ".jskit/lock.json",
@@ -164,13 +273,34 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
164
273
  metadata: {
165
274
  ui: {
166
275
  placements: {
167
- outlets: [
276
+ topology: {
277
+ placements: [
168
278
  {
169
- target: "admin-cog:primary-menu",
170
- defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
171
- source: "src/client/components/UsersWorkspaceToolsWidget.vue"
279
+ id: "admin.tools-menu",
280
+ surfaces: ["admin"],
281
+ variants: {
282
+ compact: {
283
+ outlet: "admin-cog:primary-menu",
284
+ renderers: {
285
+ link: "local.main.ui.surface-aware-menu-link-item"
286
+ }
287
+ },
288
+ medium: {
289
+ outlet: "admin-cog:primary-menu",
290
+ renderers: {
291
+ link: "local.main.ui.surface-aware-menu-link-item"
292
+ }
293
+ },
294
+ expanded: {
295
+ outlet: "admin-cog:primary-menu",
296
+ renderers: {
297
+ link: "local.main.ui.surface-aware-menu-link-item"
298
+ }
299
+ }
300
+ }
172
301
  }
173
302
  ]
303
+ }
174
304
  }
175
305
  }
176
306
  }
@@ -182,10 +312,10 @@ test("buildUiPageTemplateContext supports explicit package outlet link placement
182
312
  appRoot,
183
313
  targetFile: "admin/reports/index.vue",
184
314
  options: {
185
- "link-placement": "admin-cog:primary-menu"
315
+ "link-placement": "admin.tools-menu"
186
316
  }
187
317
  });
188
- assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin-cog:primary-menu");
318
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "admin.tools-menu");
189
319
  });
190
320
  });
191
321
 
@@ -200,15 +330,21 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
200
330
  };
201
331
  `
202
332
  );
333
+ await writePlacementTopology(appRoot, [
334
+ renderTopologyEntry({
335
+ id: "page.section-nav",
336
+ owner: "home-settings",
337
+ surfaces: ["home"],
338
+ outlet: "home-settings:primary-menu",
339
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
340
+ })
341
+ ]);
203
342
  await writeFileInApp(
204
343
  appRoot,
205
344
  "src/pages/home/settings.vue",
206
345
  `<template>
207
346
  <section>
208
- <ShellOutlet
209
- target="home-settings:primary-menu"
210
- default-link-component-token="local.main.ui.surface-aware-menu-link-item"
211
- />
347
+ <ShellOutlet target="home-settings:primary-menu" />
212
348
  <RouterView />
213
349
  </section>
214
350
  </template>
@@ -221,13 +357,14 @@ test("buildUiPageTemplateContext suppresses inferred relative link-to for surfac
221
357
  options: {}
222
358
  });
223
359
 
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");
360
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
361
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"home-settings\",\n");
362
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
226
363
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, "");
227
364
  });
228
365
  });
229
366
 
230
- test("buildUiPageTemplateContext supports explicit link component token and link-to", async () => {
367
+ test("buildUiPageTemplateContext supports explicit semantic placement and link-to", async () => {
231
368
  await withTempApp(async (appRoot) => {
232
369
  await writeConfig(
233
370
  appRoot,
@@ -244,12 +381,12 @@ test("buildUiPageTemplateContext supports explicit link component token and link
244
381
  appRoot,
245
382
  targetFile: "admin/contacts/[contactId]/index/notes/index.vue",
246
383
  options: {
247
- "link-placement": "shell-layout:top-right",
248
- "link-component-token": "local.main.ui.tab-link-item",
384
+ "link-placement": "shell.status",
249
385
  "link-to": "./notes"
250
386
  }
251
387
  });
252
- assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "local.main.ui.tab-link-item");
388
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "shell.status");
389
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
253
390
  assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
254
391
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
255
392
  assert.equal(context.__JSKIT_UI_LINK_NON_WORKSPACE_SUFFIX__, "/contacts/[contactId]/notes");
@@ -294,6 +431,15 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
294
431
  `
295
432
  );
296
433
  await writeShellLayout(appRoot);
434
+ await writePlacementTopology(appRoot, [
435
+ renderTopologyEntry({
436
+ id: "page.section-nav",
437
+ owner: "contact-view",
438
+ surfaces: ["admin"],
439
+ outlet: "contact-view:sub-pages",
440
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
441
+ })
442
+ ]);
297
443
  await writeFileInApp(
298
444
  appRoot,
299
445
  "src/pages/admin/contacts/[contactId].vue",
@@ -314,8 +460,9 @@ test("buildUiPageTemplateContext infers subpage link placement, tab token, and l
314
460
  options: {}
315
461
  });
316
462
 
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");
463
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
464
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"contact-view\",\n");
465
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
319
466
  assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
320
467
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes\",\n");
321
468
  });
@@ -333,6 +480,15 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
333
480
  `
334
481
  );
335
482
  await writeShellLayout(appRoot);
483
+ await writePlacementTopology(appRoot, [
484
+ renderTopologyEntry({
485
+ id: "page.section-nav",
486
+ owner: "contact-view",
487
+ surfaces: ["admin"],
488
+ outlet: "contact-view:sub-pages",
489
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
490
+ })
491
+ ]);
336
492
  await writeFileInApp(
337
493
  appRoot,
338
494
  "src/pages/admin/contacts/[contactId].vue",
@@ -353,8 +509,8 @@ test("buildUiPageTemplateContext inherits a file-route parent host for deeper de
353
509
  options: {}
354
510
  });
355
511
 
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");
512
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
513
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
358
514
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./notes/history\",\n");
359
515
  });
360
516
  });
@@ -371,6 +527,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
371
527
  `
372
528
  );
373
529
  await writeShellLayout(appRoot);
530
+ await writePlacementTopology(appRoot, [
531
+ renderTopologyEntry({
532
+ id: "page.section-nav",
533
+ owner: "catalog",
534
+ surfaces: ["admin"],
535
+ outlet: "catalog:sub-pages",
536
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
537
+ })
538
+ ]);
374
539
  await writeFileInApp(
375
540
  appRoot,
376
541
  "src/pages/admin/catalog/index.vue",
@@ -391,8 +556,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index-rou
391
556
  options: {}
392
557
  });
393
558
 
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");
559
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
560
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog\",\n");
561
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
396
562
  assert.equal(context.__JSKIT_UI_LINK_ICON__, "mdi-view-list-outline");
397
563
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./products\",\n");
398
564
  });
@@ -410,6 +576,22 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
410
576
  `
411
577
  );
412
578
  await writeShellLayout(appRoot);
579
+ await writePlacementTopology(appRoot, [
580
+ renderTopologyEntry({
581
+ id: "page.section-nav",
582
+ owner: "catalog",
583
+ surfaces: ["admin"],
584
+ outlet: "catalog:sub-pages",
585
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
586
+ }),
587
+ renderTopologyEntry({
588
+ id: "page.section-nav",
589
+ owner: "catalog-products",
590
+ surfaces: ["admin"],
591
+ outlet: "catalog-products:sub-pages",
592
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
593
+ })
594
+ ]);
413
595
  await writeFileInApp(
414
596
  appRoot,
415
597
  "src/pages/admin/catalog/index.vue",
@@ -443,8 +625,9 @@ test("buildUiPageTemplateContext finds the nearest index-route parent host", asy
443
625
  options: {}
444
626
  });
445
627
 
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");
628
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
629
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"catalog-products\",\n");
630
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
448
631
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./variants\",\n");
449
632
  });
450
633
  });
@@ -461,6 +644,15 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
461
644
  `
462
645
  );
463
646
  await writeShellLayout(appRoot);
647
+ await writePlacementTopology(appRoot, [
648
+ renderTopologyEntry({
649
+ id: "page.section-nav",
650
+ owner: "customer-view",
651
+ surfaces: ["admin"],
652
+ outlet: "customer-view:sub-pages",
653
+ linkRenderer: "local.main.ui.surface-aware-menu-link-item"
654
+ })
655
+ ]);
464
656
  await writeFileInApp(
465
657
  appRoot,
466
658
  "src/pages/admin/customers/[customerId]/index.vue",
@@ -481,8 +673,9 @@ test("buildUiPageTemplateContext infers subpage link placement from an index rou
481
673
  options: {}
482
674
  });
483
675
 
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");
676
+ assert.equal(context.__JSKIT_UI_LINK_PLACEMENT_TARGET__, "page.section-nav");
677
+ assert.equal(context.__JSKIT_UI_LINK_OWNER_LINE__, " owner: \"customer-view\",\n");
678
+ assert.equal(context.__JSKIT_UI_LINK_COMPONENT_TOKEN__, "");
486
679
  assert.equal(context.__JSKIT_UI_LINK_TO_PROP_LINE__, " to: \"./pets\",\n");
487
680
  assert.equal(context.__JSKIT_UI_LINK_WORKSPACE_SUFFIX__, "/customers/[customerId]/pets");
488
681
  });
@@ -641,7 +834,7 @@ test("buildUiPageTemplateContext validates link placement format", async () => {
641
834
  "link-placement": "invalid-placement"
642
835
  }
643
836
  }),
644
- /option "placement" must be a target in "host:position" format/
837
+ /option "placement" must be a semantic target in "area.slot" format/
645
838
  );
646
839
  });
647
840
  });