@jskit-ai/shell-web 0.1.31 → 0.1.33
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 +114 -15
- package/package.json +8 -2
- package/src/client/components/ShellLayout.vue +11 -4
- package/src/client/components/ShellMenuLinkItem.vue +71 -0
- package/src/client/components/ShellOutlet.vue +10 -7
- package/src/client/components/ShellOutletMenuWidget.vue +57 -0
- package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +116 -0
- package/src/client/components/ShellTabLinkItem.vue +128 -0
- package/src/client/index.js +4 -0
- package/src/client/lib/menuIcons.js +210 -0
- package/src/client/placement/runtime.js +22 -22
- package/src/client/placement/validators.js +19 -49
- package/src/client/support/menuLinkTarget.js +97 -0
- package/src/server/support/localLinkItemScaffolds.js +80 -0
- package/templates/expected-existing/src/App.vue +13 -0
- package/templates/expected-existing/src/pages/console/index.vue +12 -0
- package/templates/expected-existing/src/pages/console.vue +13 -0
- package/templates/expected-existing/src/pages/home/index.vue +12 -0
- package/templates/expected-existing/src/pages/home.vue +13 -0
- package/templates/src/components/ShellLayout.vue +11 -4
- package/templates/src/components/menus/MenuLinkItem.vue +30 -0
- package/templates/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
- package/templates/src/components/menus/TabLinkItem.vue +34 -0
- package/templates/src/pages/home/settings/index.vue +8 -8
- package/templates/src/pages/home/settings.vue +4 -1
- package/test/bootstrapClaimContract.test.js +88 -0
- package/test/linkItemScaffoldContract.test.js +209 -0
- package/test/outletMenuWidgetContract.test.js +33 -0
- package/test/placementRegistry.test.js +17 -6
- package/test/placementRuntime.test.js +59 -44
- package/test/settingsPlacementContract.test.js +16 -5
|
@@ -7,15 +7,13 @@ test("placement registry stores unique entries and builds immutable array", () =
|
|
|
7
7
|
|
|
8
8
|
const firstAdded = registry.addPlacement({
|
|
9
9
|
id: "example.profile",
|
|
10
|
-
|
|
11
|
-
position: "top-right",
|
|
10
|
+
target: "shell-layout:top-right",
|
|
12
11
|
surfaces: ["*"],
|
|
13
12
|
componentToken: "example.profile.component"
|
|
14
13
|
});
|
|
15
14
|
const duplicateAdded = registry.addPlacement({
|
|
16
15
|
id: "example.profile",
|
|
17
|
-
|
|
18
|
-
position: "top-right",
|
|
16
|
+
target: "shell-layout:top-right",
|
|
19
17
|
surfaces: ["*"],
|
|
20
18
|
componentToken: "example.profile.component.duplicate"
|
|
21
19
|
});
|
|
@@ -35,11 +33,24 @@ test("placement registry accepts explicit non-global surface ids", () => {
|
|
|
35
33
|
|
|
36
34
|
const added = registry.addPlacement({
|
|
37
35
|
id: "example.admin",
|
|
38
|
-
|
|
39
|
-
position: "top-right",
|
|
36
|
+
target: "shell-layout:top-right",
|
|
40
37
|
surfaces: ["admin"],
|
|
41
38
|
componentToken: "example.admin.component"
|
|
42
39
|
});
|
|
43
40
|
|
|
44
41
|
assert.equal(added, true);
|
|
45
42
|
});
|
|
43
|
+
|
|
44
|
+
test("placement registry rejects legacy split target fields", () => {
|
|
45
|
+
const registry = createPlacementRegistry();
|
|
46
|
+
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => registry.addPlacement({
|
|
49
|
+
id: "example.legacy",
|
|
50
|
+
host: "shell-layout",
|
|
51
|
+
position: "top-right",
|
|
52
|
+
componentToken: "example.legacy.component"
|
|
53
|
+
}),
|
|
54
|
+
/must use "target" only/
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -45,24 +45,21 @@ test("web placement runtime filters by surface/host/position, resolves component
|
|
|
45
45
|
runtime.replacePlacements([
|
|
46
46
|
definePlacement({
|
|
47
47
|
id: "test.menu",
|
|
48
|
-
|
|
49
|
-
position: "primary-menu",
|
|
48
|
+
target: "shell-layout:primary-menu",
|
|
50
49
|
surfaces: ["app"],
|
|
51
50
|
order: 30,
|
|
52
51
|
componentToken: "component.menu"
|
|
53
52
|
}),
|
|
54
53
|
definePlacement({
|
|
55
54
|
id: "test.profile",
|
|
56
|
-
|
|
57
|
-
position: "top-right",
|
|
55
|
+
target: "shell-layout:top-right",
|
|
58
56
|
surfaces: ["*"],
|
|
59
57
|
order: 20,
|
|
60
58
|
componentToken: "component.profile"
|
|
61
59
|
}),
|
|
62
60
|
definePlacement({
|
|
63
61
|
id: "test.alerts",
|
|
64
|
-
|
|
65
|
-
position: "top-right",
|
|
62
|
+
target: "shell-layout:top-right",
|
|
66
63
|
surfaces: ["app"],
|
|
67
64
|
order: 10,
|
|
68
65
|
componentToken: "component.alerts"
|
|
@@ -70,17 +67,48 @@ test("web placement runtime filters by surface/host/position, resolves component
|
|
|
70
67
|
]);
|
|
71
68
|
runtime.setContext(createPlacementContext());
|
|
72
69
|
|
|
73
|
-
const topRight = runtime.getPlacements({ surface: "app",
|
|
70
|
+
const topRight = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
74
71
|
assert.deepEqual(topRight.map((entry) => entry.id), ["test.alerts", "test.profile"]);
|
|
75
72
|
assert.equal(typeof topRight[0].component, "function");
|
|
76
73
|
|
|
77
|
-
const primaryMenu = runtime.getPlacements({ surface: "app",
|
|
74
|
+
const primaryMenu = runtime.getPlacements({ surface: "app", target: "shell-layout:primary-menu" });
|
|
78
75
|
assert.deepEqual(primaryMenu.map((entry) => entry.id), ["test.menu"]);
|
|
79
76
|
|
|
80
|
-
const adminTopRight = runtime.getPlacements({ surface: "admin",
|
|
77
|
+
const adminTopRight = runtime.getPlacements({ surface: "admin", target: "shell-layout:top-right" });
|
|
81
78
|
assert.deepEqual(adminTopRight.map((entry) => entry.id), ["test.profile"]);
|
|
82
79
|
});
|
|
83
80
|
|
|
81
|
+
test("web placement runtime preserves source order when placements share the same order", () => {
|
|
82
|
+
const app = createAppStub({
|
|
83
|
+
tokens: {
|
|
84
|
+
"component.beta": () => null,
|
|
85
|
+
"component.alpha": () => null
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const runtime = createWebPlacementRuntime({ app });
|
|
90
|
+
runtime.replacePlacements([
|
|
91
|
+
definePlacement({
|
|
92
|
+
id: "test.beta",
|
|
93
|
+
target: "home-settings:primary-menu",
|
|
94
|
+
surfaces: ["app"],
|
|
95
|
+
order: 155,
|
|
96
|
+
componentToken: "component.beta"
|
|
97
|
+
}),
|
|
98
|
+
definePlacement({
|
|
99
|
+
id: "test.alpha",
|
|
100
|
+
target: "home-settings:primary-menu",
|
|
101
|
+
surfaces: ["app"],
|
|
102
|
+
order: 155,
|
|
103
|
+
componentToken: "component.alpha"
|
|
104
|
+
})
|
|
105
|
+
]);
|
|
106
|
+
runtime.setContext(createPlacementContext());
|
|
107
|
+
|
|
108
|
+
const menu = runtime.getPlacements({ surface: "app", target: "home-settings:primary-menu" });
|
|
109
|
+
assert.deepEqual(menu.map((entry) => entry.id), ["test.beta", "test.alpha"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
84
112
|
test("web placement runtime applies context contributors and placement when() predicates", () => {
|
|
85
113
|
const app = createAppStub({
|
|
86
114
|
tokens: {
|
|
@@ -98,23 +126,21 @@ test("web placement runtime applies context contributors and placement when() pr
|
|
|
98
126
|
runtime.replacePlacements([
|
|
99
127
|
definePlacement({
|
|
100
128
|
id: "guest.item",
|
|
101
|
-
|
|
102
|
-
position: "primary-menu",
|
|
129
|
+
target: "auth-profile-menu:primary-menu",
|
|
103
130
|
surfaces: ["*"],
|
|
104
131
|
componentToken: "component.guest",
|
|
105
132
|
when: ({ auth }) => !Boolean(auth?.authenticated)
|
|
106
133
|
}),
|
|
107
134
|
definePlacement({
|
|
108
135
|
id: "auth.item",
|
|
109
|
-
|
|
110
|
-
position: "primary-menu",
|
|
136
|
+
target: "auth-profile-menu:primary-menu",
|
|
111
137
|
surfaces: ["*"],
|
|
112
138
|
componentToken: "component.authenticated",
|
|
113
139
|
when: ({ auth }) => Boolean(auth?.authenticated)
|
|
114
140
|
})
|
|
115
141
|
]);
|
|
116
142
|
|
|
117
|
-
const menu = runtime.getPlacements({ surface: "app",
|
|
143
|
+
const menu = runtime.getPlacements({ surface: "app", target: "auth-profile-menu:primary-menu" });
|
|
118
144
|
assert.deepEqual(menu.map((entry) => entry.id), ["auth.item"]);
|
|
119
145
|
});
|
|
120
146
|
|
|
@@ -136,8 +162,7 @@ test("web placement runtime uses runtime context and local context overrides con
|
|
|
136
162
|
runtime.replacePlacements([
|
|
137
163
|
definePlacement({
|
|
138
164
|
id: "allowed",
|
|
139
|
-
|
|
140
|
-
position: "primary-menu",
|
|
165
|
+
target: "auth-profile-menu:primary-menu",
|
|
141
166
|
surfaces: ["*"],
|
|
142
167
|
componentToken: "component.allowed",
|
|
143
168
|
when: ({ auth }) => Boolean(auth?.authenticated)
|
|
@@ -149,13 +174,12 @@ test("web placement runtime uses runtime context and local context overrides con
|
|
|
149
174
|
authenticated: true
|
|
150
175
|
}
|
|
151
176
|
});
|
|
152
|
-
const fromRuntime = runtime.getPlacements({ surface: "app",
|
|
177
|
+
const fromRuntime = runtime.getPlacements({ surface: "app", target: "auth-profile-menu:primary-menu" });
|
|
153
178
|
assert.deepEqual(fromRuntime.map((entry) => entry.id), ["allowed"]);
|
|
154
179
|
|
|
155
180
|
const fromLocalOverride = runtime.getPlacements({
|
|
156
181
|
surface: "app",
|
|
157
|
-
|
|
158
|
-
position: "primary-menu",
|
|
182
|
+
target: "auth-profile-menu:primary-menu",
|
|
159
183
|
context: {
|
|
160
184
|
auth: {
|
|
161
185
|
authenticated: false
|
|
@@ -198,15 +222,13 @@ test("web placement runtime rejects duplicate placement ids", () => {
|
|
|
198
222
|
runtime.replacePlacements([
|
|
199
223
|
definePlacement({
|
|
200
224
|
id: "dup.entry",
|
|
201
|
-
|
|
202
|
-
position: "top-right",
|
|
225
|
+
target: "shell-layout:top-right",
|
|
203
226
|
surfaces: ["*"],
|
|
204
227
|
componentToken: "component.a"
|
|
205
228
|
}),
|
|
206
229
|
definePlacement({
|
|
207
230
|
id: "dup.entry",
|
|
208
|
-
|
|
209
|
-
position: "primary-menu",
|
|
231
|
+
target: "shell-layout:primary-menu",
|
|
210
232
|
surfaces: ["*"],
|
|
211
233
|
componentToken: "component.b"
|
|
212
234
|
})
|
|
@@ -247,25 +269,23 @@ test("web placement runtime skips throwing component tokens and logs resolution
|
|
|
247
269
|
runtime.replacePlacements([
|
|
248
270
|
definePlacement({
|
|
249
271
|
id: "bad",
|
|
250
|
-
|
|
251
|
-
position: "top-right",
|
|
272
|
+
target: "shell-layout:top-right",
|
|
252
273
|
surfaces: ["*"],
|
|
253
274
|
componentToken: "component.bad"
|
|
254
275
|
}),
|
|
255
276
|
definePlacement({
|
|
256
277
|
id: "good",
|
|
257
|
-
|
|
258
|
-
position: "top-right",
|
|
278
|
+
target: "shell-layout:top-right",
|
|
259
279
|
surfaces: ["*"],
|
|
260
280
|
componentToken: "component.good"
|
|
261
281
|
})
|
|
262
282
|
]);
|
|
263
283
|
|
|
264
|
-
const first = runtime.getPlacements({ surface: "app",
|
|
284
|
+
const first = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
265
285
|
assert.deepEqual(first.map((entry) => entry.id), ["good"]);
|
|
266
286
|
assert.equal(errors.length, 1);
|
|
267
287
|
|
|
268
|
-
const second = runtime.getPlacements({ surface: "app",
|
|
288
|
+
const second = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
269
289
|
assert.deepEqual(second.map((entry) => entry.id), ["good"]);
|
|
270
290
|
assert.equal(errors.length, 1);
|
|
271
291
|
});
|
|
@@ -301,31 +321,29 @@ test("web placement runtime clears failed token cache when placements are replac
|
|
|
301
321
|
runtime.replacePlacements([
|
|
302
322
|
definePlacement({
|
|
303
323
|
id: "toggle",
|
|
304
|
-
|
|
305
|
-
position: "top-right",
|
|
324
|
+
target: "shell-layout:top-right",
|
|
306
325
|
surfaces: ["*"],
|
|
307
326
|
componentToken: "component.toggle"
|
|
308
327
|
})
|
|
309
328
|
]);
|
|
310
329
|
|
|
311
|
-
const initial = runtime.getPlacements({ surface: "app",
|
|
330
|
+
const initial = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
312
331
|
assert.equal(initial.length, 0);
|
|
313
332
|
|
|
314
333
|
shouldThrow = false;
|
|
315
|
-
const stillSkipped = runtime.getPlacements({ surface: "app",
|
|
334
|
+
const stillSkipped = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
316
335
|
assert.equal(stillSkipped.length, 0);
|
|
317
336
|
|
|
318
337
|
runtime.replacePlacements([
|
|
319
338
|
definePlacement({
|
|
320
339
|
id: "toggle",
|
|
321
|
-
|
|
322
|
-
position: "top-right",
|
|
340
|
+
target: "shell-layout:top-right",
|
|
323
341
|
surfaces: ["*"],
|
|
324
342
|
componentToken: "component.toggle"
|
|
325
343
|
})
|
|
326
344
|
]);
|
|
327
345
|
|
|
328
|
-
const recovered = runtime.getPlacements({ surface: "app",
|
|
346
|
+
const recovered = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
329
347
|
assert.equal(recovered.length, 1);
|
|
330
348
|
});
|
|
331
349
|
|
|
@@ -341,24 +359,21 @@ test("web placement runtime follows explicit surface targeting without role indi
|
|
|
341
359
|
runtime.replacePlacements([
|
|
342
360
|
definePlacement({
|
|
343
361
|
id: "global.banner",
|
|
344
|
-
|
|
345
|
-
position: "top-right",
|
|
362
|
+
target: "shell-layout:top-right",
|
|
346
363
|
surfaces: ["*"],
|
|
347
364
|
order: 10,
|
|
348
365
|
componentToken: "component.global"
|
|
349
366
|
}),
|
|
350
367
|
definePlacement({
|
|
351
368
|
id: "app.link",
|
|
352
|
-
|
|
353
|
-
position: "top-right",
|
|
369
|
+
target: "shell-layout:top-right",
|
|
354
370
|
surfaces: ["app"],
|
|
355
371
|
order: 20,
|
|
356
372
|
componentToken: "component.app"
|
|
357
373
|
}),
|
|
358
374
|
definePlacement({
|
|
359
375
|
id: "admin.link",
|
|
360
|
-
|
|
361
|
-
position: "top-right",
|
|
376
|
+
target: "shell-layout:top-right",
|
|
362
377
|
surfaces: ["admin"],
|
|
363
378
|
order: 30,
|
|
364
379
|
componentToken: "component.admin"
|
|
@@ -366,9 +381,9 @@ test("web placement runtime follows explicit surface targeting without role indi
|
|
|
366
381
|
]);
|
|
367
382
|
runtime.setContext(createPlacementContext());
|
|
368
383
|
|
|
369
|
-
const appEntries = runtime.getPlacements({ surface: "app",
|
|
384
|
+
const appEntries = runtime.getPlacements({ surface: "app", target: "shell-layout:top-right" });
|
|
370
385
|
assert.deepEqual(appEntries.map((placement) => placement.id), ["global.banner", "app.link"]);
|
|
371
386
|
|
|
372
|
-
const adminEntries = runtime.getPlacements({ surface: "admin",
|
|
387
|
+
const adminEntries = runtime.getPlacements({ surface: "admin", target: "shell-layout:top-right" });
|
|
373
388
|
assert.deepEqual(adminEntries.map((placement) => placement.id), ["global.banner", "admin.link"]);
|
|
374
389
|
});
|
|
@@ -11,7 +11,7 @@ const PACKAGE_DIR = path.resolve(TEST_DIRECTORY, "..");
|
|
|
11
11
|
function readSettingsOutlets() {
|
|
12
12
|
const outlets = descriptor?.metadata?.ui?.placements?.outlets;
|
|
13
13
|
return Array.isArray(outlets)
|
|
14
|
-
? outlets.filter((entry) => String(entry?.
|
|
14
|
+
? outlets.filter((entry) => String(entry?.target || "").trim() === "home-settings:primary-menu")
|
|
15
15
|
: [];
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -25,7 +25,16 @@ function findFileMutation(id) {
|
|
|
25
25
|
test("shell-web home settings template exposes surface-derived settings outlets", async () => {
|
|
26
26
|
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings.vue"), "utf8");
|
|
27
27
|
|
|
28
|
-
assert.match(source,
|
|
28
|
+
assert.match(source, /target="home-settings:primary-menu"/);
|
|
29
|
+
assert.match(source, /default-link-component-token="local\.main\.ui\.surface-aware-menu-link-item"/);
|
|
30
|
+
assert.match(source, /<RouterView \/>/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("shell-web home settings index template is a simple developer-owned stub", async () => {
|
|
34
|
+
const source = await readFile(path.join(PACKAGE_DIR, "templates", "src", "pages", "home", "settings", "index.vue"), "utf8");
|
|
35
|
+
|
|
36
|
+
assert.match(source, /definePage/);
|
|
37
|
+
assert.match(source, /your_child_segment/);
|
|
29
38
|
});
|
|
30
39
|
|
|
31
40
|
test("shell-web descriptor metadata advertises home settings outlets and installs the scaffold page", () => {
|
|
@@ -33,8 +42,8 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
33
42
|
readSettingsOutlets(),
|
|
34
43
|
[
|
|
35
44
|
{
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
target: "home-settings:primary-menu",
|
|
46
|
+
defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
|
|
38
47
|
surfaces: ["home"],
|
|
39
48
|
source: "templates/src/pages/home/settings.vue"
|
|
40
49
|
}
|
|
@@ -45,6 +54,7 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
45
54
|
from: "templates/src/pages/home/settings.vue",
|
|
46
55
|
toSurface: "home",
|
|
47
56
|
toSurfacePath: "settings.vue",
|
|
57
|
+
ownership: "app",
|
|
48
58
|
reason: "Install shell-driven home settings shell route with section navigation.",
|
|
49
59
|
category: "shell-web",
|
|
50
60
|
id: "shell-web-page-home-settings-shell"
|
|
@@ -54,7 +64,8 @@ test("shell-web descriptor metadata advertises home settings outlets and install
|
|
|
54
64
|
from: "templates/src/pages/home/settings/index.vue",
|
|
55
65
|
toSurface: "home",
|
|
56
66
|
toSurfacePath: "settings/index.vue",
|
|
57
|
-
|
|
67
|
+
ownership: "app",
|
|
68
|
+
reason: "Install shell-driven home settings index stub scaffold for app-owned landing or redirect behavior.",
|
|
58
69
|
category: "shell-web",
|
|
59
70
|
id: "shell-web-page-home-settings"
|
|
60
71
|
});
|