@jskit-ai/shell-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 (41) hide show
  1. package/package.descriptor.mjs +165 -0
  2. package/package.json +23 -0
  3. package/src/client/components/ShellErrorHost.vue +208 -0
  4. package/src/client/components/ShellLayout.vue +191 -0
  5. package/src/client/components/ShellOutlet.vue +95 -0
  6. package/src/client/components/useShellLayout.js +93 -0
  7. package/src/client/error/index.js +2 -0
  8. package/src/client/error/inject.js +142 -0
  9. package/src/client/error/normalize.js +75 -0
  10. package/src/client/error/policy.js +50 -0
  11. package/src/client/error/presenters.js +89 -0
  12. package/src/client/error/runtime.js +418 -0
  13. package/src/client/error/store.js +176 -0
  14. package/src/client/error/tokens.js +14 -0
  15. package/src/client/index.js +17 -0
  16. package/src/client/navigation/linkResolver.js +117 -0
  17. package/src/client/placement/debug.js +52 -0
  18. package/src/client/placement/index.js +26 -0
  19. package/src/client/placement/inject.js +104 -0
  20. package/src/client/placement/pathname.js +14 -0
  21. package/src/client/placement/registry.js +41 -0
  22. package/src/client/placement/runtime.js +435 -0
  23. package/src/client/placement/surfaceContext.js +290 -0
  24. package/src/client/placement/tokens.js +29 -0
  25. package/src/client/placement/validators.js +210 -0
  26. package/src/client/providers/ShellWebClientProvider.js +352 -0
  27. package/templates/src/App.vue +11 -0
  28. package/templates/src/components/ShellLayout.vue +247 -0
  29. package/templates/src/error.js +13 -0
  30. package/templates/src/pages/console/index.vue +24 -0
  31. package/templates/src/pages/console.vue +20 -0
  32. package/templates/src/pages/home/index.vue +54 -0
  33. package/templates/src/pages/home.vue +20 -0
  34. package/templates/src/placement.js +12 -0
  35. package/test/errorRuntime.test.js +191 -0
  36. package/test/errorStore.test.js +26 -0
  37. package/test/linkResolver.test.js +112 -0
  38. package/test/placementRegistry.test.js +45 -0
  39. package/test/placementRuntime.test.js +374 -0
  40. package/test/provider.test.js +163 -0
  41. package/test/surfaceContext.test.js +184 -0
@@ -0,0 +1,93 @@
1
+ import { computed, ref } from "vue";
2
+ import { normalizeObject } from "@jskit-ai/kernel/shared/support/normalize";
3
+
4
+ const DEFAULT_ACTION_FALLBACK = Object.freeze({
5
+ label: "",
6
+ to: "",
7
+ variant: "text",
8
+ color: "secondary"
9
+ });
10
+
11
+ const DEFAULT_MENU_FALLBACK = Object.freeze({
12
+ label: "",
13
+ to: "/",
14
+ icon: "$menu"
15
+ });
16
+
17
+ function normalizeAction(action, fallback) {
18
+ const source = normalizeObject(action);
19
+ const fallbackSource = normalizeObject(fallback);
20
+ const label = String(source.label || fallbackSource.label || "").trim();
21
+ if (!label) {
22
+ return null;
23
+ }
24
+
25
+ return {
26
+ label,
27
+ to: String(source.to || fallbackSource.to || "").trim(),
28
+ variant: String(source.variant || fallbackSource.variant || "text").trim(),
29
+ color: String(source.color || fallbackSource.color || "secondary").trim()
30
+ };
31
+ }
32
+
33
+ function normalizeMenuItem(item, fallback) {
34
+ const source = normalizeObject(item);
35
+ const fallbackSource = normalizeObject(fallback);
36
+ const label = String(source.label || fallbackSource.label || "").trim();
37
+ if (!label) {
38
+ return null;
39
+ }
40
+
41
+ return {
42
+ label,
43
+ to: String(source.to || fallbackSource.to || "").trim() || "/",
44
+ icon: String(source.icon || fallbackSource.icon || "$menu").trim() || "$menu"
45
+ };
46
+ }
47
+
48
+ function normalizeActionList(actions) {
49
+ const source = Array.isArray(actions) ? actions : [];
50
+ return source
51
+ .map((item) => normalizeAction(item, DEFAULT_ACTION_FALLBACK))
52
+ .filter(Boolean);
53
+ }
54
+
55
+ function normalizeMenuList(items) {
56
+ const source = Array.isArray(items) ? items : [];
57
+ return source
58
+ .map((item) => normalizeMenuItem(item, DEFAULT_MENU_FALLBACK))
59
+ .filter(Boolean);
60
+ }
61
+
62
+ function useShellLayout({ topLeftActions, topRightActions, menuItems } = {}) {
63
+ const drawerOpen = ref(true);
64
+
65
+ const resolvedTopLeftActions = computed(() => {
66
+ const source = topLeftActions?.value;
67
+ return normalizeActionList(source);
68
+ });
69
+
70
+ const resolvedTopRightActions = computed(() => {
71
+ const source = topRightActions?.value;
72
+ return normalizeActionList(source);
73
+ });
74
+
75
+ const resolvedMenuItems = computed(() => {
76
+ const source = menuItems?.value;
77
+ return normalizeMenuList(source);
78
+ });
79
+
80
+ function toggleDrawer() {
81
+ drawerOpen.value = !drawerOpen.value;
82
+ }
83
+
84
+ return {
85
+ drawerOpen,
86
+ resolvedTopLeftActions,
87
+ resolvedTopRightActions,
88
+ resolvedMenuItems,
89
+ toggleDrawer
90
+ };
91
+ }
92
+
93
+ export { useShellLayout };
@@ -0,0 +1,2 @@
1
+ export { createDefaultErrorPolicy } from "./policy.js";
2
+ export { useShellWebErrorRuntime } from "./inject.js";
@@ -0,0 +1,142 @@
1
+ import {
2
+ inject,
3
+ onBeforeUnmount,
4
+ onMounted,
5
+ shallowRef
6
+ } from "vue";
7
+ import {
8
+ SHELL_WEB_ERROR_RUNTIME_INJECTION_KEY,
9
+ SHELL_WEB_ERROR_PRESENTATION_STORE_INJECTION_KEY
10
+ } from "./tokens.js";
11
+
12
+ const EMPTY_PRESENTATION_STATE = Object.freeze({
13
+ revision: 0,
14
+ channels: Object.freeze({
15
+ snackbar: Object.freeze([]),
16
+ banner: Object.freeze([]),
17
+ dialog: Object.freeze([])
18
+ })
19
+ });
20
+
21
+ const EMPTY_ERROR_RUNTIME = Object.freeze({
22
+ report() {
23
+ return Object.freeze({
24
+ skipped: true,
25
+ reason: "unavailable"
26
+ });
27
+ },
28
+ dismiss() {
29
+ return 0;
30
+ },
31
+ configure() {
32
+ return Object.freeze({
33
+ presenterIds: Object.freeze([]),
34
+ appDefaultPresenterId: "",
35
+ moduleDefaultPresenterId: "",
36
+ resolvedDefaultPresenterId: ""
37
+ });
38
+ },
39
+ registerPresenter() {
40
+ throw new Error("Shell web error runtime is not available.");
41
+ },
42
+ registerPresenters() {
43
+ throw new Error("Shell web error runtime is not available.");
44
+ },
45
+ setPolicy() {
46
+ throw new Error("Shell web error runtime is not available.");
47
+ },
48
+ setAppDefaultPresenterId() {
49
+ throw new Error("Shell web error runtime is not available.");
50
+ },
51
+ assertBootReady() {
52
+ throw new Error("Shell web error runtime is not available.");
53
+ },
54
+ getSnapshot() {
55
+ return Object.freeze({
56
+ presenterIds: Object.freeze([]),
57
+ appDefaultPresenterId: "",
58
+ moduleDefaultPresenterId: "",
59
+ resolvedDefaultPresenterId: ""
60
+ });
61
+ },
62
+ subscribe() {
63
+ return () => {};
64
+ }
65
+ });
66
+
67
+ const EMPTY_PRESENTATION_STORE = Object.freeze({
68
+ getState() {
69
+ return EMPTY_PRESENTATION_STATE;
70
+ },
71
+ subscribe() {
72
+ return () => {};
73
+ },
74
+ present() {
75
+ throw new Error("Shell web error presentation store is not available.");
76
+ },
77
+ dismiss() {
78
+ return 0;
79
+ },
80
+ clear() {
81
+ return 0;
82
+ }
83
+ });
84
+
85
+ function useShellWebErrorRuntime({ required = false } = {}) {
86
+ const runtime = inject(SHELL_WEB_ERROR_RUNTIME_INJECTION_KEY, null);
87
+ if (runtime && typeof runtime.report === "function") {
88
+ return runtime;
89
+ }
90
+
91
+ if (required) {
92
+ throw new Error("Shell web error runtime is not available in Vue injection context.");
93
+ }
94
+
95
+ return EMPTY_ERROR_RUNTIME;
96
+ }
97
+
98
+ function useShellWebErrorPresentationStore({ required = false } = {}) {
99
+ const store = inject(SHELL_WEB_ERROR_PRESENTATION_STORE_INJECTION_KEY, null);
100
+ if (store && typeof store.getState === "function" && typeof store.subscribe === "function") {
101
+ return store;
102
+ }
103
+
104
+ if (required) {
105
+ throw new Error("Shell web error presentation store is not available in Vue injection context.");
106
+ }
107
+
108
+ return EMPTY_PRESENTATION_STORE;
109
+ }
110
+
111
+ function useShellWebErrorPresentationState({ required = false } = {}) {
112
+ const store = useShellWebErrorPresentationStore({ required });
113
+ const state = shallowRef(store.getState());
114
+ let unsubscribe = null;
115
+
116
+ onMounted(() => {
117
+ unsubscribe = store.subscribe((nextState) => {
118
+ state.value = nextState;
119
+ });
120
+ });
121
+
122
+ onBeforeUnmount(() => {
123
+ if (typeof unsubscribe === "function") {
124
+ unsubscribe();
125
+ unsubscribe = null;
126
+ }
127
+ });
128
+
129
+ return Object.freeze({
130
+ state,
131
+ store
132
+ });
133
+ }
134
+
135
+ export {
136
+ EMPTY_ERROR_RUNTIME,
137
+ EMPTY_PRESENTATION_STORE,
138
+ EMPTY_PRESENTATION_STATE,
139
+ useShellWebErrorRuntime,
140
+ useShellWebErrorPresentationStore,
141
+ useShellWebErrorPresentationState
142
+ };
@@ -0,0 +1,75 @@
1
+ import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
2
+
3
+ const ERROR_CHANNELS = Object.freeze(["snackbar", "banner", "dialog", "silent"]);
4
+ const ERROR_SEVERITIES = Object.freeze(["info", "success", "warning", "error", "critical"]);
5
+
6
+ function normalizeText(value, fallback = "") {
7
+ const normalized = String(value || "").trim();
8
+ if (normalized) {
9
+ return normalized;
10
+ }
11
+ return String(fallback || "").trim();
12
+ }
13
+
14
+ function normalizeChannel(value, fallback = "") {
15
+ const normalized = normalizeText(value).toLowerCase();
16
+ if (ERROR_CHANNELS.includes(normalized)) {
17
+ return normalized;
18
+ }
19
+ return normalizeText(fallback).toLowerCase();
20
+ }
21
+
22
+ function normalizeSeverity(value, fallback = "error") {
23
+ const normalized = normalizeText(value).toLowerCase();
24
+ if (ERROR_SEVERITIES.includes(normalized)) {
25
+ return normalized;
26
+ }
27
+
28
+ const normalizedFallback = normalizeText(fallback).toLowerCase();
29
+ if (ERROR_SEVERITIES.includes(normalizedFallback)) {
30
+ return normalizedFallback;
31
+ }
32
+
33
+ return "error";
34
+ }
35
+
36
+ function normalizeNonNegativeInteger(value, fallback = 0) {
37
+ const numericValue = Number(value);
38
+ if (!Number.isFinite(numericValue)) {
39
+ return Math.max(0, Number(fallback || 0));
40
+ }
41
+ if (numericValue < 0) {
42
+ return 0;
43
+ }
44
+ return Math.trunc(numericValue);
45
+ }
46
+
47
+ function normalizeAction(value) {
48
+ const source = isRecord(value) ? value : null;
49
+ if (!source) {
50
+ return null;
51
+ }
52
+
53
+ const label = normalizeText(source.label);
54
+ const handler = typeof source.handler === "function" ? source.handler : null;
55
+ if (!label || !handler) {
56
+ return null;
57
+ }
58
+
59
+ return Object.freeze({
60
+ label,
61
+ handler,
62
+ dismissOnRun: source.dismissOnRun !== false
63
+ });
64
+ }
65
+
66
+ export {
67
+ ERROR_CHANNELS,
68
+ ERROR_SEVERITIES,
69
+ isRecord,
70
+ normalizeText,
71
+ normalizeChannel,
72
+ normalizeSeverity,
73
+ normalizeNonNegativeInteger,
74
+ normalizeAction
75
+ };
@@ -0,0 +1,50 @@
1
+ import {
2
+ normalizeAction,
3
+ normalizeChannel,
4
+ normalizeSeverity,
5
+ normalizeText
6
+ } from "./normalize.js";
7
+
8
+ function createDefaultErrorPolicy({
9
+ defaultChannel = "snackbar",
10
+ unauthorizedChannel = "banner",
11
+ serverErrorChannel = "dialog",
12
+ defaultSeverity = "error"
13
+ } = {}) {
14
+ const normalizedDefaultChannel = normalizeChannel(defaultChannel, "snackbar") || "snackbar";
15
+ const normalizedUnauthorizedChannel = normalizeChannel(unauthorizedChannel, "banner") || "banner";
16
+ const normalizedServerErrorChannel = normalizeChannel(serverErrorChannel, "dialog") || "dialog";
17
+ const normalizedDefaultSeverity = normalizeSeverity(defaultSeverity, "error");
18
+
19
+ return function defaultErrorPolicy(event = {}) {
20
+ const status = Number(event.status || 0);
21
+ const explicitChannel = normalizeChannel(event.channel);
22
+
23
+ let channel = explicitChannel;
24
+ if (!channel) {
25
+ if (event.blocking === true || status >= 500) {
26
+ channel = normalizedServerErrorChannel;
27
+ } else if (status === 401 || status === 403) {
28
+ channel = normalizedUnauthorizedChannel;
29
+ } else {
30
+ channel = normalizedDefaultChannel;
31
+ }
32
+ }
33
+
34
+ const message = normalizeText(event.userMessage || event.message, "Request failed.");
35
+
36
+ return Object.freeze({
37
+ channel,
38
+ message,
39
+ severity: normalizeSeverity(event.severity, normalizedDefaultSeverity),
40
+ presenterId: normalizeText(event.presenterId),
41
+ action: normalizeAction(event.action),
42
+ persist: channel !== "snackbar",
43
+ dedupeKey: normalizeText(event.dedupeKey)
44
+ });
45
+ };
46
+ }
47
+
48
+ export {
49
+ createDefaultErrorPolicy
50
+ };
@@ -0,0 +1,89 @@
1
+ import { normalizeText } from "./normalize.js";
2
+
3
+ const MATERIAL_SNACKBAR_PRESENTER_ID = "material.snackbar";
4
+ const MATERIAL_BANNER_PRESENTER_ID = "material.banner";
5
+ const MATERIAL_DIALOG_PRESENTER_ID = "material.dialog";
6
+ const MODULE_DEFAULT_PRESENTER_ID = MATERIAL_SNACKBAR_PRESENTER_ID;
7
+
8
+ function createStoreBackedPresenter({
9
+ id,
10
+ channel,
11
+ store,
12
+ defaultPersist = false
13
+ } = {}) {
14
+ const normalizedId = normalizeText(id);
15
+ const normalizedChannel = normalizeText(channel).toLowerCase();
16
+ if (!normalizedId) {
17
+ throw new Error("createStoreBackedPresenter requires id.");
18
+ }
19
+ if (!normalizedChannel) {
20
+ throw new Error(`createStoreBackedPresenter("${normalizedId}") requires channel.`);
21
+ }
22
+ if (!store || typeof store.present !== "function" || typeof store.dismiss !== "function") {
23
+ throw new Error(`createStoreBackedPresenter("${normalizedId}") requires a presentation store.`);
24
+ }
25
+
26
+ return Object.freeze({
27
+ id: normalizedId,
28
+ supports(requestedChannel = "") {
29
+ return String(requestedChannel || "").trim().toLowerCase() === normalizedChannel;
30
+ },
31
+ present(payload = {}) {
32
+ return store.present(normalizedChannel, {
33
+ ...payload,
34
+ persist: typeof payload.persist === "boolean" ? payload.persist : defaultPersist,
35
+ presenterId: normalizedId
36
+ });
37
+ },
38
+ dismiss(presentationId = "") {
39
+ return store.dismiss(normalizedChannel, String(presentationId || ""));
40
+ }
41
+ });
42
+ }
43
+
44
+ function createMaterialSnackbarPresenter({ store } = {}) {
45
+ return createStoreBackedPresenter({
46
+ id: MATERIAL_SNACKBAR_PRESENTER_ID,
47
+ channel: "snackbar",
48
+ store,
49
+ defaultPersist: false
50
+ });
51
+ }
52
+
53
+ function createMaterialBannerPresenter({ store } = {}) {
54
+ return createStoreBackedPresenter({
55
+ id: MATERIAL_BANNER_PRESENTER_ID,
56
+ channel: "banner",
57
+ store,
58
+ defaultPersist: true
59
+ });
60
+ }
61
+
62
+ function createMaterialDialogPresenter({ store } = {}) {
63
+ return createStoreBackedPresenter({
64
+ id: MATERIAL_DIALOG_PRESENTER_ID,
65
+ channel: "dialog",
66
+ store,
67
+ defaultPersist: true
68
+ });
69
+ }
70
+
71
+ function createDefaultMaterialErrorPresenters({ store } = {}) {
72
+ return Object.freeze([
73
+ createMaterialSnackbarPresenter({ store }),
74
+ createMaterialBannerPresenter({ store }),
75
+ createMaterialDialogPresenter({ store })
76
+ ]);
77
+ }
78
+
79
+ export {
80
+ MATERIAL_SNACKBAR_PRESENTER_ID,
81
+ MATERIAL_BANNER_PRESENTER_ID,
82
+ MATERIAL_DIALOG_PRESENTER_ID,
83
+ MODULE_DEFAULT_PRESENTER_ID,
84
+ createStoreBackedPresenter,
85
+ createMaterialSnackbarPresenter,
86
+ createMaterialBannerPresenter,
87
+ createMaterialDialogPresenter,
88
+ createDefaultMaterialErrorPresenters
89
+ };