@jskit-ai/users-web 0.1.4

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 (97) hide show
  1. package/package.descriptor.mjs +507 -0
  2. package/package.json +31 -0
  3. package/src/client/components/ConsoleSettingsClientElement.vue +24 -0
  4. package/src/client/components/MembersAdminClientElement.vue +404 -0
  5. package/src/client/components/ProfileClientElement.vue +242 -0
  6. package/src/client/components/UsersProfileSurfaceSwitchMenuItem.vue +39 -0
  7. package/src/client/components/UsersShellMenuLinkItem.vue +140 -0
  8. package/src/client/components/UsersSurfaceAwareMenuLinkItem.vue +87 -0
  9. package/src/client/components/UsersWorkspaceMembersMenuItem.vue +36 -0
  10. package/src/client/components/UsersWorkspacePermissionMenuItem.vue +90 -0
  11. package/src/client/components/UsersWorkspaceSelector.vue +237 -0
  12. package/src/client/components/UsersWorkspaceSettingsMenuItem.vue +39 -0
  13. package/src/client/components/UsersWorkspaceToolsWidget.vue +23 -0
  14. package/src/client/components/WorkspaceMembersClientElement.vue +663 -0
  15. package/src/client/components/WorkspaceSettingsClientElement.vue +230 -0
  16. package/src/client/components/WorkspacesClientElement.vue +514 -0
  17. package/src/client/composables/accountSettingsAvatarUploadRuntime.js +241 -0
  18. package/src/client/composables/accountSettingsInvitesRuntime.js +88 -0
  19. package/src/client/composables/accountSettingsRuntimeConstants.js +77 -0
  20. package/src/client/composables/accountSettingsRuntimeHelpers.js +75 -0
  21. package/src/client/composables/errorMessageHelpers.js +66 -0
  22. package/src/client/composables/internal/useOperationScope.js +144 -0
  23. package/src/client/composables/modelStateHelpers.js +49 -0
  24. package/src/client/composables/operationUiHelpers.js +121 -0
  25. package/src/client/composables/operationValidationHelpers.js +52 -0
  26. package/src/client/composables/refValueHelpers.js +19 -0
  27. package/src/client/composables/scopeHelpers.js +145 -0
  28. package/src/client/composables/useAccess.js +109 -0
  29. package/src/client/composables/useAccountSettingsRuntime.js +533 -0
  30. package/src/client/composables/useAddEdit.js +135 -0
  31. package/src/client/composables/useAddEditCore.js +137 -0
  32. package/src/client/composables/useBootstrapQuery.js +52 -0
  33. package/src/client/composables/useCommand.js +112 -0
  34. package/src/client/composables/useCommandCore.js +130 -0
  35. package/src/client/composables/useEndpointResource.js +104 -0
  36. package/src/client/composables/useFieldErrorBag.js +61 -0
  37. package/src/client/composables/useList.js +85 -0
  38. package/src/client/composables/useListCore.js +65 -0
  39. package/src/client/composables/usePagedCollection.js +125 -0
  40. package/src/client/composables/usePaths.js +108 -0
  41. package/src/client/composables/useRealtimeQueryInvalidation.js +105 -0
  42. package/src/client/composables/useScopeRuntime.js +107 -0
  43. package/src/client/composables/useSurfaceRouteContext.js +31 -0
  44. package/src/client/composables/useUiFeedback.js +96 -0
  45. package/src/client/composables/useView.js +89 -0
  46. package/src/client/composables/useViewCore.js +104 -0
  47. package/src/client/composables/useWorkspaceRouteContext.js +28 -0
  48. package/src/client/composables/useWorkspaceSurfaceId.js +43 -0
  49. package/src/client/index.js +7 -0
  50. package/src/client/lib/bootstrap.js +95 -0
  51. package/src/client/lib/httpClient.js +67 -0
  52. package/src/client/lib/menuIcons.js +192 -0
  53. package/src/client/lib/permissions.js +34 -0
  54. package/src/client/lib/profileSurfaceMenuLinks.js +142 -0
  55. package/src/client/lib/surfaceAccessPolicy.js +350 -0
  56. package/src/client/lib/theme.js +99 -0
  57. package/src/client/lib/workspaceLinkResolver.js +207 -0
  58. package/src/client/lib/workspaceSurfaceContext.js +82 -0
  59. package/src/client/lib/workspaceSurfacePaths.js +163 -0
  60. package/src/client/providers/UsersWebClientProvider.js +85 -0
  61. package/src/client/runtime/bootstrapPlacementRouteGuards.js +371 -0
  62. package/src/client/runtime/bootstrapPlacementRuntime.js +413 -0
  63. package/src/client/runtime/bootstrapPlacementRuntimeConstants.js +32 -0
  64. package/src/client/runtime/bootstrapPlacementRuntimeHelpers.js +157 -0
  65. package/src/client/support/contractGuards.js +34 -0
  66. package/src/client/support/realtimeWorkspace.js +12 -0
  67. package/src/client/support/runtimeNormalization.js +27 -0
  68. package/src/client/support/workspaceQueryKeys.js +15 -0
  69. package/templates/packages/main/src/client/components/AccountPendingInvitesCue.vue +162 -0
  70. package/templates/src/components/WorkspaceNotFoundCard.vue +33 -0
  71. package/templates/src/components/account/settings/AccountSettingsClientElement.vue +153 -0
  72. package/templates/src/components/account/settings/AccountSettingsInvitesSection.vue +77 -0
  73. package/templates/src/components/account/settings/AccountSettingsNotificationsSection.vue +55 -0
  74. package/templates/src/components/account/settings/AccountSettingsPreferencesSection.vue +125 -0
  75. package/templates/src/components/account/settings/AccountSettingsProfileSection.vue +94 -0
  76. package/templates/src/composables/useWorkspaceNotFoundState.js +48 -0
  77. package/templates/src/pages/account/index.vue +17 -0
  78. package/templates/src/pages/admin/members/index.vue +7 -0
  79. package/templates/src/pages/admin/workspace/settings/index.vue +16 -0
  80. package/templates/src/pages/console/settings/index.vue +16 -0
  81. package/templates/src/surfaces/admin/index.vue +29 -0
  82. package/templates/src/surfaces/admin/root.vue +20 -0
  83. package/templates/src/surfaces/app/index.vue +27 -0
  84. package/templates/src/surfaces/app/root.vue +20 -0
  85. package/test/bootstrap.test.js +38 -0
  86. package/test/bootstrapPlacementRuntime.test.js +991 -0
  87. package/test/errorMessageHelpers.test.js +28 -0
  88. package/test/exportsContract.test.js +39 -0
  89. package/test/menuIcons.test.js +33 -0
  90. package/test/permissions.test.js +35 -0
  91. package/test/profileSurfaceMenuLinks.test.js +207 -0
  92. package/test/refValueHelpers.test.js +14 -0
  93. package/test/scopeHelpers.test.js +57 -0
  94. package/test/surfaceAccessPolicy.test.js +129 -0
  95. package/test/theme.test.js +95 -0
  96. package/test/workspaceLinkResolver.test.js +61 -0
  97. package/test/workspaceSurfacePaths.test.js +39 -0
@@ -0,0 +1,89 @@
1
+ import { computed } from "vue";
2
+ import { USERS_ROUTE_VISIBILITY_WORKSPACE } from "@jskit-ai/users-core/shared/support/usersVisibility";
3
+ import { useViewCore } from "./useViewCore.js";
4
+ import { useEndpointResource } from "./useEndpointResource.js";
5
+ import { useOperationScope } from "./internal/useOperationScope.js";
6
+ import { setupOperationErrorReporting } from "./operationUiHelpers.js";
7
+
8
+ function useView({
9
+ ownershipFilter = USERS_ROUTE_VISIBILITY_WORKSPACE,
10
+ surfaceId = "",
11
+ access = "auto",
12
+ apiSuffix = "",
13
+ queryKeyFactory = null,
14
+ viewPermissions = [],
15
+ readMethod = "GET",
16
+ readEnabled = true,
17
+ placementSource = "users-web.view",
18
+ fallbackLoadError = "Unable to load resource.",
19
+ notFoundStatuses = [404],
20
+ notFoundMessage = "Record not found.",
21
+ model,
22
+ mapLoadedToModel,
23
+ realtime = null
24
+ } = {}) {
25
+ const operationScope = useOperationScope({
26
+ ownershipFilter,
27
+ surfaceId,
28
+ access,
29
+ placementSource,
30
+ apiSuffix,
31
+ readEnabled,
32
+ queryKeyFactory,
33
+ permissionSets: {
34
+ view: viewPermissions
35
+ },
36
+ realtime
37
+ });
38
+ const canView = operationScope.permissionGate("view");
39
+
40
+ const resource = useEndpointResource({
41
+ queryKey: operationScope.queryKey,
42
+ path: operationScope.apiPath,
43
+ enabled: operationScope.queryCanRun(canView),
44
+ readMethod,
45
+ fallbackLoadError
46
+ });
47
+
48
+ const view = useViewCore({
49
+ resource,
50
+ model,
51
+ canView,
52
+ mapLoadedToModel,
53
+ notFoundStatuses,
54
+ notFoundMessage
55
+ });
56
+
57
+ const loadError = operationScope.loadError(view.loadError);
58
+ const isLoading = operationScope.isLoading(view.isLoading);
59
+ const isFetching = operationScope.isLoading(view.isFetching);
60
+ const isRefetching = computed(() => Boolean(isFetching.value && !isLoading.value));
61
+ setupOperationErrorReporting({
62
+ source: `${placementSource}.load`,
63
+ loadError,
64
+ notFoundError: view.notFoundError,
65
+ dedupeWindowMs: 0,
66
+ loadActionFactory: () => ({
67
+ label: "Retry",
68
+ dismissOnRun: true,
69
+ handler() {
70
+ void view.refresh();
71
+ }
72
+ })
73
+ });
74
+
75
+ return Object.freeze({
76
+ record: view.record,
77
+ canView,
78
+ isLoading,
79
+ isFetching,
80
+ isRefetching,
81
+ isNotFound: view.isNotFound,
82
+ notFoundError: view.notFoundError,
83
+ loadError,
84
+ refresh: view.refresh,
85
+ resource
86
+ });
87
+ }
88
+
89
+ export { useView };
@@ -0,0 +1,104 @@
1
+ import { computed, watch } from "vue";
2
+ import { captureModelSnapshot, restoreModelSnapshot } from "./modelStateHelpers.js";
3
+
4
+ function normalizeStatusList(value) {
5
+ if (Array.isArray(value)) {
6
+ return value.map((entry) => Number(entry)).filter((entry) => Number.isInteger(entry) && entry > 0);
7
+ }
8
+
9
+ return [404];
10
+ }
11
+
12
+ function useViewCore({
13
+ resource,
14
+ model,
15
+ canView,
16
+ mapLoadedToModel,
17
+ notFoundStatuses = [404],
18
+ notFoundMessage = "Record not found."
19
+ } = {}) {
20
+ const statusList = normalizeStatusList(notFoundStatuses);
21
+ const modelSnapshot = captureModelSnapshot(model);
22
+
23
+ watch(
24
+ () => resource?.query?.isPending?.value,
25
+ (isPending) => {
26
+ if (!isPending || !modelSnapshot) {
27
+ return;
28
+ }
29
+
30
+ restoreModelSnapshot(model, modelSnapshot);
31
+ },
32
+ {
33
+ immediate: true
34
+ }
35
+ );
36
+
37
+ watch(
38
+ () => resource?.data?.value,
39
+ (payload) => {
40
+ if (!payload || typeof mapLoadedToModel !== "function") {
41
+ return;
42
+ }
43
+
44
+ mapLoadedToModel(model, payload, {
45
+ resource
46
+ });
47
+ },
48
+ {
49
+ immediate: true
50
+ }
51
+ );
52
+
53
+ const data = resource?.data;
54
+ const record = computed(() => (model !== undefined ? model : data?.value));
55
+ const isLoading = computed(() => Boolean(resource?.query?.isPending?.value));
56
+ const isFetching = computed(() => Boolean(resource?.query?.isFetching?.value));
57
+ const error = computed(() => resource?.query?.error?.value || null);
58
+
59
+ const isNotFound = computed(() => {
60
+ const status = Number(error.value?.status || 0);
61
+ return statusList.includes(status);
62
+ });
63
+
64
+ const notFoundError = computed(() => {
65
+ if (!isNotFound.value) {
66
+ return "";
67
+ }
68
+
69
+ return String(notFoundMessage || "Record not found.");
70
+ });
71
+
72
+ const loadError = computed(() => {
73
+ if (isNotFound.value) {
74
+ return "";
75
+ }
76
+
77
+ return String(resource?.loadError?.value || "").trim();
78
+ });
79
+
80
+ const resolvedCanView = computed(() => {
81
+ if (canView === undefined) {
82
+ return true;
83
+ }
84
+
85
+ return Boolean(canView?.value);
86
+ });
87
+
88
+ async function refresh() {
89
+ return resource?.reload?.();
90
+ }
91
+
92
+ return Object.freeze({
93
+ record,
94
+ isLoading,
95
+ isFetching,
96
+ isNotFound,
97
+ notFoundError,
98
+ loadError,
99
+ canView: resolvedCanView,
100
+ refresh
101
+ });
102
+ }
103
+
104
+ export { useViewCore };
@@ -0,0 +1,28 @@
1
+ import { computed } from "vue";
2
+ import {
3
+ extractWorkspaceSlugFromSurfacePathname
4
+ } from "../lib/workspaceSurfacePaths.js";
5
+ import { useSurfaceRouteContext } from "./useSurfaceRouteContext.js";
6
+
7
+ function useWorkspaceRouteContext() {
8
+ const { route, routePath, placementContext, mergePlacementContext, currentSurfaceId } = useSurfaceRouteContext();
9
+ const workspaceSlugFromRoute = computed(() => {
10
+ const workspaceSlug = extractWorkspaceSlugFromSurfacePathname(
11
+ placementContext.value,
12
+ currentSurfaceId.value,
13
+ routePath.value
14
+ );
15
+ return String(workspaceSlug || "").trim();
16
+ });
17
+
18
+ return Object.freeze({
19
+ route,
20
+ routePath,
21
+ placementContext,
22
+ mergePlacementContext,
23
+ currentSurfaceId,
24
+ workspaceSlugFromRoute
25
+ });
26
+ }
27
+
28
+ export { useWorkspaceRouteContext };
@@ -0,0 +1,43 @@
1
+ import { computed, unref } from "vue";
2
+ import { resolveSurfaceIdFromPlacementPathname } from "@jskit-ai/shell-web/client/placement";
3
+ import {
4
+ resolveSurfaceSwitchTargetsFromPlacementContext,
5
+ surfaceRequiresWorkspaceFromPlacementContext
6
+ } from "../lib/workspaceSurfaceContext.js";
7
+
8
+ function resolveCurrentPathname(route = null) {
9
+ const routePath = String(route?.path || "").trim();
10
+ if (routePath) {
11
+ return routePath;
12
+ }
13
+
14
+ if (typeof window === "object" && window?.location?.pathname) {
15
+ return String(window.location.pathname);
16
+ }
17
+
18
+ return "/";
19
+ }
20
+
21
+ function useWorkspaceSurfaceId({ route = null, placementContext = null } = {}) {
22
+ const currentSurfaceId = computed(() =>
23
+ resolveSurfaceIdFromPlacementPathname(unref(placementContext), resolveCurrentPathname(route))
24
+ );
25
+
26
+ const workspaceSurfaceId = computed(() => {
27
+ const contextValue = unref(placementContext);
28
+ const surfaceId = String(currentSurfaceId.value || "").trim().toLowerCase();
29
+ if (surfaceId && surfaceRequiresWorkspaceFromPlacementContext(contextValue, surfaceId)) {
30
+ return surfaceId;
31
+ }
32
+
33
+ const targets = resolveSurfaceSwitchTargetsFromPlacementContext(contextValue, surfaceId);
34
+ return String(targets.workspaceSurfaceId || "").trim().toLowerCase();
35
+ });
36
+
37
+ return Object.freeze({
38
+ currentSurfaceId,
39
+ workspaceSurfaceId
40
+ });
41
+ }
42
+
43
+ export { useWorkspaceSurfaceId };
@@ -0,0 +1,7 @@
1
+ import { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
2
+
3
+ export { UsersWebClientProvider } from "./providers/UsersWebClientProvider.js";
4
+
5
+ const clientProviders = Object.freeze([UsersWebClientProvider]);
6
+
7
+ export { clientProviders };
@@ -0,0 +1,95 @@
1
+ function buildBootstrapApiPath(workspaceSlug = "") {
2
+ const normalizedWorkspaceSlug = String(workspaceSlug || "").trim();
3
+ if (!normalizedWorkspaceSlug) {
4
+ return "/api/bootstrap";
5
+ }
6
+
7
+ const query = new URLSearchParams({
8
+ workspaceSlug: normalizedWorkspaceSlug
9
+ });
10
+ return `/api/bootstrap?${query.toString()}`;
11
+ }
12
+
13
+ function normalizeWorkspaceEntry(entry) {
14
+ if (!entry || typeof entry !== "object") {
15
+ return null;
16
+ }
17
+
18
+ const id = Number(entry.id);
19
+ const slug = String(entry.slug || "").trim();
20
+ if (!Number.isInteger(id) || id < 1 || !slug) {
21
+ return null;
22
+ }
23
+
24
+ return Object.freeze({
25
+ id,
26
+ slug,
27
+ name: String(entry.name || slug).trim() || slug,
28
+ color: String(entry.color || "").trim(),
29
+ avatarUrl: String(entry.avatarUrl || "").trim(),
30
+ roleId: String(entry.roleId || "member").trim().toLowerCase() || "member",
31
+ isAccessible: entry.isAccessible !== false
32
+ });
33
+ }
34
+
35
+ function normalizeWorkspaceList(list) {
36
+ const source = Array.isArray(list) ? list : [];
37
+ return source.map(normalizeWorkspaceEntry).filter(Boolean);
38
+ }
39
+
40
+ function findWorkspaceBySlug(list, workspaceSlug) {
41
+ const normalizedWorkspaceSlug = String(workspaceSlug || "").trim();
42
+ if (!normalizedWorkspaceSlug) {
43
+ return null;
44
+ }
45
+
46
+ const source = Array.isArray(list) ? list : [];
47
+ for (const entry of source) {
48
+ const normalizedEntry = normalizeWorkspaceEntry(entry);
49
+ if (normalizedEntry && normalizedEntry.slug === normalizedWorkspaceSlug) {
50
+ return normalizedEntry;
51
+ }
52
+ }
53
+
54
+ return null;
55
+ }
56
+
57
+ function resolvePlacementUserFromBootstrapPayload(payload = {}, currentUser = null) {
58
+ const source = payload && typeof payload === "object" ? payload : {};
59
+ const session = source.session && typeof source.session === "object" ? source.session : {};
60
+ if (session.authenticated !== true) {
61
+ return null;
62
+ }
63
+
64
+ const profile = source.profile && typeof source.profile === "object" ? source.profile : {};
65
+ const profileAvatar = profile.avatar && typeof profile.avatar === "object" ? profile.avatar : {};
66
+ const fallbackUser = currentUser && typeof currentUser === "object" ? currentUser : {};
67
+ const nextUser = {};
68
+
69
+ const userId = Number(session.userId || fallbackUser.id || 0);
70
+ if (Number.isInteger(userId) && userId > 0) {
71
+ nextUser.id = userId;
72
+ }
73
+
74
+ const displayName = String(profile.displayName || fallbackUser.displayName || fallbackUser.name || "").trim();
75
+ if (displayName) {
76
+ nextUser.displayName = displayName;
77
+ nextUser.name = displayName;
78
+ }
79
+
80
+ const email = String(profile.email || fallbackUser.email || "").trim().toLowerCase();
81
+ if (email) {
82
+ nextUser.email = email;
83
+ }
84
+
85
+ nextUser.avatarUrl = String(profileAvatar.effectiveUrl || fallbackUser.avatarUrl || "").trim();
86
+ return Object.freeze(nextUser);
87
+ }
88
+
89
+ export {
90
+ buildBootstrapApiPath,
91
+ normalizeWorkspaceEntry,
92
+ normalizeWorkspaceList,
93
+ findWorkspaceBySlug,
94
+ resolvePlacementUserFromBootstrapPayload
95
+ };
@@ -0,0 +1,67 @@
1
+ import { createHttpClient } from "@jskit-ai/http-runtime/client";
2
+ import {
3
+ isTransientQueryError,
4
+ transientQueryRetryDelay
5
+ } from "@jskit-ai/kernel/shared/support";
6
+
7
+ const SAFE_RETRY_METHODS = Object.freeze(new Set(["GET", "HEAD"]));
8
+ const MAX_TRANSIENT_HTTP_RETRIES = 2;
9
+
10
+ const baseUsersWebHttpClient = createHttpClient({
11
+ credentials: "include",
12
+ csrf: {
13
+ sessionPath: "/api/session"
14
+ }
15
+ });
16
+
17
+ function sleep(delayMs) {
18
+ return new Promise((resolve) => {
19
+ setTimeout(resolve, delayMs);
20
+ });
21
+ }
22
+
23
+ function shouldRetryTransientHttpFailure(error, method, attemptIndex) {
24
+ if (!SAFE_RETRY_METHODS.has(String(method || "GET").toUpperCase())) {
25
+ return false;
26
+ }
27
+ if (!isTransientQueryError(error)) {
28
+ return false;
29
+ }
30
+ return Number(attemptIndex) < MAX_TRANSIENT_HTTP_RETRIES;
31
+ }
32
+
33
+ async function requestWithTransientRetry(executor, method) {
34
+ let attemptIndex = 0;
35
+
36
+ while (true) {
37
+ try {
38
+ return await executor();
39
+ } catch (error) {
40
+ if (!shouldRetryTransientHttpFailure(error, method, attemptIndex)) {
41
+ throw error;
42
+ }
43
+ attemptIndex += 1;
44
+ await sleep(transientQueryRetryDelay(attemptIndex));
45
+ }
46
+ }
47
+ }
48
+
49
+ const usersWebHttpClient = Object.freeze({
50
+ ...baseUsersWebHttpClient,
51
+ request(url, requestOptions = {}, state = null) {
52
+ const method = String(requestOptions?.method || "GET").toUpperCase();
53
+ return requestWithTransientRetry(
54
+ () => baseUsersWebHttpClient.request(url, requestOptions, state),
55
+ method
56
+ );
57
+ },
58
+ requestStream(url, requestOptions = {}, handlers = {}, state = null) {
59
+ const method = String(requestOptions?.method || "GET").toUpperCase();
60
+ return requestWithTransientRetry(
61
+ () => baseUsersWebHttpClient.requestStream(url, requestOptions, handlers, state),
62
+ method
63
+ );
64
+ }
65
+ });
66
+
67
+ export { usersWebHttpClient };
@@ -0,0 +1,192 @@
1
+ import {
2
+ mdiAccountCircleOutline,
3
+ mdiAccountCogOutline,
4
+ mdiAccountGroupOutline,
5
+ mdiArrowRightCircleOutline,
6
+ mdiClipboardListOutline,
7
+ mdiCogOutline,
8
+ mdiConsoleNetworkOutline,
9
+ mdiFolderOutline,
10
+ mdiHomeVariantOutline,
11
+ mdiLogin,
12
+ mdiLogout,
13
+ mdiRobotOutline,
14
+ mdiShieldCrownOutline,
15
+ mdiViewDashboardOutline,
16
+ mdiViewListOutline
17
+ } from "@mdi/js";
18
+ import { isExternalLinkTarget, splitPathQueryHash } from "@jskit-ai/kernel/shared/support/linkPath";
19
+ import { normalizePathname as normalizeKernelPathname } from "@jskit-ai/kernel/shared/surface/paths";
20
+
21
+ const SURFACE_SWITCH_ICON_BY_ID = Object.freeze({
22
+ home: mdiHomeVariantOutline,
23
+ app: mdiViewDashboardOutline,
24
+ admin: mdiShieldCrownOutline,
25
+ console: mdiConsoleNetworkOutline
26
+ });
27
+
28
+ function normalizeText(value) {
29
+ return String(value || "").trim();
30
+ }
31
+
32
+ function normalizePathname(value) {
33
+ const normalizedValue = normalizeText(value);
34
+ if (!normalizedValue) {
35
+ return "";
36
+ }
37
+
38
+ if (isExternalLinkTarget(normalizedValue)) {
39
+ const isHttpTarget = normalizedValue.startsWith("http://") || normalizedValue.startsWith("https://");
40
+ if (!isHttpTarget) {
41
+ return "";
42
+ }
43
+
44
+ let parsedPathname = "";
45
+ try {
46
+ parsedPathname = String(new URL(normalizedValue).pathname || "");
47
+ } catch {
48
+ return "";
49
+ }
50
+
51
+ const normalizedPathname = normalizeText(parsedPathname);
52
+ if (!normalizedPathname) {
53
+ return "";
54
+ }
55
+ return normalizeKernelPathname(normalizedPathname).toLowerCase();
56
+ }
57
+
58
+ const { pathname } = splitPathQueryHash(normalizedValue);
59
+ const normalizedPathname = normalizeText(pathname);
60
+ if (!normalizedPathname) {
61
+ return "";
62
+ }
63
+
64
+ return normalizeKernelPathname(normalizedPathname).toLowerCase();
65
+ }
66
+
67
+ function resolveSurfaceSwitchIdFromLabel(label = "") {
68
+ const normalizedLabel = normalizeText(label).toLowerCase();
69
+ if (!normalizedLabel.startsWith("go to ")) {
70
+ return "";
71
+ }
72
+ return normalizeText(normalizedLabel.slice("go to ".length));
73
+ }
74
+
75
+ function resolveSurfaceSwitchIcon(surfaceId = "", explicitIcon = "") {
76
+ const normalizedExplicitIcon = normalizeText(explicitIcon);
77
+ if (normalizedExplicitIcon) {
78
+ return normalizedExplicitIcon;
79
+ }
80
+
81
+ const normalizedSurfaceId = normalizeText(surfaceId).toLowerCase();
82
+ return SURFACE_SWITCH_ICON_BY_ID[normalizedSurfaceId] || mdiArrowRightCircleOutline;
83
+ }
84
+
85
+ function resolveMenuLinkIcon({ icon = "", label = "", to = "" } = {}) {
86
+ const normalizedIcon = normalizeText(icon);
87
+ if (normalizedIcon) {
88
+ return normalizedIcon;
89
+ }
90
+
91
+ const normalizedLabel = normalizeText(label).toLowerCase();
92
+ const normalizedPathname = normalizePathname(to);
93
+ if (!normalizedLabel && !normalizedPathname) {
94
+ return "";
95
+ }
96
+
97
+ const surfaceSwitchSurfaceId = resolveSurfaceSwitchIdFromLabel(normalizedLabel);
98
+ if (surfaceSwitchSurfaceId) {
99
+ return resolveSurfaceSwitchIcon(surfaceSwitchSurfaceId);
100
+ }
101
+
102
+ if (
103
+ normalizedLabel.includes("sign in") ||
104
+ normalizedPathname.includes("/auth/login")
105
+ ) {
106
+ return mdiLogin;
107
+ }
108
+
109
+ if (
110
+ normalizedLabel.includes("sign out") ||
111
+ normalizedPathname.includes("/auth/signout")
112
+ ) {
113
+ return mdiLogout;
114
+ }
115
+
116
+ if (
117
+ normalizedLabel.includes("account") ||
118
+ normalizedPathname.includes("/account")
119
+ ) {
120
+ if (
121
+ normalizedLabel.includes("settings") ||
122
+ normalizedPathname.includes("/settings") ||
123
+ normalizedPathname === "/account"
124
+ ) {
125
+ return mdiAccountCogOutline;
126
+ }
127
+ return mdiAccountCircleOutline;
128
+ }
129
+
130
+ if (
131
+ normalizedLabel.includes("members") ||
132
+ normalizedLabel.includes("team") ||
133
+ normalizedPathname.includes("/members")
134
+ ) {
135
+ return mdiAccountGroupOutline;
136
+ }
137
+
138
+ if (normalizedLabel.includes("assistant") || normalizedPathname.includes("/assistant")) {
139
+ return mdiRobotOutline;
140
+ }
141
+
142
+ if (
143
+ normalizedLabel.includes("console") ||
144
+ normalizedPathname.startsWith("/console")
145
+ ) {
146
+ return mdiConsoleNetworkOutline;
147
+ }
148
+
149
+ if (
150
+ normalizedLabel.includes("admin") ||
151
+ normalizedPathname.includes("/admin")
152
+ ) {
153
+ return mdiShieldCrownOutline;
154
+ }
155
+
156
+ if (normalizedLabel.includes("settings") || normalizedPathname.includes("/settings")) {
157
+ return mdiCogOutline;
158
+ }
159
+
160
+ if (
161
+ normalizedLabel.includes("home") ||
162
+ normalizedPathname === "/"
163
+ ) {
164
+ return mdiHomeVariantOutline;
165
+ }
166
+
167
+ if (
168
+ normalizedLabel.includes("workspace") ||
169
+ normalizedLabel.includes("dashboard") ||
170
+ normalizedPathname.includes("/w/")
171
+ ) {
172
+ return mdiViewDashboardOutline;
173
+ }
174
+
175
+ if (normalizedPathname) {
176
+ const segments = normalizedPathname.split("/").filter(Boolean);
177
+ if (segments.length === 1) {
178
+ return mdiFolderOutline;
179
+ }
180
+ }
181
+
182
+ if (normalizedLabel.includes("list")) {
183
+ return mdiClipboardListOutline;
184
+ }
185
+
186
+ return mdiViewListOutline;
187
+ }
188
+
189
+ export {
190
+ resolveMenuLinkIcon,
191
+ resolveSurfaceSwitchIcon
192
+ };
@@ -0,0 +1,34 @@
1
+ import {
2
+ hasPermission,
3
+ normalizePermissionList
4
+ } from "@jskit-ai/kernel/shared/support";
5
+
6
+ function toPermissionSet(values) {
7
+ const normalized = normalizePermissionList(values);
8
+ if (normalized.length < 1) {
9
+ return new Set();
10
+ }
11
+ return new Set(normalized);
12
+ }
13
+
14
+ function arePermissionListsEqual(left, right) {
15
+ const leftSet = toPermissionSet(left);
16
+ const rightSet = toPermissionSet(right);
17
+ if (leftSet.size !== rightSet.size) {
18
+ return false;
19
+ }
20
+
21
+ for (const permission of leftSet) {
22
+ if (!rightSet.has(permission)) {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ return true;
28
+ }
29
+
30
+ export {
31
+ normalizePermissionList,
32
+ arePermissionListsEqual,
33
+ hasPermission
34
+ };