@jskit-ai/ui-generator 0.1.49 → 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.
@@ -1,7 +1,9 @@
1
+ import { GENERATED_UI_NAVIGATION_ROLE_OPTION } from "@jskit-ai/kernel/shared/support/generatedUiContract";
2
+
1
3
  export default Object.freeze({
2
4
  packageVersion: 1,
3
5
  packageId: "@jskit-ai/ui-generator",
4
- version: "0.1.49",
6
+ version: "0.1.50",
5
7
  kind: "generator",
6
8
  description: "Create non-CRUD pages, reusable UI elements, and subpage hosts.",
7
9
  options: {
@@ -40,7 +42,16 @@ export default Object.freeze({
40
42
  inputType: "text",
41
43
  defaultValue: "",
42
44
  promptLabel: "Placement target",
43
- promptHint: "Semantic placement target for placed-element and outlet mapping (format: area.slot, default for placed-element: shell.status)."
45
+ promptHint: "Semantic placement target for placed-element, outlet, and topology mapping (format: area.slot, default for placed-element: shell.status)."
46
+ },
47
+ kind: {
48
+ required: false,
49
+ inputType: "text",
50
+ validationType: "enum",
51
+ allowedValues: ["component", "link"],
52
+ defaultValue: "",
53
+ promptLabel: "Placement kind",
54
+ promptHint: "Use component for componentToken-backed entries, or link when topology should provide a link renderer."
44
55
  },
45
56
  owner: {
46
57
  required: false,
@@ -70,6 +81,7 @@ export default Object.freeze({
70
81
  promptLabel: "Link placement",
71
82
  promptHint: "Optional semantic target for the generated page link placement (format: area.slot)."
72
83
  },
84
+ "navigation-role": GENERATED_UI_NAVIGATION_ROLE_OPTION,
73
85
  "link-to": {
74
86
  required: false,
75
87
  inputType: "text",
@@ -83,7 +95,28 @@ export default Object.freeze({
83
95
  inputType: "text",
84
96
  defaultValue: "",
85
97
  promptLabel: "Outlet target",
86
- promptHint: "Used by add-subpages and outlet. Must be a target in host:position format."
98
+ promptHint: "Used by add-subpages, outlet, and topology. Must be a target in host:position format. For topology, maps all layouts unless layout-specific targets are provided."
99
+ },
100
+ "compact-target": {
101
+ required: false,
102
+ inputType: "text",
103
+ defaultValue: "",
104
+ promptLabel: "Compact outlet target",
105
+ promptHint: "Concrete outlet used by topology for compact layouts, in host:position format."
106
+ },
107
+ "medium-target": {
108
+ required: false,
109
+ inputType: "text",
110
+ defaultValue: "",
111
+ promptLabel: "Medium outlet target",
112
+ promptHint: "Concrete outlet used by topology for medium layouts, in host:position format."
113
+ },
114
+ "expanded-target": {
115
+ required: false,
116
+ inputType: "text",
117
+ defaultValue: "",
118
+ promptLabel: "Expanded outlet target",
119
+ promptHint: "Concrete outlet used by topology for expanded layouts, in host:position format."
87
120
  },
88
121
  title: {
89
122
  required: false,
@@ -133,7 +166,7 @@ export default Object.freeze({
133
166
  descriptionKey: "page-target-file"
134
167
  }
135
168
  ],
136
- optionNames: ["name", "link-placement", "link-to", "force"],
169
+ optionNames: ["name", "navigation-role", "link-placement", "link-to", "force"],
137
170
  notes: [
138
171
  "If a nearest parent subpages target is found, semantic placement and props.to are inferred automatically.",
139
172
  "If the parent target page is index.vue, child pages belong under index/...",
@@ -217,7 +250,7 @@ export default Object.freeze({
217
250
  "npx jskit generate ui-generator add-subpages \\",
218
251
  " admin/customers/[customerId]/index.vue \\",
219
252
  " --title \"Customer\" \\",
220
- " --subtitle \"View and manage this customer.\""
253
+ " --subtitle \"Customer profile and activity.\""
221
254
  ]
222
255
  },
223
256
  {
@@ -240,8 +273,8 @@ export default Object.freeze({
240
273
  description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
241
274
  longDescription: [
242
275
  "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."
276
+ "If --placement is provided, the command also appends semantic topology for that outlet, so `jskit list-placements` exposes the public `area.slot` placement.",
277
+ "If --placement is omitted, the command only injects the concrete outlet. Use this before `ui-generator topology` when compact, medium, and expanded should map to different outlets."
245
278
  ],
246
279
  positionalArgs: [
247
280
  {
@@ -250,11 +283,12 @@ export default Object.freeze({
250
283
  descriptionKey: "existing-vue-sfc-target-file"
251
284
  }
252
285
  ],
253
- optionNames: ["target", "placement", "owner", "surface", "description", "link-renderer"],
254
- requiredOptionNames: ["target", "placement"],
286
+ optionNames: ["target", "placement", "owner", "surface", "description", "kind", "link-renderer", "compact-target", "medium-target", "expanded-target"],
287
+ requiredOptionNames: ["target"],
255
288
  notes: [
256
289
  "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."
290
+ "Omit --placement when you only want to add the concrete destination.",
291
+ "When --placement is present, the generated topology maps compact, medium, and expanded to the new concrete outlet unless layout-specific targets are provided."
258
292
  ],
259
293
  examples: [
260
294
  {
@@ -277,6 +311,48 @@ export default Object.freeze({
277
311
  ]
278
312
  }
279
313
  ]
314
+ },
315
+ topology: {
316
+ requiresShellWeb: true,
317
+ entrypoint: "src/server/subcommands/outlet.js",
318
+ export: "runGeneratorSubcommand",
319
+ description: "Append semantic placement topology that maps compact, medium, and expanded layouts to concrete outlets.",
320
+ longDescription: [
321
+ "Use this after adding one or more concrete ShellOutlet destinations.",
322
+ "The command writes only `src/placementTopology.js`; it does not inject Vue outlets.",
323
+ "Use --kind component for componentToken-backed elements. Use --kind link when topology should provide the standard link renderer."
324
+ ],
325
+ optionNames: ["placement", "kind", "target", "compact-target", "medium-target", "expanded-target", "owner", "surface", "description", "link-renderer"],
326
+ requiredOptionNames: ["placement", "kind"],
327
+ notes: [
328
+ "Use --target host:position when all layout classes should render in the same outlet.",
329
+ "Use --compact-target, --medium-target, and --expanded-target when layout classes should render in different outlets.",
330
+ "For page.* and settings.* placements, pass --owner if the layout targets use different outlet hosts."
331
+ ],
332
+ examples: [
333
+ {
334
+ label: "Component placement with adaptive outlets",
335
+ lines: [
336
+ "npx jskit generate ui-generator topology \\",
337
+ " --placement reports.actions \\",
338
+ " --kind component \\",
339
+ " --compact-target reports:bottom-actions \\",
340
+ " --medium-target reports:toolbar-actions \\",
341
+ " --expanded-target reports:side-actions \\",
342
+ " --surface admin"
343
+ ]
344
+ },
345
+ {
346
+ label: "Link placement using one outlet for every layout",
347
+ lines: [
348
+ "npx jskit generate ui-generator topology \\",
349
+ " --placement page.section-nav \\",
350
+ " --kind link \\",
351
+ " --target report-view:sub-pages \\",
352
+ " --surface admin"
353
+ ]
354
+ }
355
+ ]
280
356
  }
281
357
  },
282
358
  apiSummary: {
@@ -295,7 +371,7 @@ export default Object.freeze({
295
371
  mutations: {
296
372
  dependencies: {
297
373
  runtime: {
298
- "@jskit-ai/users-web": "0.1.81"
374
+ "@jskit-ai/users-web": "0.1.82"
299
375
  },
300
376
  dev: {}
301
377
  },
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@jskit-ai/ui-generator",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
7
7
  },
8
8
  "dependencies": {
9
- "@jskit-ai/kernel": "0.1.66",
10
- "@jskit-ai/shell-web": "0.1.65"
9
+ "@jskit-ai/kernel": "0.1.67",
10
+ "@jskit-ai/shell-web": "0.1.66"
11
11
  },
12
12
  "exports": {
13
13
  "./server/buildTemplateContext": "./src/server/buildTemplateContext.js"
@@ -2,6 +2,11 @@ import {
2
2
  resolvePageLinkTargetDetails,
3
3
  resolvePageTargetDetails
4
4
  } from "@jskit-ai/kernel/server/support";
5
+ import {
6
+ normalizeGeneratedUiNavigationRole,
7
+ resolveGeneratedUiNavigationRoleLinkPlacement,
8
+ shouldCreateGeneratedUiNavigationLink
9
+ } from "@jskit-ai/kernel/shared/support/generatedUiContract";
5
10
 
6
11
  const DEFAULT_GENERATED_LINK_ICON = "mdi-view-list-outline";
7
12
 
@@ -19,6 +24,27 @@ function resolveOwnerLine(owner = "") {
19
24
  return ` owner: ${JSON.stringify(owner)},\n`;
20
25
  }
21
26
 
27
+ function resolveNavigationInferenceRoutePath(pageTarget = {}) {
28
+ const routeSegments = Array.isArray(pageTarget?.visibleRouteSegments)
29
+ ? pageTarget.visibleRouteSegments.map((entry) => String(entry || "").trim()).filter(Boolean)
30
+ : [];
31
+ if (routeSegments.length > 0) {
32
+ return `/${routeSegments.join("/")}`;
33
+ }
34
+ return String(pageTarget?.routeUrlSuffix || "");
35
+ }
36
+
37
+ function shouldCreateNavigationLink(options = {}, inferenceContext = {}) {
38
+ return shouldCreateGeneratedUiNavigationLink(options, {
39
+ allowLinkTo: true,
40
+ routePath: inferenceContext?.routePath
41
+ });
42
+ }
43
+
44
+ function resolveNavigationRoleLinkPlacement(options = {}, inferenceContext = {}) {
45
+ return resolveGeneratedUiNavigationRoleLinkPlacement(options, inferenceContext);
46
+ }
47
+
22
48
  async function buildUiPageTemplateContext({
23
49
  appRoot,
24
50
  targetFile = "",
@@ -34,7 +60,9 @@ async function buildUiPageTemplateContext({
34
60
  pageTarget,
35
61
  targetFile,
36
62
  context: "ui-generator page",
37
- placement: options?.["link-placement"],
63
+ placement: resolveNavigationRoleLinkPlacement(options, {
64
+ routePath: resolveNavigationInferenceRoutePath(pageTarget)
65
+ }),
38
66
  linkTo: options?.["link-to"]
39
67
  });
40
68
 
@@ -51,4 +79,10 @@ async function buildUiPageTemplateContext({
51
79
  };
52
80
  }
53
81
 
54
- export { buildUiPageTemplateContext };
82
+ export {
83
+ buildUiPageTemplateContext,
84
+ normalizeGeneratedUiNavigationRole as normalizeNavigationRole,
85
+ resolveNavigationInferenceRoutePath,
86
+ resolveNavigationRoleLinkPlacement,
87
+ shouldCreateNavigationLink
88
+ };
@@ -3,13 +3,14 @@ import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
3
3
  import {
4
4
  DEFAULT_COMPONENT_DIRECTORY,
5
5
  DEFAULT_SUBPAGES_POSITION,
6
+ SUBPAGES_LINK_COMPONENT_TOKEN,
6
7
  deriveDefaultSubpagesHost,
7
8
  resolvePageTargetDetails,
8
9
  upgradePageFileToSubpages
9
10
  } from "./pageSupport.js";
10
11
  import {
11
12
  PLACEMENT_TOPOLOGY_FILE,
12
- appendBlockIfMarkerMissing,
13
+ appendTopologyBlockIfPlacementMissing,
13
14
  requireSinglePositionalTargetFile,
14
15
  resolvePathWithinApp,
15
16
  resolveOutletTargetId,
@@ -42,19 +43,19 @@ function renderSectionNavTopologyBlock({
42
43
  " compact: {\n" +
43
44
  ` outlet: "${target}",\n` +
44
45
  " renderers: {\n" +
45
- ` link: "local.main.ui.surface-aware-menu-link-item"\n` +
46
+ ` link: "${SUBPAGES_LINK_COMPONENT_TOKEN}"\n` +
46
47
  " }\n" +
47
48
  " },\n" +
48
49
  " medium: {\n" +
49
50
  ` outlet: "${target}",\n` +
50
51
  " renderers: {\n" +
51
- ` link: "local.main.ui.surface-aware-menu-link-item"\n` +
52
+ ` link: "${SUBPAGES_LINK_COMPONENT_TOKEN}"\n` +
52
53
  " }\n" +
53
54
  " },\n" +
54
55
  " expanded: {\n" +
55
56
  ` outlet: "${target}",\n` +
56
57
  " renderers: {\n" +
57
- ` link: "local.main.ui.surface-aware-menu-link-item"\n` +
58
+ ` link: "${SUBPAGES_LINK_COMPONENT_TOKEN}"\n` +
58
59
  " }\n" +
59
60
  " }\n" +
60
61
  " }\n" +
@@ -99,6 +100,33 @@ async function runGeneratorSubcommand({
99
100
  });
100
101
  const outletTarget = resolveSubpagesOutletTarget(options, pageTarget);
101
102
 
103
+ const topologyPath = resolvePathWithinApp(appRoot, PLACEMENT_TOPOLOGY_FILE, {
104
+ context: "ui-generator add-subpages"
105
+ });
106
+ const owner = resolveOutletOwner(outletTarget.id);
107
+ const topologyMarker = `jskit:ui-generator.topology:page.section-nav:${owner}`;
108
+ const topologySource = await readFile(topologyPath.absolutePath, "utf8");
109
+ const expectedVariantTargets = {
110
+ compact: outletTarget.id,
111
+ medium: outletTarget.id,
112
+ expanded: outletTarget.id
113
+ };
114
+ const topologyApplied = await appendTopologyBlockIfPlacementMissing({
115
+ topologyPath,
116
+ source: topologySource,
117
+ marker: topologyMarker,
118
+ block: renderSectionNavTopologyBlock({
119
+ marker: topologyMarker,
120
+ owner,
121
+ surface: pageTarget.surfaceId,
122
+ target: outletTarget.id
123
+ }),
124
+ placementId: "page.section-nav",
125
+ owner,
126
+ variantTargets: expectedVariantTargets,
127
+ context: "ui-generator add-subpages"
128
+ });
129
+
102
130
  const result = await upgradePageFileToSubpages({
103
131
  appRoot,
104
132
  targetFile,
@@ -110,22 +138,6 @@ async function runGeneratorSubcommand({
110
138
  dryRun
111
139
  });
112
140
 
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
141
  const touchedFiles = new Set(result.touchedFiles);
130
142
  if (topologyApplied.changed) {
131
143
  if (dryRun !== true) {
@@ -26,11 +26,25 @@ const DEFAULT_ELEMENT_PLACEMENT = "shell.status";
26
26
 
27
27
  function renderElementComponentSource(elementName = "") {
28
28
  return `<template>
29
- <section class="pa-4">
30
- <h2 class="text-h6 mb-2">${elementName}</h2>
31
- <p class="text-body-2 text-medium-emphasis">Replace this scaffold with your UI element implementation.</p>
29
+ <section class="generated-element-panel">
30
+ <div>
31
+ <p class="text-caption text-medium-emphasis mb-1">Status</p>
32
+ <h2 class="text-subtitle-1 font-weight-medium mb-0">${elementName}</h2>
33
+ </div>
34
+ <v-chip color="primary" variant="tonal" size="small">Ready</v-chip>
32
35
  </section>
33
36
  </template>
37
+
38
+ <style scoped>
39
+ .generated-element-panel {
40
+ align-items: center;
41
+ display: flex;
42
+ gap: 0.75rem;
43
+ justify-content: space-between;
44
+ min-height: 48px;
45
+ min-width: 0;
46
+ }
47
+ </style>
34
48
  `;
35
49
  }
36
50