@jskit-ai/shell-web 0.1.65 → 0.1.66
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 +74 -9
- package/package.json +8 -7
- package/src/client/components/ShellErrorHost.vue +88 -15
- package/src/client/components/ShellLayout.vue +551 -46
- package/src/client/components/ShellRouteTransition.vue +480 -0
- package/src/client/components/ShellTabLinkItem.vue +22 -6
- package/src/client/composables/useShellLayoutState.js +12 -1
- package/src/client/error/normalize.js +17 -0
- package/src/client/error/policy.js +25 -11
- package/src/client/error/runtime.js +2 -0
- package/src/client/index.js +1 -0
- package/src/client/providers/ShellWebClientProvider.js +163 -39
- package/src/client/stores/useShellLayoutStore.js +21 -1
- package/src/test/adaptiveShellSmoke.js +121 -0
- package/templates/expected-existing/src/pages/home/index.vue +40 -10
- package/templates/src/components/ShellLayout.vue +10 -86
- package/templates/src/components/menus/TabLinkItem.vue +4 -0
- package/templates/src/error.js +7 -1
- package/templates/src/pages/home/index.vue +64 -23
- package/templates/src/pages/home/settings/general/index.vue +12 -9
- package/templates/src/pages/home/settings.vue +68 -21
- package/templates/src/placementTopology.js +43 -2
- package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
- package/test/errorRuntime.test.js +42 -0
- package/test/linkItemScaffoldContract.test.js +9 -2
- package/test/placementRuntime.test.js +37 -0
- package/test/provider.test.js +97 -5
- package/test/settingsPlacementContract.test.js +205 -8
- package/test/useShellLayoutState.test.js +19 -0
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
onBeforeUnmount,
|
|
5
|
+
onMounted,
|
|
6
|
+
ref,
|
|
7
|
+
watch
|
|
8
|
+
} from "vue";
|
|
9
|
+
import { useRoute, useRouter } from "vue-router";
|
|
10
|
+
import { useDisplay } from "vuetify";
|
|
11
|
+
import { normalizePathname } from "@jskit-ai/kernel/shared";
|
|
12
|
+
import { resolveShellLinkPath } from "../navigation/linkResolver.js";
|
|
13
|
+
import {
|
|
14
|
+
useWebPlacementContext,
|
|
15
|
+
useWebPlacementRuntime
|
|
16
|
+
} from "../placement/inject.js";
|
|
17
|
+
import { resolveRuntimePathname } from "../placement/pathname.js";
|
|
18
|
+
import {
|
|
19
|
+
readPlacementSurfaceConfig,
|
|
20
|
+
resolveSurfaceNavigationTargetFromPlacementContext,
|
|
21
|
+
resolveSurfaceIdFromPlacementPathname
|
|
22
|
+
} from "../placement/surfaceContext.js";
|
|
23
|
+
import {
|
|
24
|
+
normalizeMenuLinkPathname,
|
|
25
|
+
resolveMenuLinkTarget
|
|
26
|
+
} from "../support/menuLinkTarget.js";
|
|
27
|
+
|
|
28
|
+
const props = defineProps({
|
|
29
|
+
enabled: {
|
|
30
|
+
type: Boolean,
|
|
31
|
+
default: true
|
|
32
|
+
},
|
|
33
|
+
target: {
|
|
34
|
+
type: String,
|
|
35
|
+
default: "shell-layout:primary-bottom-nav"
|
|
36
|
+
},
|
|
37
|
+
semanticTarget: {
|
|
38
|
+
type: String,
|
|
39
|
+
default: "shell.primary-nav"
|
|
40
|
+
},
|
|
41
|
+
swipeEnabled: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: true
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const route = useRoute();
|
|
48
|
+
const router = useRouter();
|
|
49
|
+
const display = useDisplay();
|
|
50
|
+
const placementRuntime = useWebPlacementRuntime();
|
|
51
|
+
const { context: placementContext } = useWebPlacementContext();
|
|
52
|
+
const revision = ref(
|
|
53
|
+
typeof placementRuntime.getRevision === "function" ? placementRuntime.getRevision() : 0
|
|
54
|
+
);
|
|
55
|
+
const transitionDirection = ref("none");
|
|
56
|
+
let unsubscribe = null;
|
|
57
|
+
let activeSwipe = null;
|
|
58
|
+
let stopSwipeClassWatch = null;
|
|
59
|
+
|
|
60
|
+
const SWIPE_MIN_DISTANCE = 36;
|
|
61
|
+
const SWIPE_MAX_VERTICAL_DRIFT = 120;
|
|
62
|
+
const SWIPE_MIN_VELOCITY = 0.08;
|
|
63
|
+
|
|
64
|
+
onMounted(() => {
|
|
65
|
+
if (typeof placementRuntime.subscribe !== "function") {
|
|
66
|
+
unsubscribe = null;
|
|
67
|
+
} else {
|
|
68
|
+
unsubscribe = placementRuntime.subscribe((event) => {
|
|
69
|
+
const next = Number(event?.revision);
|
|
70
|
+
revision.value = Number.isInteger(next) ? next : revision.value + 1;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (typeof window === "object") {
|
|
75
|
+
window.addEventListener("pointerdown", handlePointerDown, { capture: true, passive: true });
|
|
76
|
+
window.addEventListener("pointermove", handlePointerMove, { capture: true, passive: false });
|
|
77
|
+
window.addEventListener("pointerup", handlePointerEnd, { capture: true, passive: true });
|
|
78
|
+
window.addEventListener("pointercancel", handlePointerCancel, { capture: true, passive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
stopSwipeClassWatch = watch(
|
|
82
|
+
swipeNavigationEnabled,
|
|
83
|
+
(enabled) => {
|
|
84
|
+
if (typeof document !== "object") {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
document.documentElement.classList.toggle("shell-route-swipe-enabled", enabled);
|
|
88
|
+
},
|
|
89
|
+
{ immediate: true }
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
onBeforeUnmount(() => {
|
|
94
|
+
if (typeof unsubscribe === "function") {
|
|
95
|
+
unsubscribe();
|
|
96
|
+
unsubscribe = null;
|
|
97
|
+
}
|
|
98
|
+
if (typeof stopSwipeClassWatch === "function") {
|
|
99
|
+
stopSwipeClassWatch();
|
|
100
|
+
stopSwipeClassWatch = null;
|
|
101
|
+
}
|
|
102
|
+
if (typeof document === "object") {
|
|
103
|
+
document.documentElement.classList.remove("shell-route-swipe-enabled");
|
|
104
|
+
}
|
|
105
|
+
if (typeof window === "object") {
|
|
106
|
+
window.removeEventListener("pointerdown", handlePointerDown, { capture: true });
|
|
107
|
+
window.removeEventListener("pointermove", handlePointerMove, { capture: true });
|
|
108
|
+
window.removeEventListener("pointerup", handlePointerEnd, { capture: true });
|
|
109
|
+
window.removeEventListener("pointercancel", handlePointerCancel, { capture: true });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const resolvedLayoutClass = computed(() => {
|
|
114
|
+
const displayName = String(display?.name?.value || "").trim().toLowerCase();
|
|
115
|
+
if (displayName === "xs" || displayName === "sm") {
|
|
116
|
+
return "compact";
|
|
117
|
+
}
|
|
118
|
+
if (displayName === "md") {
|
|
119
|
+
return "medium";
|
|
120
|
+
}
|
|
121
|
+
return "expanded";
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const currentPathname = computed(() =>
|
|
125
|
+
normalizeComparablePathname(resolveRuntimePathname(route?.path || route?.fullPath || "/"))
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const currentSurfaceId = computed(() => {
|
|
129
|
+
const contextValue = placementContext?.value || null;
|
|
130
|
+
const surfaceFromPathname = resolveSurfaceIdFromPlacementPathname(contextValue, currentPathname.value);
|
|
131
|
+
if (surfaceFromPathname) {
|
|
132
|
+
return surfaceFromPathname;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const surfaceConfig = readPlacementSurfaceConfig(contextValue);
|
|
136
|
+
return surfaceConfig.defaultSurfaceId || "*";
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const primaryNavEntries = computed(() => {
|
|
140
|
+
void revision.value;
|
|
141
|
+
const contextValue = placementContext?.value || null;
|
|
142
|
+
const routeParams = route?.params || {};
|
|
143
|
+
|
|
144
|
+
return placementRuntime
|
|
145
|
+
.getPlacements({
|
|
146
|
+
surface: currentSurfaceId.value,
|
|
147
|
+
target: props.target,
|
|
148
|
+
layoutClass: "compact"
|
|
149
|
+
})
|
|
150
|
+
.filter((entry) => String(entry.semanticTarget || entry.target || "").trim() === props.semanticTarget)
|
|
151
|
+
.map((entry, index) => {
|
|
152
|
+
const entryProps = entry?.props && typeof entry.props === "object" ? entry.props : {};
|
|
153
|
+
const target = resolveMenuLinkTarget({
|
|
154
|
+
to: entryProps.to,
|
|
155
|
+
surface: entryProps.surface,
|
|
156
|
+
currentSurfaceId: currentSurfaceId.value,
|
|
157
|
+
placementContext: contextValue,
|
|
158
|
+
scopedSuffix: entryProps.scopedSuffix,
|
|
159
|
+
unscopedSuffix: entryProps.unscopedSuffix,
|
|
160
|
+
routeParams,
|
|
161
|
+
resolvePagePath(relativePath, options = {}) {
|
|
162
|
+
return resolveShellLinkPath({
|
|
163
|
+
context: contextValue,
|
|
164
|
+
surface: options.surface,
|
|
165
|
+
relativePath,
|
|
166
|
+
params: routeParams,
|
|
167
|
+
strictParams: options.strictParams !== false
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
const navigationTarget = resolveSurfaceNavigationTargetFromPlacementContext(contextValue, {
|
|
172
|
+
path: target,
|
|
173
|
+
surfaceId: entryProps.surface || currentSurfaceId.value
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
return Object.freeze({
|
|
177
|
+
id: entry.id,
|
|
178
|
+
index,
|
|
179
|
+
exact: entryProps.exact === true,
|
|
180
|
+
href: navigationTarget.href,
|
|
181
|
+
pathname: normalizeComparablePathname(navigationTarget.href),
|
|
182
|
+
sameOrigin: navigationTarget.sameOrigin
|
|
183
|
+
});
|
|
184
|
+
})
|
|
185
|
+
.filter((entry) => entry.pathname && entry.sameOrigin);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const activePrimaryNavIndex = computed(() => {
|
|
189
|
+
const pathname = currentPathname.value;
|
|
190
|
+
let bestMatch = null;
|
|
191
|
+
|
|
192
|
+
for (const entry of primaryNavEntries.value) {
|
|
193
|
+
if (!pathMatchesNavigationEntry(pathname, entry)) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!bestMatch || entry.pathname.length > bestMatch.pathname.length) {
|
|
197
|
+
bestMatch = entry;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return bestMatch ? bestMatch.index : -1;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
watch(
|
|
205
|
+
activePrimaryNavIndex,
|
|
206
|
+
(nextIndex, previousIndex) => {
|
|
207
|
+
if (
|
|
208
|
+
!Number.isInteger(previousIndex) ||
|
|
209
|
+
previousIndex < 0 ||
|
|
210
|
+
nextIndex < 0 ||
|
|
211
|
+
nextIndex === previousIndex
|
|
212
|
+
) {
|
|
213
|
+
transitionDirection.value = "none";
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
transitionDirection.value = nextIndex > previousIndex ? "forward" : "reverse";
|
|
218
|
+
},
|
|
219
|
+
{ flush: "sync" }
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const routeTransitionName = computed(() => {
|
|
223
|
+
if (!props.enabled || resolvedLayoutClass.value !== "compact") {
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
if (transitionDirection.value === "forward") {
|
|
227
|
+
return "shell-route-slide-forward";
|
|
228
|
+
}
|
|
229
|
+
if (transitionDirection.value === "reverse") {
|
|
230
|
+
return "shell-route-slide-reverse";
|
|
231
|
+
}
|
|
232
|
+
return "";
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const routeTransitionKey = computed(() => normalizeComparablePathname(route?.path || route?.fullPath || "/") || "/");
|
|
236
|
+
|
|
237
|
+
const swipeNavigationEnabled = computed(() =>
|
|
238
|
+
Boolean(
|
|
239
|
+
props.enabled &&
|
|
240
|
+
props.swipeEnabled &&
|
|
241
|
+
resolvedLayoutClass.value === "compact" &&
|
|
242
|
+
primaryNavEntries.value.length > 1 &&
|
|
243
|
+
activePrimaryNavIndex.value >= 0
|
|
244
|
+
)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
function handlePointerDown(event) {
|
|
248
|
+
if (!swipeNavigationEnabled.value || !isPrimaryPointerEvent(event) || isSwipeIgnoredTarget(event.target)) {
|
|
249
|
+
activeSwipe = null;
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
activeSwipe = {
|
|
254
|
+
pointerId: event.pointerId,
|
|
255
|
+
startX: event.clientX,
|
|
256
|
+
startY: event.clientY,
|
|
257
|
+
startTime: performance.now(),
|
|
258
|
+
cancelled: false
|
|
259
|
+
};
|
|
260
|
+
try {
|
|
261
|
+
event.target?.setPointerCapture?.(event.pointerId);
|
|
262
|
+
} catch {
|
|
263
|
+
// Synthetic test events and some embedded webviews may not expose an active pointer.
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function handlePointerMove(event) {
|
|
268
|
+
if (!activeSwipe || event.pointerId !== activeSwipe.pointerId) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const deltaX = event.clientX - activeSwipe.startX;
|
|
273
|
+
const deltaY = event.clientY - activeSwipe.startY;
|
|
274
|
+
const absX = Math.abs(deltaX);
|
|
275
|
+
const absY = Math.abs(deltaY);
|
|
276
|
+
|
|
277
|
+
if (absY > SWIPE_MAX_VERTICAL_DRIFT || (absY > 28 && absY > absX * 1.3)) {
|
|
278
|
+
activeSwipe.cancelled = true;
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (absX > 10 && absX > absY) {
|
|
283
|
+
event.preventDefault();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (isAcceptedSwipe({ deltaX, deltaY, elapsed: Math.max(performance.now() - activeSwipe.startTime, 1) })) {
|
|
287
|
+
const offset = deltaX < 0 ? 1 : -1;
|
|
288
|
+
activeSwipe = null;
|
|
289
|
+
navigateBySwipe(offset);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function handlePointerEnd(event) {
|
|
294
|
+
if (!activeSwipe || event.pointerId !== activeSwipe.pointerId) {
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const swipe = activeSwipe;
|
|
299
|
+
activeSwipe = null;
|
|
300
|
+
|
|
301
|
+
if (swipe.cancelled || !swipeNavigationEnabled.value) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const deltaX = event.clientX - swipe.startX;
|
|
306
|
+
const deltaY = event.clientY - swipe.startY;
|
|
307
|
+
const elapsed = Math.max(performance.now() - swipe.startTime, 1);
|
|
308
|
+
const absX = Math.abs(deltaX);
|
|
309
|
+
const velocity = absX / elapsed;
|
|
310
|
+
|
|
311
|
+
if (!isAcceptedSwipe({ deltaX, deltaY, elapsed, velocity })) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
navigateBySwipe(deltaX < 0 ? 1 : -1);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function handlePointerCancel(event) {
|
|
319
|
+
if (activeSwipe && event.pointerId === activeSwipe.pointerId) {
|
|
320
|
+
activeSwipe = null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function navigateBySwipe(offset = 0) {
|
|
325
|
+
const currentIndex = activePrimaryNavIndex.value;
|
|
326
|
+
const nextEntry = primaryNavEntries.value[currentIndex + offset];
|
|
327
|
+
if (!nextEntry?.href || normalizeComparablePathname(nextEntry.href) === currentPathname.value) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
void router.push(nextEntry.href).catch(() => {});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isAcceptedSwipe({ deltaX = 0, deltaY = 0, elapsed = 1, velocity = null } = {}) {
|
|
335
|
+
const absX = Math.abs(deltaX);
|
|
336
|
+
const absY = Math.abs(deltaY);
|
|
337
|
+
const resolvedVelocity = velocity == null ? absX / Math.max(elapsed, 1) : velocity;
|
|
338
|
+
return Boolean(
|
|
339
|
+
absX >= SWIPE_MIN_DISTANCE &&
|
|
340
|
+
absY <= SWIPE_MAX_VERTICAL_DRIFT &&
|
|
341
|
+
absX > absY &&
|
|
342
|
+
resolvedVelocity >= SWIPE_MIN_VELOCITY
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function normalizeComparablePathname(value = "") {
|
|
347
|
+
const raw = String(value || "").trim();
|
|
348
|
+
if (!raw) {
|
|
349
|
+
return "";
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const parsed = new URL(raw, window.location.href);
|
|
354
|
+
if (parsed.origin !== window.location.origin) {
|
|
355
|
+
return "";
|
|
356
|
+
}
|
|
357
|
+
return normalizePathname(parsed.pathname);
|
|
358
|
+
} catch {
|
|
359
|
+
return normalizePathname(normalizeMenuLinkPathname(raw));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function pathMatchesNavigationEntry(pathname = "", entry = {}) {
|
|
364
|
+
const target = String(entry.pathname || "").trim();
|
|
365
|
+
if (!target) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (entry.exact || target === "/") {
|
|
369
|
+
return pathname === target;
|
|
370
|
+
}
|
|
371
|
+
return pathname === target || pathname.startsWith(`${target}/`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function isPrimaryPointerEvent(event) {
|
|
375
|
+
return event?.isPrimary !== false && event?.button === 0 && event?.pointerType !== "mouse";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function isSwipeIgnoredTarget(target) {
|
|
379
|
+
return Boolean(
|
|
380
|
+
target?.closest?.(
|
|
381
|
+
[
|
|
382
|
+
"a",
|
|
383
|
+
"button",
|
|
384
|
+
"input",
|
|
385
|
+
"select",
|
|
386
|
+
"textarea",
|
|
387
|
+
"summary",
|
|
388
|
+
"[role='button']",
|
|
389
|
+
"[role='link']",
|
|
390
|
+
"[role='slider']",
|
|
391
|
+
"[contenteditable='true']",
|
|
392
|
+
"[data-shell-swipe-ignore]"
|
|
393
|
+
].join(",")
|
|
394
|
+
)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
</script>
|
|
398
|
+
|
|
399
|
+
<template>
|
|
400
|
+
<div class="shell-route-transition">
|
|
401
|
+
<Transition :name="routeTransitionName" :css="Boolean(routeTransitionName)">
|
|
402
|
+
<div :key="routeTransitionKey" class="shell-route-transition__pane">
|
|
403
|
+
<slot />
|
|
404
|
+
</div>
|
|
405
|
+
</Transition>
|
|
406
|
+
</div>
|
|
407
|
+
</template>
|
|
408
|
+
|
|
409
|
+
<style scoped>
|
|
410
|
+
.shell-route-transition {
|
|
411
|
+
--shell-route-transition-distance: 100%;
|
|
412
|
+
--shell-route-transition-opacity: 1;
|
|
413
|
+
overflow-x: clip;
|
|
414
|
+
position: relative;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
:global(.shell-route-swipe-enabled) {
|
|
418
|
+
touch-action: pan-y;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.shell-route-transition__pane {
|
|
422
|
+
background: rgb(var(--v-theme-background));
|
|
423
|
+
width: 100%;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.shell-route-slide-forward-enter-active,
|
|
427
|
+
.shell-route-slide-forward-leave-active,
|
|
428
|
+
.shell-route-slide-reverse-enter-active,
|
|
429
|
+
.shell-route-slide-reverse-leave-active {
|
|
430
|
+
transition:
|
|
431
|
+
transform 320ms cubic-bezier(0.2, 0, 0, 1),
|
|
432
|
+
opacity 120ms linear;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.shell-route-slide-forward-leave-active,
|
|
436
|
+
.shell-route-slide-reverse-leave-active {
|
|
437
|
+
left: 0;
|
|
438
|
+
pointer-events: none;
|
|
439
|
+
position: absolute;
|
|
440
|
+
right: 0;
|
|
441
|
+
top: 0;
|
|
442
|
+
width: 100%;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.shell-route-slide-forward-enter-from {
|
|
446
|
+
opacity: var(--shell-route-transition-opacity);
|
|
447
|
+
transform: translateX(var(--shell-route-transition-distance));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.shell-route-slide-forward-leave-to {
|
|
451
|
+
opacity: var(--shell-route-transition-opacity);
|
|
452
|
+
transform: translateX(calc(var(--shell-route-transition-distance) * -1));
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.shell-route-slide-reverse-enter-from {
|
|
456
|
+
opacity: var(--shell-route-transition-opacity);
|
|
457
|
+
transform: translateX(calc(var(--shell-route-transition-distance) * -1));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.shell-route-slide-reverse-leave-to {
|
|
461
|
+
opacity: var(--shell-route-transition-opacity);
|
|
462
|
+
transform: translateX(var(--shell-route-transition-distance));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
@media (prefers-reduced-motion: reduce) {
|
|
466
|
+
.shell-route-slide-forward-enter-active,
|
|
467
|
+
.shell-route-slide-forward-leave-active,
|
|
468
|
+
.shell-route-slide-reverse-enter-active,
|
|
469
|
+
.shell-route-slide-reverse-leave-active {
|
|
470
|
+
transition-duration: 1ms;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
.shell-route-slide-forward-enter-from,
|
|
474
|
+
.shell-route-slide-forward-leave-to,
|
|
475
|
+
.shell-route-slide-reverse-enter-from,
|
|
476
|
+
.shell-route-slide-reverse-leave-to {
|
|
477
|
+
transform: none;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
</style>
|
|
@@ -38,6 +38,10 @@ const props = defineProps({
|
|
|
38
38
|
disabled: {
|
|
39
39
|
type: Boolean,
|
|
40
40
|
default: false
|
|
41
|
+
},
|
|
42
|
+
exact: {
|
|
43
|
+
type: Boolean,
|
|
44
|
+
default: false
|
|
41
45
|
}
|
|
42
46
|
});
|
|
43
47
|
|
|
@@ -101,19 +105,31 @@ const resolvedIcon = computed(() =>
|
|
|
101
105
|
</script>
|
|
102
106
|
|
|
103
107
|
<template>
|
|
104
|
-
<v-
|
|
108
|
+
<v-btn
|
|
105
109
|
v-if="resolvedTarget.href"
|
|
106
|
-
class="tab-link-item"
|
|
110
|
+
class="tab-link-item text-none"
|
|
107
111
|
:to="resolvedTarget.sameOrigin ? resolvedTarget.href : undefined"
|
|
108
112
|
:href="resolvedTarget.sameOrigin ? undefined : resolvedTarget.href"
|
|
109
|
-
:
|
|
110
|
-
:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
:disabled="props.disabled"
|
|
114
|
+
:exact="props.exact"
|
|
115
|
+
min-width="72"
|
|
116
|
+
stacked
|
|
117
|
+
>
|
|
118
|
+
<v-icon v-if="resolvedIcon" :icon="resolvedIcon" />
|
|
119
|
+
<span class="tab-link-item__label">{{ props.label || "Tab" }}</span>
|
|
120
|
+
</v-btn>
|
|
113
121
|
</template>
|
|
114
122
|
|
|
115
123
|
<style scoped>
|
|
116
124
|
.tab-link-item {
|
|
117
125
|
flex: 0 0 auto;
|
|
126
|
+
min-height: 48px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.tab-link-item__label {
|
|
130
|
+
max-width: 5.5rem;
|
|
131
|
+
overflow: hidden;
|
|
132
|
+
text-overflow: ellipsis;
|
|
133
|
+
white-space: nowrap;
|
|
118
134
|
}
|
|
119
135
|
</style>
|
|
@@ -25,7 +25,12 @@ function toSurfaceLabel(surfaceId = "") {
|
|
|
25
25
|
|
|
26
26
|
function useShellLayoutState(props = {}) {
|
|
27
27
|
const shellLayoutStore = useShellLayoutStore();
|
|
28
|
-
const {
|
|
28
|
+
const {
|
|
29
|
+
drawerDefaultOpen,
|
|
30
|
+
drawerOpen,
|
|
31
|
+
supportingContentOpen,
|
|
32
|
+
supportingContentTitle
|
|
33
|
+
} = storeToRefs(shellLayoutStore);
|
|
29
34
|
let route = null;
|
|
30
35
|
try {
|
|
31
36
|
route = useRoute();
|
|
@@ -83,7 +88,13 @@ function useShellLayoutState(props = {}) {
|
|
|
83
88
|
return Object.freeze({
|
|
84
89
|
drawerDefaultOpen,
|
|
85
90
|
drawerOpen,
|
|
91
|
+
supportingContentOpen,
|
|
92
|
+
supportingContentTitle,
|
|
86
93
|
setDrawerDefaultOpen: shellLayoutStore.setDrawerDefaultOpen,
|
|
94
|
+
setDrawerOpen: shellLayoutStore.setDrawerOpen,
|
|
95
|
+
setSupportingContentOpen: shellLayoutStore.setSupportingContentOpen,
|
|
96
|
+
openSupportingContent: shellLayoutStore.openSupportingContent,
|
|
97
|
+
closeSupportingContent: shellLayoutStore.closeSupportingContent,
|
|
87
98
|
toggleDrawer,
|
|
88
99
|
resolvedSurface,
|
|
89
100
|
resolvedSurfaceLabel
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
|
|
6
6
|
const ERROR_CHANNELS = Object.freeze(["snackbar", "banner", "dialog", "silent"]);
|
|
7
7
|
const ERROR_SEVERITIES = Object.freeze(["info", "success", "warning", "error", "critical"]);
|
|
8
|
+
const ERROR_INTENTS = Object.freeze(["resource-load", "action-feedback", "app-recoverable", "blocking"]);
|
|
8
9
|
|
|
9
10
|
function normalizeText(value, fallback = "") {
|
|
10
11
|
return normalizeKernelText(value, {
|
|
@@ -34,6 +35,20 @@ function normalizeSeverity(value, fallback = "error") {
|
|
|
34
35
|
return "error";
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
function normalizeErrorIntent(value, fallback = "") {
|
|
39
|
+
const normalized = normalizeText(value).toLowerCase();
|
|
40
|
+
if (ERROR_INTENTS.includes(normalized)) {
|
|
41
|
+
return normalized;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const normalizedFallback = normalizeText(fallback).toLowerCase();
|
|
45
|
+
if (ERROR_INTENTS.includes(normalizedFallback)) {
|
|
46
|
+
return normalizedFallback;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
function normalizeNonNegativeInteger(value, fallback = 0) {
|
|
38
53
|
const numericValue = Number(value);
|
|
39
54
|
if (!Number.isFinite(numericValue)) {
|
|
@@ -67,10 +82,12 @@ function normalizeAction(value) {
|
|
|
67
82
|
export {
|
|
68
83
|
ERROR_CHANNELS,
|
|
69
84
|
ERROR_SEVERITIES,
|
|
85
|
+
ERROR_INTENTS,
|
|
70
86
|
isRecord,
|
|
71
87
|
normalizeText,
|
|
72
88
|
normalizeChannel,
|
|
73
89
|
normalizeSeverity,
|
|
90
|
+
normalizeErrorIntent,
|
|
74
91
|
normalizeNonNegativeInteger,
|
|
75
92
|
normalizeAction
|
|
76
93
|
};
|
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
import {
|
|
2
2
|
normalizeAction,
|
|
3
3
|
normalizeChannel,
|
|
4
|
+
normalizeErrorIntent,
|
|
4
5
|
normalizeSeverity,
|
|
5
6
|
normalizeText
|
|
6
7
|
} from "./normalize.js";
|
|
7
8
|
|
|
8
9
|
function createDefaultErrorPolicy({
|
|
9
10
|
defaultChannel = "snackbar",
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
resourceLoadChannel = "silent",
|
|
12
|
+
actionFeedbackChannel = "snackbar",
|
|
13
|
+
appRecoverableChannel = "banner",
|
|
14
|
+
blockingChannel = "dialog",
|
|
12
15
|
defaultSeverity = "error"
|
|
13
16
|
} = {}) {
|
|
14
17
|
const normalizedDefaultChannel = normalizeChannel(defaultChannel, "snackbar") || "snackbar";
|
|
15
|
-
const
|
|
16
|
-
const
|
|
18
|
+
const normalizedResourceLoadChannel = normalizeChannel(resourceLoadChannel, "silent") || "silent";
|
|
19
|
+
const normalizedActionFeedbackChannel = normalizeChannel(actionFeedbackChannel, "snackbar") || "snackbar";
|
|
20
|
+
const normalizedAppRecoverableChannel = normalizeChannel(appRecoverableChannel, "banner") || "banner";
|
|
21
|
+
const normalizedBlockingChannel = normalizeChannel(blockingChannel, "dialog") || "dialog";
|
|
17
22
|
const normalizedDefaultSeverity = normalizeSeverity(defaultSeverity, "error");
|
|
18
23
|
|
|
19
24
|
return function defaultErrorPolicy(event = {}) {
|
|
20
|
-
const status = Number(event.status || 0);
|
|
21
25
|
const explicitChannel = normalizeChannel(event.channel);
|
|
26
|
+
const intent = normalizeErrorIntent(event.intent || (event.blocking === true ? "blocking" : ""));
|
|
22
27
|
|
|
23
28
|
let channel = explicitChannel;
|
|
24
29
|
if (!channel) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
switch (intent) {
|
|
31
|
+
case "resource-load":
|
|
32
|
+
channel = normalizedResourceLoadChannel;
|
|
33
|
+
break;
|
|
34
|
+
case "action-feedback":
|
|
35
|
+
channel = normalizedActionFeedbackChannel;
|
|
36
|
+
break;
|
|
37
|
+
case "app-recoverable":
|
|
38
|
+
channel = normalizedAppRecoverableChannel;
|
|
39
|
+
break;
|
|
40
|
+
case "blocking":
|
|
41
|
+
channel = normalizedBlockingChannel;
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
channel = normalizedDefaultChannel;
|
|
31
45
|
}
|
|
32
46
|
}
|
|
33
47
|
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
isRecord,
|
|
3
3
|
normalizeAction,
|
|
4
4
|
normalizeChannel,
|
|
5
|
+
normalizeErrorIntent,
|
|
5
6
|
normalizeNonNegativeInteger,
|
|
6
7
|
normalizeSeverity,
|
|
7
8
|
normalizeText
|
|
@@ -43,6 +44,7 @@ function normalizeErrorEvent(rawEvent = {}) {
|
|
|
43
44
|
source: normalizeText(source.source, "app"),
|
|
44
45
|
message: normalizeText(userMessage || runtimeMessage, "Request failed."),
|
|
45
46
|
userMessage,
|
|
47
|
+
intent: normalizeErrorIntent(source.intent || source.kind),
|
|
46
48
|
severity: normalizeSeverity(source.severity, "error"),
|
|
47
49
|
channel: normalizeChannel(source.channel),
|
|
48
50
|
presenterId: normalizeText(source.presenterId),
|
package/src/client/index.js
CHANGED
|
@@ -9,6 +9,7 @@ export {
|
|
|
9
9
|
export { default as ShellLayout } from "./components/ShellLayout.vue";
|
|
10
10
|
export { default as ShellOutlet } from "./components/ShellOutlet.vue";
|
|
11
11
|
export { default as ShellOutletMenuWidget } from "./components/ShellOutletMenuWidget.vue";
|
|
12
|
+
export { default as ShellRouteTransition } from "./components/ShellRouteTransition.vue";
|
|
12
13
|
export { default as ShellErrorHost } from "./components/ShellErrorHost.vue";
|
|
13
14
|
export { default as ShellMenuLinkItem } from "./components/ShellMenuLinkItem.vue";
|
|
14
15
|
export { default as ShellSurfaceAwareMenuLinkItem } from "./components/ShellSurfaceAwareMenuLinkItem.vue";
|