@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
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
<script setup>
|
|
2
|
+
import {
|
|
3
|
+
computed,
|
|
4
|
+
inject,
|
|
5
|
+
onBeforeUnmount,
|
|
6
|
+
onMounted,
|
|
7
|
+
ref,
|
|
8
|
+
watch
|
|
9
|
+
} from "vue";
|
|
10
|
+
import { useDisplay } from "vuetify";
|
|
2
11
|
import { useShellLayoutState } from "../composables/useShellLayoutState.js";
|
|
3
12
|
import ShellOutlet from "./ShellOutlet.vue";
|
|
13
|
+
import ShellRouteTransition from "./ShellRouteTransition.vue";
|
|
4
14
|
|
|
5
15
|
const props = defineProps({
|
|
6
16
|
surface: {
|
|
@@ -21,61 +31,481 @@ const props = defineProps({
|
|
|
21
31
|
}
|
|
22
32
|
});
|
|
23
33
|
|
|
24
|
-
const {
|
|
34
|
+
const {
|
|
35
|
+
drawerDefaultOpen,
|
|
36
|
+
drawerOpen,
|
|
37
|
+
setDrawerOpen,
|
|
38
|
+
supportingContentOpen,
|
|
39
|
+
supportingContentTitle,
|
|
40
|
+
setSupportingContentOpen,
|
|
41
|
+
closeSupportingContent,
|
|
42
|
+
toggleDrawer,
|
|
43
|
+
resolvedSurface,
|
|
44
|
+
resolvedSurfaceLabel
|
|
45
|
+
} = useShellLayoutState(props);
|
|
46
|
+
const display = useDisplay();
|
|
47
|
+
const refreshRuntime = inject("jskit.shell-web.runtime.web-refresh.client", null);
|
|
48
|
+
const pullDistance = ref(0);
|
|
49
|
+
const pullRefreshing = ref(false);
|
|
50
|
+
let activePull = null;
|
|
51
|
+
|
|
52
|
+
const PULL_REFRESH_TRIGGER_DISTANCE = 72;
|
|
53
|
+
const PULL_REFRESH_MAX_DISTANCE = 112;
|
|
54
|
+
|
|
55
|
+
const layoutClass = computed(() => {
|
|
56
|
+
const displayName = String(display?.name?.value || "").trim().toLowerCase();
|
|
57
|
+
if (displayName === "xs" || displayName === "sm") {
|
|
58
|
+
return "compact";
|
|
59
|
+
}
|
|
60
|
+
if (displayName === "md") {
|
|
61
|
+
return "medium";
|
|
62
|
+
}
|
|
63
|
+
return "expanded";
|
|
64
|
+
});
|
|
65
|
+
const isCompactLayout = computed(() => layoutClass.value === "compact");
|
|
66
|
+
const pullProgress = computed(() =>
|
|
67
|
+
Math.min(100, Math.round((pullDistance.value / PULL_REFRESH_TRIGGER_DISTANCE) * 100))
|
|
68
|
+
);
|
|
69
|
+
const pullIndicatorVisible = computed(() =>
|
|
70
|
+
Boolean(isCompactLayout.value && (pullDistance.value > 0 || pullRefreshing.value))
|
|
71
|
+
);
|
|
72
|
+
const pullRefreshLabel = computed(() => {
|
|
73
|
+
if (pullRefreshing.value) {
|
|
74
|
+
return "Refreshing";
|
|
75
|
+
}
|
|
76
|
+
return pullProgress.value >= 100 ? "Release to refresh" : "Pull to refresh";
|
|
77
|
+
});
|
|
78
|
+
const pullRefreshStyle = computed(() => ({
|
|
79
|
+
"--shell-pull-refresh-distance": `${Math.round(Math.min(pullDistance.value, PULL_REFRESH_MAX_DISTANCE))}px`
|
|
80
|
+
}));
|
|
81
|
+
|
|
82
|
+
watch(
|
|
83
|
+
isCompactLayout,
|
|
84
|
+
(compact) => {
|
|
85
|
+
setDrawerOpen(compact ? false : drawerDefaultOpen.value);
|
|
86
|
+
},
|
|
87
|
+
{ immediate: true }
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
onMounted(() => {
|
|
91
|
+
if (typeof window !== "object") {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
window.addEventListener("pointerdown", handlePullPointerDown, { capture: true, passive: true });
|
|
96
|
+
window.addEventListener("pointermove", handlePullPointerMove, { capture: true, passive: false });
|
|
97
|
+
window.addEventListener("pointerup", handlePullPointerEnd, { capture: true, passive: true });
|
|
98
|
+
window.addEventListener("pointercancel", handlePullPointerCancel, { capture: true, passive: true });
|
|
99
|
+
window.addEventListener("touchstart", handlePullTouchStart, { capture: true, passive: true });
|
|
100
|
+
window.addEventListener("touchmove", handlePullTouchMove, { capture: true, passive: false });
|
|
101
|
+
window.addEventListener("touchend", handlePullTouchEnd, { capture: true, passive: true });
|
|
102
|
+
window.addEventListener("touchcancel", handlePullTouchCancel, { capture: true, passive: true });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
onBeforeUnmount(() => {
|
|
106
|
+
if (typeof window !== "object") {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
window.removeEventListener("pointerdown", handlePullPointerDown, { capture: true });
|
|
111
|
+
window.removeEventListener("pointermove", handlePullPointerMove, { capture: true });
|
|
112
|
+
window.removeEventListener("pointerup", handlePullPointerEnd, { capture: true });
|
|
113
|
+
window.removeEventListener("pointercancel", handlePullPointerCancel, { capture: true });
|
|
114
|
+
window.removeEventListener("touchstart", handlePullTouchStart, { capture: true });
|
|
115
|
+
window.removeEventListener("touchmove", handlePullTouchMove, { capture: true });
|
|
116
|
+
window.removeEventListener("touchend", handlePullTouchEnd, { capture: true });
|
|
117
|
+
window.removeEventListener("touchcancel", handlePullTouchCancel, { capture: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function handlePullPointerDown(event) {
|
|
121
|
+
if (!canStartPullRefresh(event)) {
|
|
122
|
+
activePull = null;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
activePull = {
|
|
127
|
+
pointerId: event.pointerId,
|
|
128
|
+
touchIdentifier: null,
|
|
129
|
+
startX: event.clientX,
|
|
130
|
+
startY: event.clientY,
|
|
131
|
+
pointerCancelled: false
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handlePullPointerMove(event) {
|
|
136
|
+
if (!activePull || event.pointerId !== activePull.pointerId) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
updatePullGesture(event.clientX, event.clientY, event);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function handlePullPointerEnd(event) {
|
|
144
|
+
if (!activePull || event.pointerId !== activePull.pointerId) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
finishPullGesture();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function handlePullPointerCancel(event) {
|
|
152
|
+
if (!activePull || event.pointerId !== activePull.pointerId) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
activePull.pointerId = null;
|
|
157
|
+
activePull.pointerCancelled = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handlePullTouchStart(event) {
|
|
161
|
+
if (activePull || !canStartTouchPullRefresh(event)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const touch = event.touches?.[0] || null;
|
|
166
|
+
if (!touch) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
activePull = {
|
|
171
|
+
pointerId: null,
|
|
172
|
+
touchIdentifier: touch.identifier,
|
|
173
|
+
startX: touch.clientX,
|
|
174
|
+
startY: touch.clientY,
|
|
175
|
+
pointerCancelled: false
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function handlePullTouchMove(event) {
|
|
180
|
+
const touch = findActiveTouch(event.touches);
|
|
181
|
+
if (!activePull || !touch) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
updatePullGesture(touch.clientX, touch.clientY, event);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function handlePullTouchEnd(event) {
|
|
189
|
+
if (!activePull || !touchListIncludesActiveTouch(event.changedTouches)) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
finishPullGesture();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handlePullTouchCancel(event) {
|
|
197
|
+
if (activePull && touchListIncludesActiveTouch(event.changedTouches)) {
|
|
198
|
+
cancelPullRefresh();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function updatePullGesture(clientX, clientY, event) {
|
|
203
|
+
if (!activePull) {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const deltaX = clientX - activePull.startX;
|
|
208
|
+
const deltaY = clientY - activePull.startY;
|
|
209
|
+
const absX = Math.abs(deltaX);
|
|
210
|
+
|
|
211
|
+
if (deltaY < -4 || (absX > 24 && absX > deltaY * 1.15)) {
|
|
212
|
+
cancelPullRefresh();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (deltaY <= 6 || !isAtPageTop()) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (event?.cancelable) {
|
|
221
|
+
event.preventDefault();
|
|
222
|
+
}
|
|
223
|
+
pullDistance.value = Math.min(PULL_REFRESH_MAX_DISTANCE, Math.round(deltaY * 0.55));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function finishPullGesture() {
|
|
227
|
+
const shouldRefresh = pullDistance.value >= PULL_REFRESH_TRIGGER_DISTANCE;
|
|
228
|
+
activePull = null;
|
|
229
|
+
|
|
230
|
+
if (!shouldRefresh) {
|
|
231
|
+
pullDistance.value = 0;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
void refreshFromPullGesture();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function cancelPullRefresh() {
|
|
239
|
+
activePull = null;
|
|
240
|
+
if (!pullRefreshing.value) {
|
|
241
|
+
pullDistance.value = 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function refreshFromPullGesture() {
|
|
246
|
+
if (!refreshRuntime || typeof refreshRuntime.refresh !== "function" || pullRefreshing.value) {
|
|
247
|
+
pullDistance.value = 0;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
pullRefreshing.value = true;
|
|
252
|
+
pullDistance.value = PULL_REFRESH_TRIGGER_DISTANCE;
|
|
253
|
+
try {
|
|
254
|
+
await refreshRuntime.refresh("pull-to-refresh");
|
|
255
|
+
} finally {
|
|
256
|
+
pullRefreshing.value = false;
|
|
257
|
+
pullDistance.value = 0;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function canStartPullRefresh(event) {
|
|
262
|
+
return Boolean(
|
|
263
|
+
canStartPullRefreshFromTarget(event.target) &&
|
|
264
|
+
isPrimaryTouchPointer(event)
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function canStartTouchPullRefresh(event) {
|
|
269
|
+
return Boolean(
|
|
270
|
+
event?.touches?.length === 1 &&
|
|
271
|
+
canStartPullRefreshFromTarget(event.target)
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function canStartPullRefreshFromTarget(target) {
|
|
276
|
+
return Boolean(
|
|
277
|
+
isCompactLayout.value &&
|
|
278
|
+
refreshRuntime &&
|
|
279
|
+
typeof refreshRuntime.refresh === "function" &&
|
|
280
|
+
!pullRefreshing.value &&
|
|
281
|
+
isAtPageTop() &&
|
|
282
|
+
!isPullRefreshIgnoredTarget(target)
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isPrimaryTouchPointer(event) {
|
|
287
|
+
return event?.isPrimary !== false && event?.button === 0 && event?.pointerType !== "mouse";
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isAtPageTop() {
|
|
291
|
+
if (typeof window !== "object" || typeof document !== "object") {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const documentScrollTop = Number(document.documentElement?.scrollTop || 0);
|
|
296
|
+
const bodyScrollTop = Number(document.body?.scrollTop || 0);
|
|
297
|
+
return Math.max(Number(window.scrollY || 0), documentScrollTop, bodyScrollTop) <= 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isPullRefreshIgnoredTarget(target) {
|
|
301
|
+
return Boolean(
|
|
302
|
+
target?.closest?.(
|
|
303
|
+
[
|
|
304
|
+
"a",
|
|
305
|
+
"button",
|
|
306
|
+
"input",
|
|
307
|
+
"select",
|
|
308
|
+
"textarea",
|
|
309
|
+
"summary",
|
|
310
|
+
"[role='button']",
|
|
311
|
+
"[role='link']",
|
|
312
|
+
"[role='slider']",
|
|
313
|
+
"[contenteditable='true']",
|
|
314
|
+
"[data-shell-pull-refresh-ignore]",
|
|
315
|
+
"[data-shell-swipe-ignore]"
|
|
316
|
+
].join(",")
|
|
317
|
+
)
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function findActiveTouch(touchList) {
|
|
322
|
+
if (!activePull || !touchList || touchList.length < 1) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (activePull.touchIdentifier === null && touchList.length === 1) {
|
|
327
|
+
return touchList[0];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const touch of touchList) {
|
|
331
|
+
if (touch.identifier === activePull.touchIdentifier) {
|
|
332
|
+
return touch;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function touchListIncludesActiveTouch(touchList) {
|
|
340
|
+
if (!activePull) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (activePull.touchIdentifier === null) {
|
|
345
|
+
return !touchList || touchList.length <= 1;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return Boolean(findActiveTouch(touchList));
|
|
349
|
+
}
|
|
25
350
|
</script>
|
|
26
351
|
|
|
27
352
|
<template>
|
|
28
|
-
<v-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
353
|
+
<v-app-bar
|
|
354
|
+
border
|
|
355
|
+
:density="isCompactLayout ? 'compact' : 'comfortable'"
|
|
356
|
+
elevation="0"
|
|
357
|
+
class="shell-layout__app-bar bg-surface"
|
|
358
|
+
data-testid="jskit-shell-app-bar"
|
|
359
|
+
>
|
|
360
|
+
<v-app-bar-nav-icon
|
|
361
|
+
class="shell-layout__nav-toggle"
|
|
362
|
+
aria-label="Toggle navigation menu"
|
|
363
|
+
@click="toggleDrawer"
|
|
364
|
+
/>
|
|
365
|
+
|
|
366
|
+
<slot name="top-left" :surface="resolvedSurface">
|
|
367
|
+
<div class="shell-layout__top-left d-flex align-center ga-2">
|
|
368
|
+
<span class="shell-layout__surface-label">
|
|
369
|
+
{{ resolvedSurfaceLabel }}
|
|
370
|
+
</span>
|
|
371
|
+
<ShellOutlet target="shell-layout:top-left" />
|
|
372
|
+
</div>
|
|
373
|
+
</slot>
|
|
374
|
+
|
|
375
|
+
<v-spacer />
|
|
376
|
+
|
|
377
|
+
<slot name="top-right" :surface="resolvedSurface">
|
|
378
|
+
<div class="shell-layout__top-right d-flex align-center ga-2">
|
|
379
|
+
<ShellOutlet target="shell-layout:top-right" />
|
|
380
|
+
</div>
|
|
381
|
+
</slot>
|
|
382
|
+
</v-app-bar>
|
|
383
|
+
|
|
384
|
+
<div
|
|
385
|
+
v-if="pullIndicatorVisible"
|
|
386
|
+
class="shell-layout__pull-refresh"
|
|
387
|
+
:class="{ 'shell-layout__pull-refresh--refreshing': pullRefreshing }"
|
|
388
|
+
:style="pullRefreshStyle"
|
|
389
|
+
data-testid="jskit-shell-pull-refresh"
|
|
390
|
+
aria-live="polite"
|
|
391
|
+
>
|
|
392
|
+
<v-progress-circular
|
|
393
|
+
:model-value="pullProgress"
|
|
394
|
+
:indeterminate="pullRefreshing"
|
|
395
|
+
color="primary"
|
|
396
|
+
size="22"
|
|
397
|
+
width="3"
|
|
398
|
+
/>
|
|
399
|
+
<span class="shell-layout__pull-refresh-label">{{ pullRefreshLabel }}</span>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
<v-navigation-drawer
|
|
403
|
+
v-model="drawerOpen"
|
|
404
|
+
border
|
|
405
|
+
class="bg-surface"
|
|
406
|
+
data-testid="jskit-shell-drawer"
|
|
407
|
+
:temporary="isCompactLayout"
|
|
408
|
+
:permanent="!isCompactLayout"
|
|
409
|
+
:width="248"
|
|
410
|
+
>
|
|
411
|
+
<slot name="menu" :surface="resolvedSurface">
|
|
412
|
+
<v-list nav density="comfortable" class="pt-2">
|
|
413
|
+
<v-list-subheader class="text-uppercase text-caption">{{ resolvedSurfaceLabel }}</v-list-subheader>
|
|
414
|
+
<ShellOutlet
|
|
415
|
+
target="shell-layout:primary-menu"
|
|
416
|
+
default
|
|
417
|
+
/>
|
|
418
|
+
<v-divider class="my-2" />
|
|
419
|
+
<ShellOutlet target="shell-layout:secondary-menu" />
|
|
420
|
+
</v-list>
|
|
421
|
+
</slot>
|
|
422
|
+
</v-navigation-drawer>
|
|
423
|
+
|
|
424
|
+
<v-main class="bg-background">
|
|
425
|
+
<v-container fluid class="shell-layout__content">
|
|
426
|
+
<h1 v-if="title" class="shell-layout__title text-h5">{{ title }}</h1>
|
|
427
|
+
<p v-if="subtitle" class="shell-layout__subtitle text-body-2 text-medium-emphasis">{{ subtitle }}</p>
|
|
428
|
+
<ShellRouteTransition>
|
|
66
429
|
<slot />
|
|
67
|
-
</
|
|
68
|
-
</v-
|
|
69
|
-
</v-
|
|
430
|
+
</ShellRouteTransition>
|
|
431
|
+
</v-container>
|
|
432
|
+
</v-main>
|
|
433
|
+
|
|
434
|
+
<v-bottom-navigation
|
|
435
|
+
v-if="isCompactLayout"
|
|
436
|
+
class="shell-layout__bottom-nav"
|
|
437
|
+
data-testid="jskit-shell-bottom-nav"
|
|
438
|
+
bg-color="surface"
|
|
439
|
+
color="primary"
|
|
440
|
+
density="comfortable"
|
|
441
|
+
grow
|
|
442
|
+
mandatory
|
|
443
|
+
>
|
|
444
|
+
<ShellOutlet target="shell-layout:primary-bottom-nav" />
|
|
445
|
+
</v-bottom-navigation>
|
|
446
|
+
|
|
447
|
+
<v-bottom-sheet
|
|
448
|
+
v-if="isCompactLayout"
|
|
449
|
+
:model-value="supportingContentOpen"
|
|
450
|
+
@update:model-value="setSupportingContentOpen"
|
|
451
|
+
>
|
|
452
|
+
<v-card rounded="t-xl" class="shell-layout__supporting-sheet" data-testid="jskit-shell-supporting-bottom-sheet">
|
|
453
|
+
<v-card-title class="shell-layout__supporting-title">
|
|
454
|
+
<span>{{ supportingContentTitle || 'Details' }}</span>
|
|
455
|
+
<v-btn variant="text" @click="closeSupportingContent">Close</v-btn>
|
|
456
|
+
</v-card-title>
|
|
457
|
+
<v-card-text>
|
|
458
|
+
<ShellOutlet target="shell-layout:supporting-bottom-sheet" />
|
|
459
|
+
</v-card-text>
|
|
460
|
+
</v-card>
|
|
461
|
+
</v-bottom-sheet>
|
|
462
|
+
|
|
463
|
+
<v-navigation-drawer
|
|
464
|
+
v-if="!isCompactLayout"
|
|
465
|
+
:model-value="supportingContentOpen"
|
|
466
|
+
border
|
|
467
|
+
temporary
|
|
468
|
+
location="right"
|
|
469
|
+
:width="384"
|
|
470
|
+
data-testid="jskit-shell-supporting-side-panel"
|
|
471
|
+
@update:model-value="setSupportingContentOpen"
|
|
472
|
+
>
|
|
473
|
+
<div class="shell-layout__supporting-side-panel">
|
|
474
|
+
<div class="shell-layout__supporting-title">
|
|
475
|
+
<strong>{{ supportingContentTitle || 'Details' }}</strong>
|
|
476
|
+
<v-btn variant="text" @click="closeSupportingContent">Close</v-btn>
|
|
477
|
+
</div>
|
|
478
|
+
<ShellOutlet target="shell-layout:supporting-side-panel" />
|
|
479
|
+
</div>
|
|
480
|
+
</v-navigation-drawer>
|
|
70
481
|
</template>
|
|
71
482
|
|
|
72
483
|
<style scoped>
|
|
73
|
-
.shell-
|
|
74
|
-
|
|
484
|
+
.shell-layout__content {
|
|
485
|
+
padding: 0.75rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
|
|
75
486
|
}
|
|
76
487
|
|
|
77
|
-
.shell-
|
|
78
|
-
|
|
488
|
+
.shell-layout__top-left,
|
|
489
|
+
.shell-layout__top-right {
|
|
490
|
+
min-width: 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.shell-layout__top-right {
|
|
494
|
+
max-width: min(45vw, 18rem);
|
|
495
|
+
overflow: hidden;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.shell-layout__surface-label {
|
|
499
|
+
color: rgb(var(--v-theme-on-surface));
|
|
500
|
+
display: block;
|
|
501
|
+
font-size: 0.95rem;
|
|
502
|
+
font-weight: 650;
|
|
503
|
+
letter-spacing: -0.01em;
|
|
504
|
+
line-height: 1.2;
|
|
505
|
+
max-width: 12rem;
|
|
506
|
+
overflow: hidden;
|
|
507
|
+
text-overflow: ellipsis;
|
|
508
|
+
white-space: nowrap;
|
|
79
509
|
}
|
|
80
510
|
|
|
81
511
|
.shell-layout__title {
|
|
@@ -85,4 +515,79 @@ const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useS
|
|
|
85
515
|
.shell-layout__subtitle {
|
|
86
516
|
margin-bottom: 0.75rem;
|
|
87
517
|
}
|
|
518
|
+
|
|
519
|
+
.shell-layout__bottom-nav {
|
|
520
|
+
border-top: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
521
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.shell-layout__supporting-sheet {
|
|
525
|
+
max-height: min(72vh, 40rem);
|
|
526
|
+
overflow: auto;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
.shell-layout__supporting-side-panel {
|
|
530
|
+
display: flex;
|
|
531
|
+
flex-direction: column;
|
|
532
|
+
gap: 1rem;
|
|
533
|
+
padding: 1rem;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.shell-layout__supporting-title {
|
|
537
|
+
align-items: center;
|
|
538
|
+
display: flex;
|
|
539
|
+
gap: 0.75rem;
|
|
540
|
+
justify-content: space-between;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.shell-layout__supporting-title :deep(.v-btn) {
|
|
544
|
+
min-height: 48px;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.shell-layout__pull-refresh {
|
|
548
|
+
align-items: center;
|
|
549
|
+
background: rgb(var(--v-theme-surface));
|
|
550
|
+
border: 1px solid rgba(var(--v-border-color), var(--v-border-opacity));
|
|
551
|
+
border-radius: 999px;
|
|
552
|
+
box-shadow: var(--v-shadow-3);
|
|
553
|
+
display: flex;
|
|
554
|
+
gap: 0.5rem;
|
|
555
|
+
left: 50%;
|
|
556
|
+
opacity: min(1, calc(var(--shell-pull-refresh-distance) / 56));
|
|
557
|
+
padding: 0.45rem 0.75rem;
|
|
558
|
+
pointer-events: none;
|
|
559
|
+
position: fixed;
|
|
560
|
+
top: calc(env(safe-area-inset-top, 0px) + 3.75rem);
|
|
561
|
+
transform: translate3d(-50%, calc((var(--shell-pull-refresh-distance) - 72px) * 0.35), 0);
|
|
562
|
+
transition:
|
|
563
|
+
opacity 160ms ease,
|
|
564
|
+
transform 160ms ease;
|
|
565
|
+
z-index: 2700;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.shell-layout__pull-refresh--refreshing {
|
|
569
|
+
opacity: 1;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
.shell-layout__pull-refresh-label {
|
|
573
|
+
font-size: 0.78rem;
|
|
574
|
+
font-weight: 600;
|
|
575
|
+
white-space: nowrap;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
@media (max-width: 640px) {
|
|
579
|
+
.shell-layout__content {
|
|
580
|
+
padding-inline:
|
|
581
|
+
calc(1px + env(safe-area-inset-left, 0px))
|
|
582
|
+
calc(1px + env(safe-area-inset-right, 0px));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.shell-layout__surface-label {
|
|
586
|
+
max-width: 8rem;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.shell-layout__top-right {
|
|
590
|
+
max-width: 40vw;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
88
593
|
</style>
|