@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.
- package/package.descriptor.mjs +117 -24
- package/package.json +3 -3
- package/src/server/buildTemplateContext.js +44 -3
- package/src/server/subcommands/addSubpages.js +89 -2
- package/src/server/subcommands/element.js +42 -10
- package/src/server/subcommands/outlet.js +399 -20
- package/src/server/subcommands/page.js +49 -23
- package/src/server/subcommands/pageSupport.js +115 -37
- package/src/server/subcommands/support.js +128 -23
- package/test/addSubpagesSubcommand.test.js +163 -15
- package/test/buildTemplateContext.test.js +227 -34
- package/test/elementSubcommand.test.js +89 -12
- package/test/outletSubcommand.test.js +305 -14
- package/test/packageDescriptor.test.js +11 -0
- package/test/pageSubcommand.test.js +234 -17
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,23 +42,46 @@ export default Object.freeze({
|
|
|
40
42
|
inputType: "text",
|
|
41
43
|
defaultValue: "",
|
|
42
44
|
promptLabel: "Placement target",
|
|
43
|
-
promptHint: "
|
|
45
|
+
promptHint: "Semantic placement target for placed-element, outlet, and topology mapping (format: area.slot, default for placed-element: shell.status)."
|
|
44
46
|
},
|
|
45
|
-
|
|
47
|
+
kind: {
|
|
46
48
|
required: false,
|
|
47
49
|
inputType: "text",
|
|
50
|
+
validationType: "enum",
|
|
51
|
+
allowedValues: ["component", "link"],
|
|
48
52
|
defaultValue: "",
|
|
49
|
-
promptLabel: "
|
|
50
|
-
promptHint: "
|
|
53
|
+
promptLabel: "Placement kind",
|
|
54
|
+
promptHint: "Use component for componentToken-backed entries, or link when topology should provide a link renderer."
|
|
51
55
|
},
|
|
52
|
-
|
|
56
|
+
owner: {
|
|
53
57
|
required: false,
|
|
54
58
|
inputType: "text",
|
|
55
59
|
defaultValue: "",
|
|
56
|
-
promptLabel: "
|
|
57
|
-
promptHint:
|
|
58
|
-
|
|
60
|
+
promptLabel: "Placement owner",
|
|
61
|
+
promptHint: "Optional owner id for semantic topology mappings. Page/settings placements default to the outlet host."
|
|
62
|
+
},
|
|
63
|
+
description: {
|
|
64
|
+
required: false,
|
|
65
|
+
inputType: "text",
|
|
66
|
+
defaultValue: "",
|
|
67
|
+
promptLabel: "Description",
|
|
68
|
+
promptHint: "Optional description for generated semantic topology mappings."
|
|
69
|
+
},
|
|
70
|
+
"link-renderer": {
|
|
71
|
+
required: false,
|
|
72
|
+
inputType: "text",
|
|
73
|
+
defaultValue: "local.main.ui.surface-aware-menu-link-item",
|
|
74
|
+
promptLabel: "Link renderer",
|
|
75
|
+
promptHint: "Default link renderer token for generated topology variants."
|
|
59
76
|
},
|
|
77
|
+
"link-placement": {
|
|
78
|
+
required: false,
|
|
79
|
+
inputType: "text",
|
|
80
|
+
defaultValue: "",
|
|
81
|
+
promptLabel: "Link placement",
|
|
82
|
+
promptHint: "Optional semantic target for the generated page link placement (format: area.slot)."
|
|
83
|
+
},
|
|
84
|
+
"navigation-role": GENERATED_UI_NAVIGATION_ROLE_OPTION,
|
|
60
85
|
"link-to": {
|
|
61
86
|
required: false,
|
|
62
87
|
inputType: "text",
|
|
@@ -70,7 +95,28 @@ export default Object.freeze({
|
|
|
70
95
|
inputType: "text",
|
|
71
96
|
defaultValue: "",
|
|
72
97
|
promptLabel: "Outlet target",
|
|
73
|
-
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."
|
|
74
120
|
},
|
|
75
121
|
title: {
|
|
76
122
|
required: false,
|
|
@@ -120,9 +166,9 @@ export default Object.freeze({
|
|
|
120
166
|
descriptionKey: "page-target-file"
|
|
121
167
|
}
|
|
122
168
|
],
|
|
123
|
-
optionNames: ["name", "
|
|
169
|
+
optionNames: ["name", "navigation-role", "link-placement", "link-to", "force"],
|
|
124
170
|
notes: [
|
|
125
|
-
"If a nearest parent subpages target is found, placement
|
|
171
|
+
"If a nearest parent subpages target is found, semantic placement and props.to are inferred automatically.",
|
|
126
172
|
"If the parent target page is index.vue, child pages belong under index/...",
|
|
127
173
|
"If the target page file already exists, rerun with --force to overwrite it."
|
|
128
174
|
],
|
|
@@ -151,10 +197,10 @@ export default Object.freeze({
|
|
|
151
197
|
entrypoint: "src/server/subcommands/element.js",
|
|
152
198
|
export: "runGeneratorSubcommand",
|
|
153
199
|
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"],
|
|
200
|
+
optionNames: ["name", "surface", "path", "placement", "owner", "force"],
|
|
155
201
|
requiredOptionNames: ["name"],
|
|
156
202
|
notes: [
|
|
157
|
-
"If --placement is omitted, the placed element is added at shell
|
|
203
|
+
"If --placement is omitted, the placed element is added at shell.status.",
|
|
158
204
|
"If the placement target belongs to a page-owned outlet, JSKIT infers the surface automatically.",
|
|
159
205
|
"If the target is shared and the app has multiple enabled surfaces, pass --surface explicitly.",
|
|
160
206
|
"If the component file already exists, rerun with --force to overwrite it."
|
|
@@ -174,7 +220,7 @@ export default Object.freeze({
|
|
|
174
220
|
" --name \"Ops Panel\" \\",
|
|
175
221
|
" --surface admin \\",
|
|
176
222
|
" --path src/widgets \\",
|
|
177
|
-
" --placement shell
|
|
223
|
+
" --placement shell.status \\",
|
|
178
224
|
" --force"
|
|
179
225
|
]
|
|
180
226
|
}
|
|
@@ -204,7 +250,7 @@ export default Object.freeze({
|
|
|
204
250
|
"npx jskit generate ui-generator add-subpages \\",
|
|
205
251
|
" admin/customers/[customerId]/index.vue \\",
|
|
206
252
|
" --title \"Customer\" \\",
|
|
207
|
-
" --subtitle \"
|
|
253
|
+
" --subtitle \"Customer profile and activity.\""
|
|
208
254
|
]
|
|
209
255
|
},
|
|
210
256
|
{
|
|
@@ -226,9 +272,9 @@ export default Object.freeze({
|
|
|
226
272
|
export: "runGeneratorSubcommand",
|
|
227
273
|
description: "Inject a generic ShellOutlet block into an existing Vue page/component.",
|
|
228
274
|
longDescription: [
|
|
229
|
-
"A ShellOutlet creates a
|
|
230
|
-
"
|
|
231
|
-
"
|
|
275
|
+
"A ShellOutlet creates a concrete placement recipient inside a Vue file.",
|
|
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."
|
|
232
278
|
],
|
|
233
279
|
positionalArgs: [
|
|
234
280
|
{
|
|
@@ -237,10 +283,12 @@ export default Object.freeze({
|
|
|
237
283
|
descriptionKey: "existing-vue-sfc-target-file"
|
|
238
284
|
}
|
|
239
285
|
],
|
|
240
|
-
optionNames: ["target"],
|
|
286
|
+
optionNames: ["target", "placement", "owner", "surface", "description", "kind", "link-renderer", "compact-target", "medium-target", "expanded-target"],
|
|
241
287
|
requiredOptionNames: ["target"],
|
|
242
288
|
notes: [
|
|
243
|
-
"Use --target host:position."
|
|
289
|
+
"Use --target host:position for the concrete outlet and --placement area.slot for the public semantic placement.",
|
|
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."
|
|
244
292
|
],
|
|
245
293
|
examples: [
|
|
246
294
|
{
|
|
@@ -248,7 +296,8 @@ export default Object.freeze({
|
|
|
248
296
|
lines: [
|
|
249
297
|
"npx jskit generate ui-generator outlet \\",
|
|
250
298
|
" src/components/ContactSummaryCard.vue \\",
|
|
251
|
-
" --target contact-view:sub-pages"
|
|
299
|
+
" --target contact-view:sub-pages \\",
|
|
300
|
+
" --placement page.section-nav"
|
|
252
301
|
]
|
|
253
302
|
},
|
|
254
303
|
{
|
|
@@ -256,7 +305,51 @@ export default Object.freeze({
|
|
|
256
305
|
lines: [
|
|
257
306
|
"npx jskit generate ui-generator outlet \\",
|
|
258
307
|
" src/pages/admin/customers/[customerId]/index.vue \\",
|
|
259
|
-
" --target customer-view:summary-actions"
|
|
308
|
+
" --target customer-view:summary-actions \\",
|
|
309
|
+
" --placement page.actions \\",
|
|
310
|
+
" --surface admin"
|
|
311
|
+
]
|
|
312
|
+
}
|
|
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"
|
|
260
353
|
]
|
|
261
354
|
}
|
|
262
355
|
]
|
|
@@ -278,7 +371,7 @@ export default Object.freeze({
|
|
|
278
371
|
mutations: {
|
|
279
372
|
dependencies: {
|
|
280
373
|
runtime: {
|
|
281
|
-
"@jskit-ai/users-web": "0.1.
|
|
374
|
+
"@jskit-ai/users-web": "0.1.82"
|
|
282
375
|
},
|
|
283
376
|
dev: {}
|
|
284
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
|
|
|
@@ -12,6 +17,34 @@ function resolveLinkToPropLine(linkTo = "") {
|
|
|
12
17
|
return ` to: ${JSON.stringify(linkTo)},\n`;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
function resolveOwnerLine(owner = "") {
|
|
21
|
+
if (!owner) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
return ` owner: ${JSON.stringify(owner)},\n`;
|
|
25
|
+
}
|
|
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
|
+
|
|
15
48
|
async function buildUiPageTemplateContext({
|
|
16
49
|
appRoot,
|
|
17
50
|
targetFile = "",
|
|
@@ -27,14 +60,16 @@ async function buildUiPageTemplateContext({
|
|
|
27
60
|
pageTarget,
|
|
28
61
|
targetFile,
|
|
29
62
|
context: "ui-generator page",
|
|
30
|
-
placement: options
|
|
31
|
-
|
|
63
|
+
placement: resolveNavigationRoleLinkPlacement(options, {
|
|
64
|
+
routePath: resolveNavigationInferenceRoutePath(pageTarget)
|
|
65
|
+
}),
|
|
32
66
|
linkTo: options?.["link-to"]
|
|
33
67
|
});
|
|
34
68
|
|
|
35
69
|
return {
|
|
36
70
|
__JSKIT_UI_LINK_PLACEMENT_ID__: pageTarget.placementId,
|
|
37
71
|
__JSKIT_UI_LINK_PLACEMENT_TARGET__: String(linkTarget.placementTarget?.id || ""),
|
|
72
|
+
__JSKIT_UI_LINK_OWNER_LINE__: resolveOwnerLine(linkTarget.placementTarget?.owner || ""),
|
|
38
73
|
__JSKIT_UI_LINK_COMPONENT_TOKEN__: String(linkTarget.componentToken || ""),
|
|
39
74
|
__JSKIT_UI_LINK_ICON__: DEFAULT_GENERATED_LINK_ICON,
|
|
40
75
|
__JSKIT_UI_LINK_WORKSPACE_SUFFIX__: pageTarget.routeUrlSuffix,
|
|
@@ -44,4 +79,10 @@ async function buildUiPageTemplateContext({
|
|
|
44
79
|
};
|
|
45
80
|
}
|
|
46
81
|
|
|
47
|
-
export {
|
|
82
|
+
export {
|
|
83
|
+
buildUiPageTemplateContext,
|
|
84
|
+
normalizeGeneratedUiNavigationRole as normalizeNavigationRole,
|
|
85
|
+
resolveNavigationInferenceRoutePath,
|
|
86
|
+
resolveNavigationRoleLinkPlacement,
|
|
87
|
+
shouldCreateNavigationLink
|
|
88
|
+
};
|
|
@@ -1,17 +1,68 @@
|
|
|
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,
|
|
4
5
|
DEFAULT_SUBPAGES_POSITION,
|
|
6
|
+
SUBPAGES_LINK_COMPONENT_TOKEN,
|
|
5
7
|
deriveDefaultSubpagesHost,
|
|
6
8
|
resolvePageTargetDetails,
|
|
7
9
|
upgradePageFileToSubpages
|
|
8
10
|
} from "./pageSupport.js";
|
|
9
11
|
import {
|
|
12
|
+
PLACEMENT_TOPOLOGY_FILE,
|
|
13
|
+
appendTopologyBlockIfPlacementMissing,
|
|
10
14
|
requireSinglePositionalTargetFile,
|
|
15
|
+
resolvePathWithinApp,
|
|
11
16
|
resolveOutletTargetId,
|
|
12
17
|
rejectUnexpectedOptions
|
|
13
18
|
} from "./support.js";
|
|
14
19
|
|
|
20
|
+
function resolveOutletOwner(target = "") {
|
|
21
|
+
const normalizedTarget = normalizeText(target);
|
|
22
|
+
const separatorIndex = normalizedTarget.indexOf(":");
|
|
23
|
+
if (separatorIndex <= 0) {
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
return normalizedTarget.slice(0, separatorIndex);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderSectionNavTopologyBlock({
|
|
30
|
+
marker = "",
|
|
31
|
+
owner = "",
|
|
32
|
+
surface = "",
|
|
33
|
+
target = ""
|
|
34
|
+
} = {}) {
|
|
35
|
+
return (
|
|
36
|
+
`// ${marker}\n` +
|
|
37
|
+
"addPlacementTopology({\n" +
|
|
38
|
+
` id: "page.section-nav",\n` +
|
|
39
|
+
` owner: "${owner}",\n` +
|
|
40
|
+
` description: "Navigation between child pages in this section.",\n` +
|
|
41
|
+
` surfaces: ["${surface}"],\n` +
|
|
42
|
+
" variants: {\n" +
|
|
43
|
+
" compact: {\n" +
|
|
44
|
+
` outlet: "${target}",\n` +
|
|
45
|
+
" renderers: {\n" +
|
|
46
|
+
` link: "${SUBPAGES_LINK_COMPONENT_TOKEN}"\n` +
|
|
47
|
+
" }\n" +
|
|
48
|
+
" },\n" +
|
|
49
|
+
" medium: {\n" +
|
|
50
|
+
` outlet: "${target}",\n` +
|
|
51
|
+
" renderers: {\n" +
|
|
52
|
+
` link: "${SUBPAGES_LINK_COMPONENT_TOKEN}"\n` +
|
|
53
|
+
" }\n" +
|
|
54
|
+
" },\n" +
|
|
55
|
+
" expanded: {\n" +
|
|
56
|
+
` outlet: "${target}",\n` +
|
|
57
|
+
" renderers: {\n" +
|
|
58
|
+
` link: "${SUBPAGES_LINK_COMPONENT_TOKEN}"\n` +
|
|
59
|
+
" }\n" +
|
|
60
|
+
" }\n" +
|
|
61
|
+
" }\n" +
|
|
62
|
+
"});\n"
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
15
66
|
function resolveSubpagesOutletTarget(options = {}, pageTarget = {}) {
|
|
16
67
|
const rawTarget = normalizeText(options?.target);
|
|
17
68
|
const defaultTarget = `${deriveDefaultSubpagesHost(pageTarget)}:${DEFAULT_SUBPAGES_POSITION}`;
|
|
@@ -49,6 +100,33 @@ async function runGeneratorSubcommand({
|
|
|
49
100
|
});
|
|
50
101
|
const outletTarget = resolveSubpagesOutletTarget(options, pageTarget);
|
|
51
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
|
+
|
|
52
130
|
const result = await upgradePageFileToSubpages({
|
|
53
131
|
appRoot,
|
|
54
132
|
targetFile,
|
|
@@ -60,10 +138,19 @@ async function runGeneratorSubcommand({
|
|
|
60
138
|
dryRun
|
|
61
139
|
});
|
|
62
140
|
|
|
141
|
+
const touchedFiles = new Set(result.touchedFiles);
|
|
142
|
+
if (topologyApplied.changed) {
|
|
143
|
+
if (dryRun !== true) {
|
|
144
|
+
await writeFile(topologyPath.absolutePath, topologyApplied.content, "utf8");
|
|
145
|
+
}
|
|
146
|
+
touchedFiles.add(topologyPath.relativePath);
|
|
147
|
+
}
|
|
148
|
+
const touchedFileList = [...touchedFiles].sort((left, right) => left.localeCompare(right));
|
|
149
|
+
|
|
63
150
|
return {
|
|
64
|
-
touchedFiles:
|
|
151
|
+
touchedFiles: touchedFileList,
|
|
65
152
|
summary:
|
|
66
|
-
|
|
153
|
+
touchedFileList.length > 0
|
|
67
154
|
? `Enabled subpages in ${result.targetFile} for "${pageTarget.routeUrlSuffix}" using outlet target "${outletTarget.id}".`
|
|
68
155
|
: `Subpages are already enabled in ${result.targetFile}.`
|
|
69
156
|
};
|
|
@@ -3,7 +3,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import {
|
|
4
4
|
listSurfacePageRoots,
|
|
5
5
|
resolveBestSurfaceMatchFromPageFile,
|
|
6
|
-
|
|
6
|
+
resolveSemanticPlacementTargetFromApp,
|
|
7
7
|
toPosixPath
|
|
8
8
|
} from "@jskit-ai/kernel/server/support";
|
|
9
9
|
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface";
|
|
@@ -22,15 +22,29 @@ import {
|
|
|
22
22
|
insertBeforeClassDeclaration
|
|
23
23
|
} from "./support.js";
|
|
24
24
|
|
|
25
|
-
const DEFAULT_ELEMENT_PLACEMENT = "shell
|
|
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
|
|
|
@@ -41,14 +55,28 @@ async function resolvePlacedElementSurface({
|
|
|
41
55
|
context = "ui-generator placed-element"
|
|
42
56
|
} = {}) {
|
|
43
57
|
const explicitSurface = normalizeSurfaceId(surface);
|
|
58
|
+
const placementSurfaces = Array.isArray(placementTarget?.surfaces)
|
|
59
|
+
? placementTarget.surfaces.map((entry) => normalizeSurfaceId(entry)).filter(Boolean)
|
|
60
|
+
: [];
|
|
61
|
+
const isPlacementGlobal = placementSurfaces.length < 1 || placementSurfaces.includes("*");
|
|
62
|
+
const concreteSourcePath = normalizeText(placementTarget?.sourcePath).startsWith("src/")
|
|
63
|
+
? normalizeText(placementTarget?.sourcePath)
|
|
64
|
+
: "";
|
|
44
65
|
const inferredSurfaceMatch = await resolveBestSurfaceMatchFromPageFile(
|
|
45
|
-
|
|
66
|
+
concreteSourcePath,
|
|
46
67
|
await listSurfacePageRoots(appRoot, { context }),
|
|
47
68
|
{ context }
|
|
48
69
|
);
|
|
49
|
-
const inferredSurface =
|
|
70
|
+
const inferredSurface =
|
|
71
|
+
normalizeSurfaceId(inferredSurfaceMatch?.surfaceId) ||
|
|
72
|
+
(placementSurfaces.length === 1 && !isPlacementGlobal ? placementSurfaces[0] : "");
|
|
50
73
|
|
|
51
74
|
if (explicitSurface) {
|
|
75
|
+
if (!isPlacementGlobal && placementSurfaces.length > 0 && !placementSurfaces.includes(explicitSurface)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" is not available on surface "${explicitSurface}".`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
52
80
|
if (inferredSurface && explicitSurface !== inferredSurface) {
|
|
53
81
|
throw new Error(
|
|
54
82
|
`${context} target "${normalizeText(placementTarget?.id) || "<unknown>"}" belongs to surface "${inferredSurface}", ` +
|
|
@@ -89,7 +117,7 @@ async function runGeneratorSubcommand({
|
|
|
89
117
|
if (Array.isArray(args) && args.length > 0) {
|
|
90
118
|
throw new Error("ui-generator placed-element does not accept positional arguments.");
|
|
91
119
|
}
|
|
92
|
-
rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "force"], {
|
|
120
|
+
rejectUnexpectedOptions(options, ["name", "surface", "path", "placement", "owner", "force"], {
|
|
93
121
|
context: "ui-generator placed-element"
|
|
94
122
|
});
|
|
95
123
|
|
|
@@ -115,10 +143,11 @@ async function runGeneratorSubcommand({
|
|
|
115
143
|
context: "ui-generator placed-element"
|
|
116
144
|
});
|
|
117
145
|
const componentToken = `local.main.ui.element.${elementNameKebab}`;
|
|
118
|
-
const placementTarget = await
|
|
146
|
+
const placementTarget = await resolveSemanticPlacementTargetFromApp({
|
|
119
147
|
appRoot,
|
|
120
148
|
context: "ui-generator",
|
|
121
|
-
placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT
|
|
149
|
+
placement: options?.placement || DEFAULT_ELEMENT_PLACEMENT,
|
|
150
|
+
owner: options?.owner
|
|
122
151
|
});
|
|
123
152
|
const surface = await resolvePlacedElementSurface({
|
|
124
153
|
appRoot,
|
|
@@ -179,12 +208,15 @@ async function runGeneratorSubcommand({
|
|
|
179
208
|
|
|
180
209
|
const placementSource = await readFile(placementPath.absolutePath, "utf8");
|
|
181
210
|
const placementMarker = `jskit:ui-generator.element:${surface}:${elementNameKebab}`;
|
|
211
|
+
const ownerLine = placementTarget.owner ? ` owner: "${placementTarget.owner}",\n` : "";
|
|
182
212
|
const placementBlock =
|
|
183
213
|
`// ${placementMarker}\n` +
|
|
184
214
|
"{\n" +
|
|
185
215
|
" addPlacement({\n" +
|
|
186
216
|
` id: "ui-generator.element.${elementNameKebab}",\n` +
|
|
187
217
|
` target: "${placementTarget.id}",\n` +
|
|
218
|
+
ownerLine +
|
|
219
|
+
` kind: "component",\n` +
|
|
188
220
|
` surfaces: ["${surface}"],\n` +
|
|
189
221
|
" order: 155,\n" +
|
|
190
222
|
` componentToken: "${componentToken}"\n` +
|