@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.
- package/package.descriptor.mjs +87 -11
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +36 -2
- package/src/server/subcommands/addSubpages.js +32 -20
- package/src/server/subcommands/element.js +17 -3
- package/src/server/subcommands/outlet.js +305 -60
- package/src/server/subcommands/page.js +43 -22
- package/src/server/subcommands/pageSupport.js +114 -36
- package/src/server/subcommands/support.js +126 -23
- package/test/addSubpagesSubcommand.test.js +132 -8
- package/test/buildTemplateContext.test.js +31 -0
- package/test/elementSubcommand.test.js +13 -1
- package/test/outletSubcommand.test.js +258 -0
- package/test/packageDescriptor.test.js +11 -0
- package/test/pageSubcommand.test.js +134 -5
package/package.descriptor.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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 \"
|
|
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
|
-
"
|
|
244
|
-
"
|
|
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"
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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.
|
|
10
|
-
"@jskit-ai/shell-web": "0.1.
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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="
|
|
30
|
-
<
|
|
31
|
-
|
|
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
|
|