@jskit-ai/users-web 0.1.67 → 0.1.69

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,9 +1,9 @@
1
- import { HOME_TOOLS_OUTLET } from "./src/shared/toolsOutletContracts.js";
1
+ import { HOME_COG_OUTLET } from "./src/shared/toolsOutletContracts.js";
2
2
 
3
3
  export default Object.freeze({
4
4
  packageVersion: 1,
5
5
  packageId: "@jskit-ai/users-web",
6
- version: "0.1.67",
6
+ version: "0.1.69",
7
7
  kind: "runtime",
8
8
  description: "Users web module: account/profile UI plus shared users web widgets.",
9
9
  dependsOn: [
@@ -96,7 +96,7 @@ export default Object.freeze({
96
96
  },
97
97
  {
98
98
  subpath: "./client/account-settings/sections",
99
- summary: "Exports account settings section extension seam helpers."
99
+ summary: "Exports placement-backed account settings section helpers."
100
100
  }
101
101
  ],
102
102
  containerTokens: {
@@ -111,10 +111,15 @@ export default Object.freeze({
111
111
  placements: {
112
112
  outlets: [
113
113
  {
114
- target: HOME_TOOLS_OUTLET.target,
115
- defaultLinkComponentToken: HOME_TOOLS_OUTLET.defaultLinkComponentToken,
114
+ target: HOME_COG_OUTLET.target,
115
+ defaultLinkComponentToken: HOME_COG_OUTLET.defaultLinkComponentToken,
116
116
  surfaces: ["home"],
117
117
  source: "src/client/components/UsersHomeToolsWidget.vue"
118
+ },
119
+ {
120
+ target: "account-settings:sections",
121
+ surfaces: ["account"],
122
+ source: "templates/src/components/account/settings/AccountSettingsClientElement.vue"
118
123
  }
119
124
  ],
120
125
  contributions: [
@@ -138,7 +143,7 @@ export default Object.freeze({
138
143
  },
139
144
  {
140
145
  id: "users.home.menu.settings",
141
- target: "home-tools:primary-menu",
146
+ target: "home-cog:primary-menu",
142
147
  surfaces: ["home"],
143
148
  order: 100,
144
149
  componentToken: "local.main.ui.surface-aware-menu-link-item",
@@ -154,12 +159,12 @@ export default Object.freeze({
154
159
  runtime: {
155
160
  "@tanstack/vue-query": "5.92.12",
156
161
  "@mdi/js": "^7.4.47",
157
- "@jskit-ai/http-runtime": "0.1.51",
158
- "@jskit-ai/realtime": "0.1.51",
159
- "@jskit-ai/kernel": "0.1.52",
160
- "@jskit-ai/shell-web": "0.1.51",
161
- "@jskit-ai/uploads-image-web": "0.1.30",
162
- "@jskit-ai/users-core": "0.1.62",
162
+ "@jskit-ai/http-runtime": "0.1.53",
163
+ "@jskit-ai/realtime": "0.1.53",
164
+ "@jskit-ai/kernel": "0.1.54",
165
+ "@jskit-ai/shell-web": "0.1.53",
166
+ "@jskit-ai/uploads-image-web": "0.1.32",
167
+ "@jskit-ai/users-core": "0.1.64",
163
168
  vuetify: "^4.0.0"
164
169
  },
165
170
  dev: {}
@@ -238,7 +243,7 @@ export default Object.freeze({
238
243
  position: "bottom",
239
244
  skipIfContains: "id: \"users.home.tools.widget\"",
240
245
  value:
241
- "\naddPlacement({\n id: \"users.home.tools.widget\",\n target: \"shell-layout:top-right\",\n surfaces: [\"home\"],\n order: 900,\n componentToken: \"users.web.home.tools.widget\",\n when: ({ auth }) => auth?.authenticated === true\n});\n\naddPlacement({\n id: \"users.home.menu.settings\",\n target: \"home-tools:primary-menu\",\n surfaces: [\"home\"],\n order: 100,\n componentToken: \"local.main.ui.surface-aware-menu-link-item\",\n props: {\n label: \"Settings\",\n surface: \"home\",\n scopedSuffix: \"/settings\",\n unscopedSuffix: \"/settings\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
246
+ "\naddPlacement({\n id: \"users.home.tools.widget\",\n target: \"shell-layout:top-right\",\n surfaces: [\"home\"],\n order: 900,\n componentToken: \"users.web.home.tools.widget\",\n when: ({ auth }) => auth?.authenticated === true\n});\n\naddPlacement({\n id: \"users.home.menu.settings\",\n target: \"home-cog:primary-menu\",\n surfaces: [\"home\"],\n order: 100,\n componentToken: \"local.main.ui.surface-aware-menu-link-item\",\n props: {\n label: \"Settings\",\n surface: \"home\",\n scopedSuffix: \"/settings\",\n unscopedSuffix: \"/settings\"\n },\n when: ({ auth }) => auth?.authenticated === true\n});\n",
242
247
  reason: "Append users-web home tools widget and settings menu placements into app-owned placement registry.",
243
248
  category: "users-web",
244
249
  id: "users-web-home-tools-placement"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/users-web",
3
- "version": "0.1.67",
3
+ "version": "0.1.69",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "test": "node --test"
@@ -35,12 +35,12 @@
35
35
  "dependencies": {
36
36
  "@tanstack/vue-query": "5.92.12",
37
37
  "@mdi/js": "^7.4.47",
38
- "@jskit-ai/http-runtime": "0.1.51",
39
- "@jskit-ai/kernel": "0.1.52",
40
- "@jskit-ai/realtime": "0.1.51",
41
- "@jskit-ai/shell-web": "0.1.51",
42
- "@jskit-ai/uploads-image-web": "0.1.30",
43
- "@jskit-ai/users-core": "0.1.62",
38
+ "@jskit-ai/http-runtime": "0.1.53",
39
+ "@jskit-ai/kernel": "0.1.54",
40
+ "@jskit-ai/realtime": "0.1.53",
41
+ "@jskit-ai/shell-web": "0.1.53",
42
+ "@jskit-ai/uploads-image-web": "0.1.32",
43
+ "@jskit-ai/users-core": "0.1.64",
44
44
  "vuetify": "^4.0.0"
45
45
  }
46
46
  }
@@ -1,21 +1,31 @@
1
- import { inject } from "vue";
1
+ import {
2
+ computed,
3
+ inject,
4
+ onBeforeUnmount,
5
+ onMounted,
6
+ ref
7
+ } from "vue";
8
+ import { useWebPlacementContext } from "@jskit-ai/shell-web/client/placement";
2
9
 
3
- const ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY = Symbol("users-web.account-settings.sections");
4
- const ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG = "users.web.account-settings.sections";
10
+ const ACCOUNT_SETTINGS_SECTION_TARGET = "account-settings:sections";
5
11
  const EMPTY_ACCOUNT_SETTINGS_SECTIONS = Object.freeze([]);
6
12
  const RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES = Object.freeze([
7
13
  "profile",
8
14
  "preferences",
9
15
  "notifications"
10
16
  ]);
17
+ const WEB_PLACEMENT_RUNTIME_INJECTION_KEY = "jskit.shell-web.runtime.web-placement.client";
11
18
 
12
19
  function normalizeAccountSettingsSectionEntry(entry = null) {
13
20
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
14
21
  return null;
15
22
  }
16
23
 
17
- const value = String(entry.value || "").trim().toLowerCase();
18
- const title = String(entry.title || "").trim();
24
+ const props = entry?.props && typeof entry.props === "object" && !Array.isArray(entry.props)
25
+ ? entry.props
26
+ : {};
27
+ const value = String(props.value || entry.value || "").trim().toLowerCase();
28
+ const title = String(props.title || entry.title || "").trim();
19
29
  const component = entry.component;
20
30
  if (!value || !title || !component) {
21
31
  return null;
@@ -26,7 +36,7 @@ function normalizeAccountSettingsSectionEntry(entry = null) {
26
36
  title,
27
37
  component,
28
38
  order: Number.isFinite(Number(entry.order)) ? Number(entry.order) : 500,
29
- usesSharedRuntime: entry.usesSharedRuntime === true
39
+ usesSharedRuntime: props.usesSharedRuntime === true || entry.usesSharedRuntime === true
30
40
  });
31
41
  }
32
42
 
@@ -51,6 +61,7 @@ function resolveAccountSettingsSections(entries = []) {
51
61
  if (!resolved || seen.has(resolved.value)) {
52
62
  continue;
53
63
  }
64
+
54
65
  seen.add(resolved.value);
55
66
  normalized.push(resolved);
56
67
  }
@@ -59,12 +70,49 @@ function resolveAccountSettingsSections(entries = []) {
59
70
  }
60
71
 
61
72
  function useAccountSettingsSections() {
62
- return inject(ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY, EMPTY_ACCOUNT_SETTINGS_SECTIONS);
73
+ const placementRuntime = inject(WEB_PLACEMENT_RUNTIME_INJECTION_KEY, null);
74
+ const { context: placementContext } = useWebPlacementContext();
75
+ const revision = ref(
76
+ placementRuntime && typeof placementRuntime.getRevision === "function"
77
+ ? placementRuntime.getRevision()
78
+ : 0
79
+ );
80
+ let unsubscribe = null;
81
+
82
+ onMounted(() => {
83
+ if (!placementRuntime || typeof placementRuntime.subscribe !== "function") {
84
+ return;
85
+ }
86
+ unsubscribe = placementRuntime.subscribe((event = {}) => {
87
+ const nextRevision = Number(event.revision);
88
+ revision.value = Number.isInteger(nextRevision) ? nextRevision : revision.value + 1;
89
+ });
90
+ });
91
+
92
+ onBeforeUnmount(() => {
93
+ if (typeof unsubscribe === "function") {
94
+ unsubscribe();
95
+ unsubscribe = null;
96
+ }
97
+ });
98
+
99
+ return computed(() => {
100
+ void revision.value;
101
+ if (!placementRuntime || typeof placementRuntime.getPlacements !== "function") {
102
+ return EMPTY_ACCOUNT_SETTINGS_SECTIONS;
103
+ }
104
+
105
+ const placements = placementRuntime.getPlacements({
106
+ surface: "account",
107
+ target: ACCOUNT_SETTINGS_SECTION_TARGET,
108
+ context: placementContext.value
109
+ });
110
+ return resolveAccountSettingsSections(placements);
111
+ });
63
112
  }
64
113
 
65
114
  export {
66
- ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
67
- ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
115
+ ACCOUNT_SETTINGS_SECTION_TARGET,
68
116
  EMPTY_ACCOUNT_SETTINGS_SECTIONS,
69
117
  RESERVED_ACCOUNT_SETTINGS_SECTION_VALUES,
70
118
  normalizeAccountSettingsSectionEntry,
@@ -1,12 +1,12 @@
1
1
  <script setup>
2
2
  import ShellOutletMenuWidget from "@jskit-ai/shell-web/client/components/ShellOutletMenuWidget";
3
- import { HOME_TOOLS_OUTLET } from "../../shared/toolsOutletContracts.js";
3
+ import { HOME_COG_OUTLET } from "../../shared/toolsOutletContracts.js";
4
4
  </script>
5
5
 
6
6
  <template>
7
7
  <ShellOutletMenuWidget
8
- :target="HOME_TOOLS_OUTLET.target"
9
- :default-link-component-token="HOME_TOOLS_OUTLET.defaultLinkComponentToken"
10
- :aria-label="HOME_TOOLS_OUTLET.ariaLabel"
8
+ :target="HOME_COG_OUTLET.target"
9
+ :default-link-component-token="HOME_COG_OUTLET.defaultLinkComponentToken"
10
+ :aria-label="HOME_COG_OUTLET.ariaLabel"
11
11
  />
12
12
  </template>
@@ -1,6 +1,5 @@
1
1
  import UsersHomeToolsWidget from "../components/UsersHomeToolsWidget.vue";
2
2
  import ProfileClientElement from "../components/ProfileClientElement.vue";
3
- import { bootUsersWebClientProvider } from "./bootUsersWebClientProvider.js";
4
3
 
5
4
  class UsersWebClientProvider {
6
5
  static id = "users.web.client";
@@ -14,10 +13,6 @@ class UsersWebClientProvider {
14
13
  app.singleton("users.web.home.tools.widget", () => UsersHomeToolsWidget);
15
14
  app.singleton("users.web.profile.element", () => ProfileClientElement);
16
15
  }
17
-
18
- async boot(app) {
19
- await bootUsersWebClientProvider(app);
20
- }
21
16
  }
22
17
 
23
18
  export { UsersWebClientProvider };
@@ -1,12 +1,12 @@
1
- const DEFAULT_TOOLS_LINK_COMPONENT_TOKEN = "local.main.ui.surface-aware-menu-link-item";
1
+ const DEFAULT_COG_LINK_COMPONENT_TOKEN = "local.main.ui.surface-aware-menu-link-item";
2
2
 
3
- const HOME_TOOLS_OUTLET = Object.freeze({
4
- target: "home-tools:primary-menu",
5
- defaultLinkComponentToken: DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
6
- ariaLabel: "Home tools"
3
+ const HOME_COG_OUTLET = Object.freeze({
4
+ target: "home-cog:primary-menu",
5
+ defaultLinkComponentToken: DEFAULT_COG_LINK_COMPONENT_TOKEN,
6
+ ariaLabel: "Home cog"
7
7
  });
8
8
 
9
9
  export {
10
- DEFAULT_TOOLS_LINK_COMPONENT_TOKEN,
11
- HOME_TOOLS_OUTLET
10
+ DEFAULT_COG_LINK_COMPONENT_TOKEN,
11
+ HOME_COG_OUTLET
12
12
  };
@@ -32,7 +32,7 @@ const sections = computed(() => {
32
32
  }
33
33
  ];
34
34
 
35
- for (const entry of extensionSections) {
35
+ for (const entry of extensionSections.value) {
36
36
  nextSections.push(entry);
37
37
  }
38
38
 
@@ -1,79 +1,92 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
- ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
4
+ ACCOUNT_SETTINGS_SECTION_TARGET,
5
+ normalizeAccountSettingsSectionEntry,
5
6
  resolveAccountSettingsSections
6
7
  } from "../src/client/account-settings/sections.js";
7
- import { bootUsersWebClientProvider } from "../src/client/providers/bootUsersWebClientProvider.js";
8
8
 
9
- test("resolveAccountSettingsSections normalizes, deduplicates, and sorts tagged account section entries", () => {
9
+ test("account settings sections use the standard placement target", () => {
10
+ assert.equal(ACCOUNT_SETTINGS_SECTION_TARGET, "account-settings:sections");
11
+ });
12
+
13
+ test("resolveAccountSettingsSections normalizes, deduplicates, and sorts placement-backed account section entries", () => {
10
14
  const SectionA = {};
11
15
  const SectionB = {};
12
16
 
13
17
  const resolved = resolveAccountSettingsSections([
14
- { value: "notifications", title: "Ignore duplicate", component: SectionA, order: 999 },
15
- { value: "invites", title: "Invites", component: SectionA, order: 400 },
16
- { value: "profile", title: "Broken missing component" },
17
- { value: "security", title: "Security", component: SectionB, order: 350 },
18
- { value: "invites", title: "Second duplicate", component: SectionB, order: 100 }
19
- ]);
20
-
21
- assert.deepEqual(
22
- resolved.map((entry) => ({ value: entry.value, title: entry.title, order: entry.order })),
23
- [
24
- { value: "security", title: "Security", order: 350 },
25
- { value: "invites", title: "Invites", order: 400 }
26
- ]
27
- );
28
- });
29
-
30
- test("bootUsersWebClientProvider provides normalized account section extensions", async () => {
31
- const SectionComponent = {};
32
- const provided = new Map();
33
- let resolvedTagName = "";
34
-
35
- await bootUsersWebClientProvider({
36
- make(token) {
37
- if (token === "jskit.client.vue.app") {
38
- return {
39
- provide(key, value) {
40
- provided.set(key, value);
41
- }
42
- };
18
+ {
19
+ id: "users.notifications.duplicate",
20
+ order: 999,
21
+ component: SectionA,
22
+ props: {
23
+ value: "notifications",
24
+ title: "Ignore duplicate"
25
+ }
26
+ },
27
+ {
28
+ id: "workspaces.invites",
29
+ order: 400,
30
+ component: SectionA,
31
+ props: {
32
+ value: "invites",
33
+ title: "Invites"
43
34
  }
44
- throw new Error(`Unexpected token: ${token}`);
45
35
  },
46
- resolveTag(tagName) {
47
- resolvedTagName = tagName;
48
- return [
49
- {
50
- value: "invites",
51
- title: "Invites",
52
- component: SectionComponent,
53
- order: 400,
54
- usesSharedRuntime: false
55
- }
56
- ];
36
+ {
37
+ id: "invalid.missing-component",
38
+ order: 250,
39
+ props: {
40
+ value: "profile",
41
+ title: "Broken missing component"
42
+ }
43
+ },
44
+ {
45
+ id: "security.section",
46
+ order: 350,
47
+ component: SectionB,
48
+ props: {
49
+ value: "security",
50
+ title: "Security",
51
+ usesSharedRuntime: true
52
+ }
53
+ },
54
+ {
55
+ id: "workspaces.invites.duplicate",
56
+ order: 100,
57
+ component: SectionB,
58
+ props: {
59
+ value: "invites",
60
+ title: "Second duplicate"
61
+ }
57
62
  }
58
- });
63
+ ]);
59
64
 
60
- assert.equal(resolvedTagName, ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG);
61
- const [providedSections] = [...provided.values()];
62
- assert.equal(Array.isArray(providedSections), true);
63
65
  assert.deepEqual(
64
- providedSections.map((entry) => ({
66
+ resolved.map((entry) => ({
65
67
  value: entry.value,
66
68
  title: entry.title,
67
69
  order: entry.order,
68
70
  usesSharedRuntime: entry.usesSharedRuntime
69
71
  })),
70
72
  [
71
- {
72
- value: "invites",
73
- title: "Invites",
74
- order: 400,
75
- usesSharedRuntime: false
76
- }
73
+ { value: "security", title: "Security", order: 350, usesSharedRuntime: true },
74
+ { value: "invites", title: "Invites", order: 400, usesSharedRuntime: false }
77
75
  ]
78
76
  );
79
77
  });
78
+
79
+ test("normalizeAccountSettingsSectionEntry rejects malformed placement entries", () => {
80
+ assert.equal(normalizeAccountSettingsSectionEntry(null), null);
81
+ assert.equal(normalizeAccountSettingsSectionEntry({}), null);
82
+ assert.equal(
83
+ normalizeAccountSettingsSectionEntry({
84
+ component: {},
85
+ props: {
86
+ value: "",
87
+ title: "Broken"
88
+ }
89
+ }),
90
+ null
91
+ );
92
+ });
@@ -71,27 +71,37 @@ function expectTextMutation(id, { reason = "", category = "", skipIfContains = "
71
71
  }
72
72
  }
73
73
 
74
- test("users-web home tools widget exposes home-tools outlet", async () => {
74
+ test("users-web home tools widget exposes home-cog outlet", async () => {
75
75
  const source = await readFile(path.join(PACKAGE_DIR, "src", "client", "components", "UsersHomeToolsWidget.vue"), "utf8");
76
76
 
77
- assert.match(source, /import \{ HOME_TOOLS_OUTLET \} from "\.\.\/\.\.\/shared\/toolsOutletContracts\.js";/);
77
+ assert.match(source, /import \{ HOME_COG_OUTLET \} from "\.\.\/\.\.\/shared\/toolsOutletContracts\.js";/);
78
78
  assert.match(source, /<ShellOutletMenuWidget/);
79
- assert.match(source, /:target="HOME_TOOLS_OUTLET\.target"/);
80
- assert.match(source, /:default-link-component-token="HOME_TOOLS_OUTLET\.defaultLinkComponentToken"/);
79
+ assert.match(source, /:target="HOME_COG_OUTLET\.target"/);
80
+ assert.match(source, /:default-link-component-token="HOME_COG_OUTLET\.defaultLinkComponentToken"/);
81
81
  });
82
82
 
83
- test("users-web descriptor metadata advertises home tools outlet and standard home settings placements", () => {
83
+ test("users-web descriptor metadata advertises home cog outlet and standard home settings placements", () => {
84
84
  assert.deepEqual(
85
- readOutlets("home-tools:primary-menu"),
85
+ readOutlets("home-cog:primary-menu"),
86
86
  [
87
87
  {
88
- target: "home-tools:primary-menu",
88
+ target: "home-cog:primary-menu",
89
89
  defaultLinkComponentToken: "local.main.ui.surface-aware-menu-link-item",
90
90
  surfaces: ["home"],
91
91
  source: "src/client/components/UsersHomeToolsWidget.vue"
92
92
  }
93
93
  ]
94
94
  );
95
+ assert.deepEqual(
96
+ readOutlets("account-settings:sections"),
97
+ [
98
+ {
99
+ target: "account-settings:sections",
100
+ surfaces: ["account"],
101
+ source: "templates/src/components/account/settings/AccountSettingsClientElement.vue"
102
+ }
103
+ ]
104
+ );
95
105
 
96
106
  expectContribution("users.profile.menu.settings", {
97
107
  target: "auth-profile-menu:primary-menu",
@@ -112,7 +122,7 @@ test("users-web descriptor metadata advertises home tools outlet and standard ho
112
122
  });
113
123
 
114
124
  expectContribution("users.home.menu.settings", {
115
- target: "home-tools:primary-menu",
125
+ target: "home-cog:primary-menu",
116
126
  surfaces: ["home"],
117
127
  order: 100,
118
128
  componentToken: "local.main.ui.surface-aware-menu-link-item",
@@ -129,7 +139,7 @@ test("users-web descriptor metadata advertises home tools outlet and standard ho
129
139
  'id: "users.home.tools.widget"',
130
140
  'componentToken: "users.web.home.tools.widget"',
131
141
  'id: "users.home.menu.settings"',
132
- 'target: "home-tools:primary-menu"',
142
+ 'target: "home-cog:primary-menu"',
133
143
  'componentToken: "local.main.ui.surface-aware-menu-link-item"',
134
144
  'scopedSuffix: "/settings"',
135
145
  'unscopedSuffix: "/settings"'
@@ -1,28 +0,0 @@
1
- import {
2
- ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
3
- ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG,
4
- resolveAccountSettingsSections
5
- } from "../account-settings/sections.js";
6
-
7
- async function bootUsersWebClientProvider(app) {
8
- if (!app || typeof app.make !== "function") {
9
- throw new Error("bootUsersWebClientProvider requires application make().");
10
- }
11
-
12
- const vueApp = app.make("jskit.client.vue.app");
13
- if (!vueApp || typeof vueApp.provide !== "function") {
14
- throw new Error("bootUsersWebClientProvider requires jskit.client.vue.app provide().");
15
- }
16
-
17
- const extensionSections =
18
- typeof app.resolveTag === "function"
19
- ? app.resolveTag(ACCOUNT_SETTINGS_SECTION_REGISTRY_TAG)
20
- : [];
21
-
22
- vueApp.provide(
23
- ACCOUNT_SETTINGS_SECTIONS_INJECTION_KEY,
24
- resolveAccountSettingsSections(extensionSections)
25
- );
26
- }
27
-
28
- export { bootUsersWebClientProvider };