@jskit-ai/shell-web 0.1.32 → 0.1.34

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.
Files changed (31) hide show
  1. package/package.descriptor.mjs +111 -33
  2. package/package.json +8 -2
  3. package/src/client/components/ShellLayout.vue +11 -4
  4. package/src/client/components/ShellMenuLinkItem.vue +71 -0
  5. package/src/client/components/ShellOutlet.vue +10 -7
  6. package/src/client/components/ShellOutletMenuWidget.vue +57 -0
  7. package/src/client/components/ShellSurfaceAwareMenuLinkItem.vue +116 -0
  8. package/src/client/components/ShellTabLinkItem.vue +128 -0
  9. package/src/client/index.js +4 -0
  10. package/src/client/lib/menuIcons.js +210 -0
  11. package/src/client/placement/runtime.js +22 -22
  12. package/src/client/placement/validators.js +19 -49
  13. package/src/client/support/menuLinkTarget.js +97 -0
  14. package/src/server/support/localLinkItemScaffolds.js +80 -0
  15. package/templates/expected-existing/src/App.vue +13 -0
  16. package/templates/expected-existing/src/pages/home/index.vue +12 -0
  17. package/templates/expected-existing/src/pages/home.vue +13 -0
  18. package/templates/src/components/ShellLayout.vue +11 -4
  19. package/templates/src/components/menus/MenuLinkItem.vue +30 -0
  20. package/templates/src/components/menus/SurfaceAwareMenuLinkItem.vue +42 -0
  21. package/templates/src/components/menus/TabLinkItem.vue +34 -0
  22. package/templates/src/pages/home/settings/index.vue +8 -8
  23. package/templates/src/pages/home/settings.vue +4 -1
  24. package/test/bootstrapClaimContract.test.js +66 -0
  25. package/test/linkItemScaffoldContract.test.js +209 -0
  26. package/test/outletMenuWidgetContract.test.js +33 -0
  27. package/test/placementRegistry.test.js +17 -6
  28. package/test/placementRuntime.test.js +59 -44
  29. package/test/settingsPlacementContract.test.js +16 -5
  30. package/templates/src/pages/console/index.vue +0 -24
  31. package/templates/src/pages/console.vue +0 -20
@@ -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
- host: "shell-layout",
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
- host: "shell-layout",
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
- host: "shell-layout",
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", host: "shell-layout", position: "top-right" });
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", host: "shell-layout", position: "primary-menu" });
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", host: "shell-layout", position: "top-right" });
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
- host: "auth-profile-menu",
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
- host: "auth-profile-menu",
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", host: "auth-profile-menu", position: "primary-menu" });
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
- host: "auth-profile-menu",
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", host: "auth-profile-menu", position: "primary-menu" });
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
- host: "auth-profile-menu",
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
- host: "shell-layout",
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
- host: "shell-layout",
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
- host: "shell-layout",
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
- host: "shell-layout",
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", host: "shell-layout", position: "top-right" });
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", host: "shell-layout", position: "top-right" });
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
- host: "shell-layout",
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", host: "shell-layout", position: "top-right" });
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", host: "shell-layout", position: "top-right" });
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
- host: "shell-layout",
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", host: "shell-layout", position: "top-right" });
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
- host: "shell-layout",
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
- host: "shell-layout",
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
- host: "shell-layout",
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", host: "shell-layout", position: "top-right" });
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", host: "shell-layout", position: "top-right" });
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?.host || "").trim() === "home-settings")
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, /<ShellOutlet host="home-settings" position="primary-menu" \/>/);
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
- host: "home-settings",
37
- position: "primary-menu",
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
- reason: "Install shell-driven home settings landing page scaffold.",
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
  });
@@ -1,24 +0,0 @@
1
- <template>
2
- <v-card rounded="lg" elevation="1" border>
3
- <v-card-item>
4
- <template #prepend>
5
- <v-chip color="primary" size="small" label>Console</v-chip>
6
- </template>
7
- <v-card-title class="text-h5">Operations Console</v-card-title>
8
- <v-card-subtitle>Operator tools, scripts, and diagnostics.</v-card-subtitle>
9
- </v-card-item>
10
- <v-divider />
11
- <v-card-text class="d-flex flex-column ga-4">
12
- <div class="d-flex flex-wrap ga-3">
13
- <v-chip color="secondary" variant="tonal" label>Route: /console</v-chip>
14
- <v-chip color="info" variant="tonal" label>Surface status: enabled</v-chip>
15
- </div>
16
- <p class="text-medium-emphasis mb-0">
17
- Ideal for operator actions, scripts, and technical insights not meant for end users.
18
- </p>
19
- <div>
20
- <v-btn color="primary" variant="flat" to="/home">Back to home</v-btn>
21
- </div>
22
- </v-card-text>
23
- </v-card>
24
- </template>
@@ -1,20 +0,0 @@
1
- <route lang="json">
2
- {
3
- "meta": {
4
- "jskit": {
5
- "surface": "console"
6
- }
7
- }
8
- }
9
- </route>
10
-
11
- <script setup>
12
- import ShellLayout from "@/components/ShellLayout.vue";
13
- import { RouterView } from "vue-router";
14
- </script>
15
-
16
- <template>
17
- <ShellLayout title="" subtitle="">
18
- <RouterView />
19
- </ShellLayout>
20
- </template>