@jskit-ai/ui-generator 0.1.47 → 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.
@@ -1,7 +1,7 @@
1
1
  export default Object.freeze({
2
2
  packageVersion: 1,
3
3
  packageId: "@jskit-ai/ui-generator",
4
- version: "0.1.47",
4
+ version: "0.1.49",
5
5
  kind: "generator",
6
6
  description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
7
7
  options: {
@@ -40,22 +40,35 @@ export default Object.freeze({
40
40
  inputType: "text",
41
41
  defaultValue: "",
42
42
  promptLabel: "Placement target",
43
- promptHint: "Optional target for placed-element placement (format: host:position, default: shell-layout:top-right)."
43
+ promptHint: "Semantic placement target for placed-element and outlet mapping (format: area.slot, default for placed-element: shell.status)."
44
44
  },
45
- "link-placement": {
45
+ owner: {
46
46
  required: false,
47
47
  inputType: "text",
48
48
  defaultValue: "",
49
- promptLabel: "Link placement",
50
- promptHint: "Optional target for the generated page link placement (format: host:position)."
49
+ promptLabel: "Placement owner",
50
+ promptHint: "Optional owner id for semantic topology mappings. Page/settings placements default to the outlet host."
51
51
  },
52
- "link-component-token": {
52
+ description: {
53
53
  required: false,
54
54
  inputType: "text",
55
55
  defaultValue: "",
56
- promptLabel: "Link component token",
57
- promptHint:
58
- "Optional component token override for the generated page link placement (example: local.main.ui.tab-link-item)."
56
+ promptLabel: "Description",
57
+ promptHint: "Optional description for generated semantic topology mappings."
58
+ },
59
+ "link-renderer": {
60
+ required: false,
61
+ inputType: "text",
62
+ defaultValue: "local.main.ui.surface-aware-menu-link-item",
63
+ promptLabel: "Link renderer",
64
+ promptHint: "Default link renderer token for generated topology variants."
65
+ },
66
+ "link-placement": {
67
+ required: false,
68
+ inputType: "text",
69
+ defaultValue: "",
70
+ promptLabel: "Link placement",
71
+ promptHint: "Optional semantic target for the generated page link placement (format: area.slot)."
59
72
  },
60
73
  "link-to": {
61
74
  required: false,
@@ -120,9 +133,9 @@ export default Object.freeze({
120
133
  descriptionKey: "page-target-file"
121
134
  }
122
135
  ],
123
- optionNames: ["name", "link-placement", "link-component-token", "link-to", "force"],
136
+ optionNames: ["name", "link-placement", "link-to", "force"],
124
137
  notes: [
125
- "If a nearest parent subpages target is found, placement, link component token, and props.to are inferred automatically.",
138
+ "If a nearest parent subpages target is found, semantic placement and props.to are inferred automatically.",
126
139
  "If the parent target page is index.vue, child pages belong under index/...",
127
140
  "If the target page file already exists, rerun with --force to overwrite it."
128
141
  ],
@@ -151,10 +164,10 @@ export default Object.freeze({
151
164
  entrypoint: "src/server/subcommands/element.js",
152
165
  export: "runGeneratorSubcommand",
153
166
  description: "Create a Vue component file under the chosen component directory (default: src/components) and add a placement entry that renders it.",
154
- optionNames: ["name", "surface", "path", "placement", "force"],
167
+ optionNames: ["name", "surface", "path", "placement", "owner", "force"],
155
168
  requiredOptionNames: ["name"],
156
169
  notes: [
157
- "If --placement is omitted, the placed element is added at shell-layout:top-right.",
170
+ "If --placement is omitted, the placed element is added at shell.status.",
158
171
  "If the placement target belongs to a page-owned outlet, JSKIT infers the surface automatically.",
159
172
  "If the target is shared and the app has multiple enabled surfaces, pass --surface explicitly.",
160
173
  "If the component file already exists, rerun with --force to overwrite it."
@@ -174,7 +187,7 @@ export default Object.freeze({
174
187
  " --name \"Ops Panel\" \\",
175
188
  " --surface admin \\",
176
189
  " --path src/widgets \\",
177
- " --placement shell-layout:top-right \\",
190
+ " --placement shell.status \\",
178
191
  " --force"
179
192
  ]
180
193
  }
@@ -226,9 +239,9 @@ export default Object.freeze({
226
239
  export: "runGeneratorSubcommand",
227
240
  description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
228
241
  longDescription: [
229
- "A ShellOutlet creates a named placement target inside a Vue file. That target is what other parts of JSKIT render into later.",
230
- "After an outlet exists, `jskit list-placements` will discover it and show its `target`. That makes the outlet visible to humans and to generators that need a placement destination.",
231
- "Commands that create placed UI, such as `ui-generator placed-element`, and commands that add page links can then target that outlet by writing placement entries that point at the same target."
242
+ "A ShellOutlet creates a concrete placement recipient inside a Vue file.",
243
+ "The command also appends the semantic topology mapping for that outlet, so `jskit list-placements` exposes the public `area.slot` placement instead of leaving the concrete recipient unused.",
244
+ "Generated topology includes compact, medium, and expanded variants. By default all three point at the inserted outlet; hand-edit `src/placementTopology.js` if the adaptive layout uses different concrete outlets."
232
245
  ],
233
246
  positionalArgs: [
234
247
  {
@@ -237,10 +250,11 @@ export default Object.freeze({
237
250
  descriptionKey: "existing-vue-sfc-target-file"
238
251
  }
239
252
  ],
240
- optionNames: ["target"],
241
- requiredOptionNames: ["target"],
253
+ optionNames: ["target", "placement", "owner", "surface", "description", "link-renderer"],
254
+ requiredOptionNames: ["target", "placement"],
242
255
  notes: [
243
- "Use --target host:position."
256
+ "Use --target host:position for the concrete outlet and --placement area.slot for the public semantic placement.",
257
+ "The generated topology maps compact, medium, and expanded to the new concrete outlet."
244
258
  ],
245
259
  examples: [
246
260
  {
@@ -248,7 +262,8 @@ export default Object.freeze({
248
262
  lines: [
249
263
  "npx jskit generate ui-generator outlet \\",
250
264
  " src/components/ContactSummaryCard.vue \\",
251
- " --target contact-view:sub-pages"
265
+ " --target contact-view:sub-pages \\",
266
+ " --placement page.section-nav"
252
267
  ]
253
268
  },
254
269
  {
@@ -256,7 +271,9 @@ export default Object.freeze({
256
271
  lines: [
257
272
  "npx jskit generate ui-generator outlet \\",
258
273
  " src/pages/admin/customers/[customerId]/index.vue \\",
259
- " --target customer-view:summary-actions"
274
+ " --target customer-view:summary-actions \\",
275
+ " --placement page.actions \\",
276
+ " --surface admin"
260
277
  ]
261
278
  }
262
279
  ]
@@ -278,7 +295,7 @@ export default Object.freeze({
278
295
  mutations: {
279
296
  dependencies: {
280
297
  runtime: {
281
- "@jskit-ai/users-web": "0.1.79"
298
+ "@jskit-ai/users-web": "0.1.81"
282
299
  },
283
300
  dev: {}
284
301
  },
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/ui-generator",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/kernel": "0.1.64",
10
- "@jskit-ai/shell-web": "0.1.63"
9
+ "@jskit-ai/kernel": "0.1.66",
10
+ "@jskit-ai/shell-web": "0.1.65"
11
11
  },
12
12
  "exports": {
13
13
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -12,6 +12,13 @@ function resolveLinkToPropLine(linkTo = "") {
12
12
  return ` to: ${JSON.stringify(linkTo)},\n`;
13
13
  }
14
14
 
15
+ function resolveOwnerLine(owner = "") {
16
+ if (!owner) {
17
+ return "";
18
+ }
19
+ return ` owner: ${JSON.stringify(owner)},\n`;
20
+ }
21
+
15
22
  async function buildUiPageTemplateContext({
16
23
  appRoot,
17
24
  targetFile = "",
@@ -28,13 +35,13 @@ async function buildUiPageTemplateContext({
28
35
  targetFile,
29
36
  context: "ui-generator page",
30
37
  placement: options?.["link-placement"],
31
- componentToken: options?.["link-component-token"],
32
38
  linkTo: options?.["link-to"]
33
39
  });
34
40
 
35
41
  return {
36
42
  __JSKIT_UI_LINK_PLACEMENT_ID__: pageTarget.placementId,
37
43
  __JSKIT_UI_LINK_PLACEMENT_TARGET__: String(linkTarget.placementTarget?.id || ""),
44
+ __JSKIT_UI_LINK_OWNER_LINE__: resolveOwnerLine(linkTarget.placementTarget?.owner || ""),
38
45
  __JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
39
46
  __JSKIT_UI_LINK_ICON__: DEFAULT_GENERATED_LINK_ICON,
40
47
  __JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
@@ -1,3 +1,4 @@
1
+ import { readFile, writeFile } from "node:fs/promises";
1
2
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
2
3
  import {
3
4
  DEFAULT_COMPONENT_DIRECTORY,
@@ -7,11 +8,60 @@ import {
7
8
  upgradePageFileToSubpages
8
9
  } from "./pageSupport.js";
9
10
  import {
11
+ PLACEMENT_TOPOLOGY_FILE,
12
+ appendBlockIfMarkerMissing,
10
13
  requireSinglePositionalTargetFile,
14
+ resolvePathWithinApp,
11
15
  resolveOutletTargetId,
12
16
  rejectUnexpectedOptions
13
17
  } from "./support.js";
14
18
 
19
+ function resolveOutletOwner(target = "") {
20
+ const normalizedTarget = normalizeText(target);
21
+ const separatorIndex = normalizedTarget.indexOf(":");
22
+ if (separatorIndex <= 0) {
23
+ return "";
24
+ }
25
+ return normalizedTarget.slice(0, separatorIndex);
26
+ }
27
+
28
+ function renderSectionNavTopologyBlock({
29
+ marker = "",
30
+ owner = "",
31
+ surface = "",
32
+ target = ""
33
+ } = {}) {
34
+ return (
35
+ `// ${marker}\n` +
36
+ "addPlacementTopology({\n" +
37
+ ` id: "page.section-nav",\n` +
38
+ ` owner: "${owner}",\n` +
39
+ ` description: "Navigation between child pages in this section.",\n` +
40
+ ` surfaces: ["${surface}"],\n` +
41
+ " variants: {\n" +
42
+ " compact: {\n" +
43
+ ` outlet: "${target}",\n` +
44
+ " renderers: {\n" +
45
+ ` link: "local.main.ui.surface-aware-menu-link-item"\n` +
46
+ " }\n" +
47
+ " },\n" +
48
+ " medium: {\n" +
49
+ ` outlet: "${target}",\n` +
50
+ " renderers: {\n" +
51
+ ` link: "local.main.ui.surface-aware-menu-link-item"\n` +
52
+ " }\n" +
53
+ " },\n" +
54
+ " expanded: {\n" +
55
+ ` outlet: "${target}",\n` +
56
+ " renderers: {\n" +
57
+ ` link: "local.main.ui.surface-aware-menu-link-item"\n` +
58
+ " }\n" +
59
+ " }\n" +
60
+ " }\n" +
61
+ "});\n"
62
+ );
63
+ }
64
+
15
65
  function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
16
66
  const rawTarget = normalizeText(options?.target);
17
67
  const defaultTarget = `${deriveDefaultSubpagesHost(pageTarget)}:${DEFAULT_SUBPAGES_POSITION}`;
@@ -60,10 +110,35 @@ async function runGeneratorSubcommand({
60
110
  dryRun
61
111
  });
62
112
 
113
+ const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
114
+ context: "ui-generator add-subpages"
115
+ });
116
+ const owner = resolveOutletOwner(outletTarget.id);
117
+ const topologyMarker = `jskit:ui-generator.topology:page.section-nav:${owner}`;
118
+ const topologySource = await readFile(topologyPath.absolutePath, "utf8");
119
+ const topologyApplied = appendBlockIfMarkerMissing(
120
+ topologySource,
121
+ topologyMarker,
122
+ renderSectionNavTopologyBlock({
123
+ marker: topologyMarker,
124
+ owner,
125
+ surface: result.surfaceId,
126
+ target: outletTarget.id
127
+ })
128
+ );
129
+ const touchedFiles = new Set(result.touchedFiles);
130
+ if (topologyApplied.changed) {
131
+ if (dryRun !== true) {
132
+ await writeFile(topologyPath.absolutePath, topologyApplied.content, "utf8");
133
+ }
134
+ touchedFiles.add(topologyPath.relativePath);
135
+ }
136
+ const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
137
+
63
138
  return {
64
- touchedFiles: result.touchedFiles,
139
+ touchedFiles: touchedFileList,
65
140
  summary:
66
- result.touchedFiles.length > 0
141
+ touchedFileList.length > 0
67
142
  ? `Enabled subpages in ${result.targetFile} for "${pageTarget.routeUrlSuffix}" using outlet target "${outletTarget.id}".`
68
143
  : `Subpages are already enabled in ${result.targetFile}.`
69
144
  };
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import {
4
4
  listSurfacePageRoots,
5
5
  resolveBestSurfaceMatchFromPageFile,
6
- resolveShellOutletPlacementTargetFromApp,
6
+ resolveSemanticPlacementTargetFromApp,
7
7
  toPosixPath
8
8
  } from "@jskit-ai/kernel/server/support";
9
9
  import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface";
@@ -22,7 +22,7 @@ import {
22
22
  insertBeforeClassDeclaration
23
23
  } from "./support.js";
24
24
 
25
- const DEFAULT_ELEMENT_PLACEMENT = "shell-layout:top-right";
25
+ const DEFAULT_ELEMENT_PLACEMENT = "shell.status";
26
26
 
27
27
  function renderElementComponentSource(elementName = "") {
28
28
  return `<template>
@@ -41,14 +41,28 @@ async function resolvePlacedElementSurface({
41
41
  context = "ui-generator placed-element"
42
42
  } = {}) {
43
43
  const explicitSurface = normalizeSurfaceId(surface);
44
+ const placementSurfaces = Array.isArray(placementTarget?.surfaces)
45
+ ? placementTarget.surfaces.map((entry) => normalizeSurfaceId(entry)).filter(Boolean)
46
+ : [];
47
+ const isPlacementGlobal = placementSurfaces.length < 1 || placementSurfaces.includes("*");
48
+ const concreteSourcePath = normalizeText(placementTarget?.sourcePath).startsWith("src/")
49
+ ? normalizeText(placementTarget?.sourcePath)
50
+ : "";
44
51
  const inferredSurfaceMatch = await resolveBestSurfaceMatchFromPageFile(
45
- String(placementTarget?.sourcePath || ""),
52
+ concreteSourcePath,
46
53
  await listSurfacePageRoots(appRoot, { context }),
47
54
  { context }
48
55
  );
49
- const inferredSurface = normalizeSurfaceId(inferredSurfaceMatch?.surfaceId);
56
+ const inferredSurface =
57
+ normalizeSurfaceId(inferredSurfaceMatch?.surfaceId) ||
58
+ (placementSurfaces.length === 1 && !isPlacementGlobal ? placementSurfaces[0] : "");
50
59
 
51
60
  if (explicitSurface) {
61
+ if (!isPlacementGlobal && placementSurfaces.length > 0 && !placementSurfaces.includes(explicitSurface)) {
62
+ throw new Error(
63
+ `${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" is not available on surface "${explicitSurface}".`
64
+ );
65
+ }
52
66
  if (inferredSurface && explicitSurface !== inferredSurface) {
53
67
  throw new Error(
54
68
  `${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" belongs to surface "${inferredSurface}", ` +
@@ -89,7 +103,7 @@ async function runGeneratorSubcommand({
89
103
  if (Array.isArray(args) && args.length > 0) {
90
104
  throw new Error("ui-generator placed-element does not accept positional arguments.");
91
105
  }
92
- rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "force"], {
106
+ rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "owner", "force"], {
93
107
  context: "ui-generator placed-element"
94
108
  });
95
109
 
@@ -115,10 +129,11 @@ async function runGeneratorSubcommand({
115
129
  context: "ui-generator placed-element"
116
130
  });
117
131
  const componentToken = `local.main.ui.element.${elementNameKebab}`;
118
- const placementTarget = await resolveShellOutletPlacementTargetFromApp({
132
+ const placementTarget = await resolveSemanticPlacementTargetFromApp({
119
133
  appRoot,
120
134
  context: "ui-generator",
121
- placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT
135
+ placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT,
136
+ owner: options?.owner
122
137
  });
123
138
  const surface = await resolvePlacedElementSurface({
124
139
  appRoot,
@@ -179,12 +194,15 @@ async function runGeneratorSubcommand({
179
194
 
180
195
  const placementSource = await readFile(placementPath.absolutePath, "utf8");
181
196
  const placementMarker = `jskit:ui-generator.element:${surface}:${elementNameKebab}`;
197
+ const ownerLine = placementTarget.owner ? ` owner: "${placementTarget.owner}",\n` : "";
182
198
  const placementBlock =
183
199
  `// ${placementMarker}\n` +
184
200
  "{\n" +
185
201
  " addPlacement({\n" +
186
202
  ` id: "ui-generator.element.${elementNameKebab}",\n` +
187
203
  ` target: "${placementTarget.id}",\n` +
204
+ ownerLine +
205
+ ` kind: "component",\n` +
188
206
  ` surfaces: ["${surface}"],\n` +
189
207
  " order: 155,\n" +
190
208
  ` componentToken: "${componentToken}"\n` +
@@ -1,6 +1,13 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import {
4
+ normalizePlacementOwnerId,
5
+ normalizePlacementSurfaceId,
6
+ normalizeSemanticPlacementId
7
+ } from "@jskit-ai/kernel/shared/support/shellLayoutTargets";
8
+ import {
9
+ PLACEMENT_TOPOLOGY_FILE,
10
+ appendBlockIfMarkerMissing,
4
11
  requireSinglePositionalTargetFile,
5
12
  rejectUnexpectedOptions,
6
13
  resolveOutletTargetId,
@@ -75,6 +82,96 @@ function createOutletBlock({ target = "" } = {}) {
75
82
  return `<ShellOutlet target="${target}" />`;
76
83
  }
77
84
 
85
+ function resolveOutletOwner(target = "") {
86
+ const normalizedTarget = normalizeText(target);
87
+ const separatorIndex = normalizedTarget.indexOf(":");
88
+ if (separatorIndex <= 0) {
89
+ return "";
90
+ }
91
+ return normalizedTarget.slice(0, separatorIndex);
92
+ }
93
+
94
+ function resolveSemanticPlacementOption(options = {}) {
95
+ const placementId = normalizeSemanticPlacementId(options?.placement);
96
+ if (!placementId) {
97
+ throw new Error('ui-generator outlet requires --placement in semantic "area.slot" format.');
98
+ }
99
+ return placementId;
100
+ }
101
+
102
+ function resolveSemanticPlacementOwner({ placementId = "", targetId = "", owner = "" } = {}) {
103
+ const explicitOwner = normalizePlacementOwnerId(owner);
104
+ if (explicitOwner) {
105
+ return explicitOwner;
106
+ }
107
+ if (placementId.startsWith("page.") || placementId.startsWith("settings.")) {
108
+ return resolveOutletOwner(targetId);
109
+ }
110
+ return "";
111
+ }
112
+
113
+ function resolveTopologySurfaces(options = {}) {
114
+ const surface = normalizePlacementSurfaceId(options?.surface);
115
+ if (surface) {
116
+ return [surface];
117
+ }
118
+ return ["*"];
119
+ }
120
+
121
+ function renderTopologyOwnerLine(owner = "") {
122
+ if (!owner) {
123
+ return "";
124
+ }
125
+ return ` owner: "${owner}",\n`;
126
+ }
127
+
128
+ function renderLinkRendererBlock(rendererToken = "") {
129
+ const normalizedRendererToken = normalizeText(rendererToken) || "local.main.ui.surface-aware-menu-link-item";
130
+ return (
131
+ " renderers: {\n" +
132
+ ` link: "${normalizedRendererToken}"\n` +
133
+ " }\n"
134
+ );
135
+ }
136
+
137
+ function renderOutletTopologyBlock({
138
+ marker = "",
139
+ placementId = "",
140
+ owner = "",
141
+ surfaces = ["*"],
142
+ description = "",
143
+ target = "",
144
+ rendererToken = ""
145
+ } = {}) {
146
+ const descriptionLine = normalizeText(description)
147
+ ? ` description: ${JSON.stringify(normalizeText(description))},\n`
148
+ : "";
149
+ const rendererBlock = renderLinkRendererBlock(rendererToken);
150
+ return (
151
+ `// ${marker}\n` +
152
+ "addPlacementTopology({\n" +
153
+ ` id: "${placementId}",\n` +
154
+ renderTopologyOwnerLine(owner) +
155
+ descriptionLine +
156
+ ` surfaces: ${JSON.stringify(surfaces)},\n` +
157
+ " variants: {\n" +
158
+ " compact: {\n" +
159
+ ` outlet: "${target}",\n` +
160
+ rendererBlock +
161
+ " },\n" +
162
+ " medium: {\n" +
163
+ ` outlet: "${target}",\n` +
164
+ rendererBlock +
165
+ " },\n" +
166
+ " expanded: {\n" +
167
+ ` outlet: "${target}",\n` +
168
+ rendererBlock +
169
+ " }\n" +
170
+ " }\n" +
171
+ "});\n"
172
+ );
173
+ }
174
+
78
175
  function findLastTemplateCloseTag(source = "") {
79
176
  const sourceText = String(source || "");
80
177
  let lastMatch = null;
@@ -123,7 +220,7 @@ async function runGeneratorSubcommand({
123
220
  throw new Error(`Unsupported ui-generator subcommand: ${normalizedSubcommand || "<empty>"}.`);
124
221
  }
125
222
  const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator outlet" });
126
- rejectUnexpectedOptions(options, ["target"], {
223
+ rejectUnexpectedOptions(options, ["target", "placement", "owner", "surface", "description", "link-renderer"], {
127
224
  context: "ui-generator outlet"
128
225
  });
129
226
 
@@ -132,6 +229,13 @@ async function runGeneratorSubcommand({
132
229
  optionName: "target"
133
230
  });
134
231
  const targetId = outletTarget.id;
232
+ const placementId = resolveSemanticPlacementOption(options);
233
+ const owner = resolveSemanticPlacementOwner({
234
+ placementId,
235
+ targetId,
236
+ owner: options?.owner
237
+ });
238
+ const surfaces = resolveTopologySurfaces(options);
135
239
 
136
240
  const targetFilePath = resolvePathWithinApp(appRoot, targetFile, {
137
241
  context: "ui-generator outlet"
@@ -162,11 +266,41 @@ async function runGeneratorSubcommand({
162
266
  await writeFile(targetFilePath.absolutePath, scriptApplied.content, "utf8");
163
267
  }
164
268
 
269
+ const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
270
+ context: "ui-generator outlet"
271
+ });
272
+ const topologyMarker = `jskit:ui-generator.topology:${placementId}:${owner || "global"}`;
273
+ const topologySource = await readFile(topologyPath.absolutePath, "utf8");
274
+ const topologyApplied = appendBlockIfMarkerMissing(
275
+ topologySource,
276
+ topologyMarker,
277
+ renderOutletTopologyBlock({
278
+ marker: topologyMarker,
279
+ placementId,
280
+ owner,
281
+ surfaces,
282
+ description: options?.description,
283
+ target: targetId,
284
+ rendererToken: options?.["link-renderer"]
285
+ })
286
+ );
287
+ if (topologyApplied.changed && dryRun !== true) {
288
+ await writeFile(topologyPath.absolutePath, topologyApplied.content, "utf8");
289
+ }
290
+
291
+ const touchedFiles = new Set();
292
+ if (changed) {
293
+ touchedFiles.add(targetFilePath.relativePath);
294
+ }
295
+ if (topologyApplied.changed) {
296
+ touchedFiles.add(topologyPath.relativePath);
297
+ }
298
+
165
299
  return {
166
- touchedFiles: changed ? [targetFilePath.relativePath] : [],
167
- summary: changed
168
- ? `Injected outlet "${targetId}" into ${targetFilePath.relativePath}.`
169
- : `Outlet "${targetId}" is already present in ${targetFilePath.relativePath}.`
300
+ touchedFiles: [...touchedFiles].sort((left, right) => left.localeCompare(right)),
301
+ summary: touchedFiles.size > 0
302
+ ? `Injected outlet "${targetId}" and mapped semantic placement "${placementId}"${owner ? ` for owner "${owner}"` : ""}.`
303
+ : `Outlet "${targetId}" and semantic placement "${placementId}" are already present.`
170
304
  };
171
305
  }
172
306
 
@@ -20,15 +20,20 @@ function renderPageLinkPlacementBlock({
20
20
  label = "",
21
21
  surface = ""
22
22
  } = {}) {
23
+ const componentTokenLine = context.__JSKIT_UI_LINK_COMPONENT_TOKEN__
24
+ ? ` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n`
25
+ : "";
23
26
  return (
24
27
  `// ${marker}\n` +
25
28
  "{\n" +
26
29
  " addPlacement({\n" +
27
30
  ` id: "${context.__JSKIT_UI_LINK_PLACEMENT_ID__}",\n` +
28
31
  ` target: "${context.__JSKIT_UI_LINK_PLACEMENT_TARGET__}",\n` +
32
+ `${context.__JSKIT_UI_LINK_OWNER_LINE__}` +
33
+ ` kind: "link",\n` +
29
34
  ` surfaces: ["${surface}"],\n` +
30
35
  " order: 155,\n" +
31
- ` componentToken: "${context.__JSKIT_UI_LINK_COMPONENT_TOKEN__}",\n` +
36
+ componentTokenLine +
32
37
  " props: {\n" +
33
38
  ` label: "${label}",\n` +
34
39
  ` icon: "${context.__JSKIT_UI_LINK_ICON__}",\n` +
@@ -56,7 +61,7 @@ async function runGeneratorSubcommand({
56
61
  const targetFile = requireSinglePositionalTargetFile(args, { context: "ui-generator page" });
57
62
  rejectUnexpectedOptions(
58
63
  options,
59
- ["name", "link-placement", "link-component-token", "link-to", "force"],
64
+ ["name", "link-placement", "link-to", "force"],
60
65
  { context: "ui-generator page" }
61
66
  );
62
67
 
@@ -313,7 +313,7 @@ function renderSubpagesTemplate({
313
313
  "<template>",
314
314
  renderSectionContainerOpenTag({ title, subtitle }),
315
315
  " <template #tabs>",
316
- ` <ShellOutlet target="${normalizedTarget}" default-link-component-token="${SUBPAGES_LINK_COMPONENT_TOKEN}" />`,
316
+ ` <ShellOutlet target="${normalizedTarget}" />`,
317
317
  " </template>"
318
318
  ];
319
319
 
@@ -10,6 +10,7 @@ import { toCamelCase, toSnakeCase } from "@jskit-ai/kernel/shared/support/string
10
10
  const DEFAULT_COMPONENT_DIRECTORY = "src/components";
11
11
  const MAIN_CLIENT_PROVIDER_FILE = "packages/main/src/client/providers/MainClientProvider.js";
12
12
  const PLACEMENT_FILE = "src/placement.js";
13
+ const PLACEMENT_TOPOLOGY_FILE = "src/placementTopology.js";
13
14
  const SCRIPT_TAG_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
14
15
  const SCRIPT_SETUP_ATTRIBUTE_PATTERN = /\bsetup\b/i;
15
16
  const ATTRIBUTE_PATTERN = /([:@]?[A-Za-z_][A-Za-z0-9_-]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'))?/g;
@@ -266,6 +267,7 @@ export {
266
267
  DEFAULT_COMPONENT_DIRECTORY,
267
268
  MAIN_CLIENT_PROVIDER_FILE,
268
269
  PLACEMENT_FILE,
270
+ PLACEMENT_TOPOLOGY_FILE,
269
271
  toKebabCase,
270
272
  toPascalCase,
271
273
  requireOption,