@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,11 +1,8 @@
|
|
|
1
1
|
import { getClientAppConfig } from "@jskit-ai/kernel/client";
|
|
2
2
|
import {
|
|
3
|
-
isRecord
|
|
4
|
-
shouldRetryTransientQueryFailure,
|
|
5
|
-
transientQueryRetryDelay
|
|
3
|
+
isRecord
|
|
6
4
|
} from "@jskit-ai/kernel/shared/support";
|
|
7
5
|
import { createProviderLogger as createSharedProviderLogger } from "@jskit-ai/kernel/shared/support/providerLogger";
|
|
8
|
-
import { QueryClient, VueQueryPlugin } from "@tanstack/vue-query";
|
|
9
6
|
import {
|
|
10
7
|
createDefaultErrorPolicy
|
|
11
8
|
} from "../error/policy.js";
|
|
@@ -33,19 +30,6 @@ const APP_PLACEMENT_MODULE_SPECIFIER = "/src/placement.js";
|
|
|
33
30
|
const APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER = "/src/placementTopology.js";
|
|
34
31
|
const APP_ERROR_MODULE_SPECIFIER = "/src/error.js";
|
|
35
32
|
|
|
36
|
-
function createShellWebQueryClient() {
|
|
37
|
-
return new QueryClient({
|
|
38
|
-
defaultOptions: {
|
|
39
|
-
queries: {
|
|
40
|
-
refetchOnWindowFocus: false,
|
|
41
|
-
refetchOnReconnect: true,
|
|
42
|
-
retry: shouldRetryTransientQueryFailure,
|
|
43
|
-
retryDelay: transientQueryRetryDelay
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
|
|
49
33
|
function isMissingDynamicModule(error, moduleSpecifier) {
|
|
50
34
|
const message = String(error?.message || error || "");
|
|
51
35
|
return (
|
|
@@ -93,21 +77,7 @@ async function loadAppPlacementTopology(logger) {
|
|
|
93
77
|
try {
|
|
94
78
|
const moduleNamespace = await import("/src/placementTopology.js");
|
|
95
79
|
const exported = moduleNamespace?.default;
|
|
96
|
-
|
|
97
|
-
if (Array.isArray(resolved)) {
|
|
98
|
-
return resolved;
|
|
99
|
-
}
|
|
100
|
-
if (resolved && typeof resolved === "object") {
|
|
101
|
-
return [resolved];
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
logger.warn(
|
|
105
|
-
{
|
|
106
|
-
module: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER,
|
|
107
|
-
exportedType: typeof exported
|
|
108
|
-
},
|
|
109
|
-
"App placement topology module default export did not resolve to an object or array; using empty topology."
|
|
110
|
-
);
|
|
80
|
+
return resolveAppPlacementTopologyExport(exported, logger);
|
|
111
81
|
} catch (error) {
|
|
112
82
|
if (isMissingDynamicModule(error, APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER)) {
|
|
113
83
|
return [];
|
|
@@ -125,6 +95,25 @@ async function loadAppPlacementTopology(logger) {
|
|
|
125
95
|
return [];
|
|
126
96
|
}
|
|
127
97
|
|
|
98
|
+
function resolveAppPlacementTopologyExport(exported, logger) {
|
|
99
|
+
const resolved = typeof exported === "function" ? exported() : exported;
|
|
100
|
+
if (Array.isArray(resolved)) {
|
|
101
|
+
return resolved;
|
|
102
|
+
}
|
|
103
|
+
if (resolved && typeof resolved === "object") {
|
|
104
|
+
return resolved;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
logger.warn(
|
|
108
|
+
{
|
|
109
|
+
module: APP_PLACEMENT_TOPOLOGY_MODULE_SPECIFIER,
|
|
110
|
+
exportedType: typeof exported
|
|
111
|
+
},
|
|
112
|
+
"App placement topology module default export did not resolve to an object or array; using empty topology."
|
|
113
|
+
);
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
|
|
128
117
|
function createErrorConfigToolkit(errorRuntime) {
|
|
129
118
|
return Object.freeze({
|
|
130
119
|
createDefaultErrorPolicy,
|
|
@@ -197,6 +186,136 @@ function applyAppErrorConfig(errorRuntime, errorConfig = {}) {
|
|
|
197
186
|
errorRuntime.assertBootReady();
|
|
198
187
|
}
|
|
199
188
|
|
|
189
|
+
function isPullRefreshQuery(query = null) {
|
|
190
|
+
const meta = isRecord(query?.meta) ? query.meta : {};
|
|
191
|
+
if (meta.jskitRefresh === "pull") {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (meta.jskitRefresh === false) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const jskitMeta = isRecord(meta.jskit) ? meta.jskit : {};
|
|
199
|
+
return jskitMeta.refreshOnPull !== false;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function createShellRefreshRuntime({
|
|
203
|
+
app,
|
|
204
|
+
logger = null
|
|
205
|
+
} = {}) {
|
|
206
|
+
if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
|
|
207
|
+
throw new Error("createShellRefreshRuntime requires application has()/make().");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const runtimeLogger = logger || createSharedProviderLogger(app);
|
|
211
|
+
let refreshQueue = Promise.resolve(null);
|
|
212
|
+
|
|
213
|
+
async function refreshBootstrap(reason) {
|
|
214
|
+
if (!app.has("runtime.web-bootstrap.client")) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const bootstrapRuntime = app.make("runtime.web-bootstrap.client");
|
|
219
|
+
if (!bootstrapRuntime || typeof bootstrapRuntime.refresh !== "function") {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await bootstrapRuntime.refresh(reason);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function refetchPullQueries() {
|
|
228
|
+
if (!app.has("jskit.client.query-client")) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const queryClient = app.make("jskit.client.query-client");
|
|
233
|
+
if (!queryClient || typeof queryClient.refetchQueries !== "function") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
await queryClient.refetchQueries(
|
|
238
|
+
{
|
|
239
|
+
type: "active",
|
|
240
|
+
predicate: isPullRefreshQuery
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
throwOnError: false
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function reportRefreshFailure(error) {
|
|
250
|
+
runtimeLogger.warn(
|
|
251
|
+
{
|
|
252
|
+
error: String(error?.message || error || "unknown error")
|
|
253
|
+
},
|
|
254
|
+
"Shell refresh failed."
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (!app.has("runtime.web-error.client")) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const errorRuntime = app.make("runtime.web-error.client");
|
|
262
|
+
if (!errorRuntime || typeof errorRuntime.report !== "function") {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
errorRuntime.report({
|
|
267
|
+
source: "shell-web.refresh",
|
|
268
|
+
message: "Unable to refresh. Check the connection and try again.",
|
|
269
|
+
intent: "app-recoverable",
|
|
270
|
+
severity: "error",
|
|
271
|
+
dedupeKey: "shell-web.refresh.failed",
|
|
272
|
+
dedupeWindowMs: 2000,
|
|
273
|
+
action: {
|
|
274
|
+
label: "Retry",
|
|
275
|
+
dismissOnRun: true,
|
|
276
|
+
handler() {
|
|
277
|
+
void refresh("retry");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function performRefresh(reason = "manual") {
|
|
284
|
+
const normalizedReason = String(reason || "manual").trim() || "manual";
|
|
285
|
+
try {
|
|
286
|
+
const [bootstrapRefreshed, queriesRefetched] = await Promise.all([
|
|
287
|
+
refreshBootstrap(normalizedReason),
|
|
288
|
+
refetchPullQueries()
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
return Object.freeze({
|
|
292
|
+
reason: normalizedReason,
|
|
293
|
+
bootstrapRefreshed,
|
|
294
|
+
queriesRefetched
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
reportRefreshFailure(error);
|
|
298
|
+
return Object.freeze({
|
|
299
|
+
reason: normalizedReason,
|
|
300
|
+
bootstrapRefreshed: false,
|
|
301
|
+
queriesRefetched: false,
|
|
302
|
+
error
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function refresh(reason = "manual") {
|
|
308
|
+
refreshQueue = refreshQueue
|
|
309
|
+
.catch(() => null)
|
|
310
|
+
.then(() => performRefresh(reason));
|
|
311
|
+
return refreshQueue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
refresh
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
200
319
|
function installVueErrorBridge(vueApp, errorRuntime, logger) {
|
|
201
320
|
if (!vueApp || !isRecord(vueApp.config)) {
|
|
202
321
|
return;
|
|
@@ -210,8 +329,8 @@ function installVueErrorBridge(vueApp, errorRuntime, logger) {
|
|
|
210
329
|
source: "shell-web.vue.error-handler",
|
|
211
330
|
message: String(error?.message || "Unexpected UI error."),
|
|
212
331
|
cause: error,
|
|
332
|
+
intent: "blocking",
|
|
213
333
|
severity: "error",
|
|
214
|
-
channel: "dialog",
|
|
215
334
|
details: {
|
|
216
335
|
info: String(info || "")
|
|
217
336
|
}
|
|
@@ -248,8 +367,8 @@ function installRouterErrorBridge(app, errorRuntime, logger) {
|
|
|
248
367
|
source: "shell-web.router.on-error",
|
|
249
368
|
message: String(error?.message || "Navigation failed."),
|
|
250
369
|
cause: error,
|
|
370
|
+
intent: "app-recoverable",
|
|
251
371
|
severity: "error",
|
|
252
|
-
channel: "banner",
|
|
253
372
|
dedupeKey: String(error?.message || "navigation-failed"),
|
|
254
373
|
dedupeWindowMs: 2000
|
|
255
374
|
});
|
|
@@ -312,7 +431,12 @@ class ShellWebClientProvider {
|
|
|
312
431
|
logger
|
|
313
432
|
})
|
|
314
433
|
);
|
|
315
|
-
app.singleton("
|
|
434
|
+
app.singleton("runtime.web-refresh.client", (scope) =>
|
|
435
|
+
createShellRefreshRuntime({
|
|
436
|
+
app: scope,
|
|
437
|
+
logger
|
|
438
|
+
})
|
|
439
|
+
);
|
|
316
440
|
app.singleton("runtime.web-error.presentation-store.client", () => createErrorPresentationStore());
|
|
317
441
|
app.singleton("runtime.web-error.client", (scope) =>
|
|
318
442
|
createErrorRuntime({
|
|
@@ -384,12 +508,11 @@ class ShellWebClientProvider {
|
|
|
384
508
|
throw new Error("ShellWebClientProvider requires Pinia installed in the client app.");
|
|
385
509
|
}
|
|
386
510
|
const errorPresentationStore = app.make("runtime.web-error.presentation-store.client");
|
|
511
|
+
const refreshRuntime = app.make("runtime.web-refresh.client");
|
|
387
512
|
useShellErrorPresentationStore(pinia).attachRuntimeStore(errorPresentationStore);
|
|
388
513
|
|
|
389
|
-
vueApp.use(VueQueryPlugin, {
|
|
390
|
-
queryClient: app.make("shell.web.query-client")
|
|
391
|
-
});
|
|
392
514
|
vueApp.provide("jskit.shell-web.runtime.web-placement.client", placementRuntime);
|
|
515
|
+
vueApp.provide("jskit.shell-web.runtime.web-refresh.client", refreshRuntime);
|
|
393
516
|
vueApp.provide("jskit.shell-web.runtime.web-error.client", errorRuntime);
|
|
394
517
|
vueApp.provide(
|
|
395
518
|
"jskit.shell-web.runtime.web-error.presentation-store.client",
|
|
@@ -402,5 +525,6 @@ class ShellWebClientProvider {
|
|
|
402
525
|
}
|
|
403
526
|
|
|
404
527
|
export {
|
|
405
|
-
ShellWebClientProvider
|
|
528
|
+
ShellWebClientProvider,
|
|
529
|
+
resolveAppPlacementTopologyExport
|
|
406
530
|
};
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
export const useShellLayoutStore = defineStore("jskit.shell-web.layout", () => {
|
|
9
9
|
const drawerDefaultOpen = ref(readDrawerDefaultOpenPreference());
|
|
10
10
|
const drawerOpen = ref(drawerDefaultOpen.value);
|
|
11
|
+
const supportingContentOpen = ref(false);
|
|
12
|
+
const supportingContentTitle = ref("");
|
|
11
13
|
|
|
12
14
|
function setDrawerDefaultOpen(open) {
|
|
13
15
|
const normalized = Boolean(open);
|
|
@@ -24,11 +26,29 @@ export const useShellLayoutStore = defineStore("jskit.shell-web.layout", () => {
|
|
|
24
26
|
drawerOpen.value = !drawerOpen.value;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
function openSupportingContent({ title = "" } = {}) {
|
|
30
|
+
supportingContentTitle.value = String(title || "").trim();
|
|
31
|
+
supportingContentOpen.value = true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function closeSupportingContent() {
|
|
35
|
+
supportingContentOpen.value = false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function setSupportingContentOpen(open) {
|
|
39
|
+
supportingContentOpen.value = Boolean(open);
|
|
40
|
+
}
|
|
41
|
+
|
|
27
42
|
return {
|
|
28
43
|
drawerDefaultOpen,
|
|
29
44
|
drawerOpen,
|
|
45
|
+
supportingContentOpen,
|
|
46
|
+
supportingContentTitle,
|
|
30
47
|
setDrawerDefaultOpen,
|
|
31
48
|
setDrawerOpen,
|
|
32
|
-
toggleDrawer
|
|
49
|
+
toggleDrawer,
|
|
50
|
+
openSupportingContent,
|
|
51
|
+
closeSupportingContent,
|
|
52
|
+
setSupportingContentOpen
|
|
33
53
|
};
|
|
34
54
|
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = String(process.env.PLAYWRIGHT_BASE_URL || "http://127.0.0.1:5173").replace(/\/+$/u, "");
|
|
2
|
+
const DEFAULT_SMOKE_PATH = String(process.env.JSKIT_PLAYWRIGHT_SMOKE_PATH || "/home");
|
|
3
|
+
const DEFAULT_VIEWPORTS = Object.freeze([
|
|
4
|
+
Object.freeze({ name: "compact", width: 390, height: 844 }),
|
|
5
|
+
Object.freeze({ name: "medium", width: 768, height: 1024 }),
|
|
6
|
+
Object.freeze({ name: "expanded", width: 1280, height: 900 })
|
|
7
|
+
]);
|
|
8
|
+
|
|
9
|
+
async function expectNoHorizontalOverflow(page, expect) {
|
|
10
|
+
const metrics = await page.evaluate(() => ({
|
|
11
|
+
clientWidth: document.documentElement.clientWidth,
|
|
12
|
+
scrollWidth: document.documentElement.scrollWidth
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
expect(metrics.scrollWidth).toBeLessThanOrEqual(metrics.clientWidth + 1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function expectGeneratedScreenContract(page, expect) {
|
|
19
|
+
const screen = page.locator(".generated-ui-screen").first();
|
|
20
|
+
|
|
21
|
+
await expect(screen).toBeVisible();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function pullToRefresh(page, expect) {
|
|
25
|
+
await page.evaluate(() => {
|
|
26
|
+
window.scrollTo(0, 0);
|
|
27
|
+
});
|
|
28
|
+
await page.dispatchEvent("body", "pointerdown", {
|
|
29
|
+
pointerId: 41,
|
|
30
|
+
pointerType: "touch",
|
|
31
|
+
isPrimary: true,
|
|
32
|
+
button: 0,
|
|
33
|
+
clientX: 180,
|
|
34
|
+
clientY: 90
|
|
35
|
+
});
|
|
36
|
+
await page.dispatchEvent("body", "pointermove", {
|
|
37
|
+
pointerId: 41,
|
|
38
|
+
pointerType: "touch",
|
|
39
|
+
isPrimary: true,
|
|
40
|
+
button: 0,
|
|
41
|
+
clientX: 184,
|
|
42
|
+
clientY: 250
|
|
43
|
+
});
|
|
44
|
+
await expect(page.getByTestId("jskit-shell-pull-refresh")).toBeVisible();
|
|
45
|
+
await page.dispatchEvent("body", "pointerup", {
|
|
46
|
+
pointerId: 41,
|
|
47
|
+
pointerType: "touch",
|
|
48
|
+
isPrimary: true,
|
|
49
|
+
button: 0,
|
|
50
|
+
clientX: 184,
|
|
51
|
+
clientY: 250
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function isElementVisibleInViewport(page, testId) {
|
|
56
|
+
return page.getByTestId(testId).evaluate((element) => {
|
|
57
|
+
const rect = element.getBoundingClientRect();
|
|
58
|
+
const style = window.getComputedStyle(element);
|
|
59
|
+
return (
|
|
60
|
+
style.display !== "none" &&
|
|
61
|
+
style.visibility !== "hidden" &&
|
|
62
|
+
rect.width > 0 &&
|
|
63
|
+
rect.height > 0 &&
|
|
64
|
+
rect.right > 0 &&
|
|
65
|
+
rect.left < window.innerWidth &&
|
|
66
|
+
rect.bottom > 0 &&
|
|
67
|
+
rect.top < window.innerHeight
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runAdaptiveShellSmoke({
|
|
73
|
+
test,
|
|
74
|
+
expect,
|
|
75
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
76
|
+
smokePath = DEFAULT_SMOKE_PATH,
|
|
77
|
+
viewports = DEFAULT_VIEWPORTS
|
|
78
|
+
} = {}) {
|
|
79
|
+
if (!test || !expect) {
|
|
80
|
+
throw new Error("runAdaptiveShellSmoke requires Playwright test and expect.");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
test.describe("generated adaptive shell smoke", () => {
|
|
84
|
+
for (const viewport of viewports) {
|
|
85
|
+
test(`${viewport.name} layout has reachable navigation and no horizontal overflow`, async ({ page }) => {
|
|
86
|
+
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
87
|
+
await page.goto(`${baseUrl}${smokePath}`);
|
|
88
|
+
await expect(page.locator("body")).toBeVisible();
|
|
89
|
+
await expectGeneratedScreenContract(page, expect);
|
|
90
|
+
await expectNoHorizontalOverflow(page, expect);
|
|
91
|
+
|
|
92
|
+
if (viewport.name === "compact") {
|
|
93
|
+
let bootstrapRequests = 0;
|
|
94
|
+
await page.route("**/api/bootstrap**", async (route) => {
|
|
95
|
+
bootstrapRequests += 1;
|
|
96
|
+
await route.continue();
|
|
97
|
+
});
|
|
98
|
+
const bootstrapRequestsBeforePull = bootstrapRequests;
|
|
99
|
+
|
|
100
|
+
await expect(page.getByTestId("jskit-shell-bottom-nav")).toBeVisible();
|
|
101
|
+
expect(await isElementVisibleInViewport(page, "jskit-shell-drawer")).toBe(false);
|
|
102
|
+
|
|
103
|
+
const navButtonHeights = await page.getByTestId("jskit-shell-bottom-nav").locator(".v-btn").evaluateAll((buttons) =>
|
|
104
|
+
buttons.map((button) => button.getBoundingClientRect().height)
|
|
105
|
+
);
|
|
106
|
+
expect(navButtonHeights.length).toBeGreaterThan(0);
|
|
107
|
+
for (const height of navButtonHeights) {
|
|
108
|
+
expect(height).toBeGreaterThanOrEqual(48);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await pullToRefresh(page, expect);
|
|
112
|
+
await expect.poll(() => bootstrapRequests).toBeGreaterThan(bootstrapRequestsBeforePull);
|
|
113
|
+
} else {
|
|
114
|
+
await expect(page.getByTestId("jskit-shell-drawer")).toBeVisible();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { DEFAULT_VIEWPORTS, runAdaptiveShellSmoke };
|
|
@@ -1,12 +1,42 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
<
|
|
4
|
-
<
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
<section class="generated-ui-screen generated-ui-screen--app home-start-screen d-flex flex-column ga-4">
|
|
3
|
+
<header>
|
|
4
|
+
<p class="text-overline text-medium-emphasis mb-1">Home</p>
|
|
5
|
+
<h1 class="home-start-screen__title">Home base</h1>
|
|
6
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
7
|
+
The app runtime is online and ready for the first real workflow.
|
|
8
|
+
</p>
|
|
9
|
+
</header>
|
|
10
|
+
|
|
11
|
+
<v-sheet rounded="lg" border class="home-start-screen__panel">
|
|
12
|
+
<h2 class="text-h6 mb-2">No activity yet</h2>
|
|
13
|
+
<p class="text-body-2 text-medium-emphasis mb-0">
|
|
14
|
+
Recent work, saved records, and next actions will appear here once this app has data.
|
|
15
|
+
</p>
|
|
16
|
+
</v-sheet>
|
|
17
|
+
</section>
|
|
12
18
|
</template>
|
|
19
|
+
|
|
20
|
+
<style scoped>
|
|
21
|
+
.generated-ui-screen {
|
|
22
|
+
--generated-ui-screen-title-size: clamp(1.5rem, 3vw, 2.5rem);
|
|
23
|
+
--generated-ui-screen-panel-padding: 1rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.home-start-screen {
|
|
27
|
+
margin-inline: auto;
|
|
28
|
+
max-width: 48rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.home-start-screen__title {
|
|
32
|
+
font-size: var(--generated-ui-screen-title-size);
|
|
33
|
+
font-weight: 700;
|
|
34
|
+
letter-spacing: -0.03em;
|
|
35
|
+
line-height: 1.08;
|
|
36
|
+
margin: 0 0 0.45rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.home-start-screen__panel {
|
|
40
|
+
padding: var(--generated-ui-screen-panel-padding);
|
|
41
|
+
}
|
|
42
|
+
</style>
|
|
@@ -1,88 +1,12 @@
|
|
|
1
|
-
<script
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
surfaceLabel: {
|
|
11
|
-
type: String,
|
|
12
|
-
default: ""
|
|
13
|
-
},
|
|
14
|
-
title: {
|
|
15
|
-
type: String,
|
|
16
|
-
default: ""
|
|
17
|
-
},
|
|
18
|
-
subtitle: {
|
|
19
|
-
type: String,
|
|
20
|
-
default: ""
|
|
1
|
+
<script>
|
|
2
|
+
import { h } from "vue";
|
|
3
|
+
import PackageShellLayout from "@jskit-ai/shell-web/client/components/ShellLayout";
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
name: "ShellLayout",
|
|
7
|
+
inheritAttrs: false,
|
|
8
|
+
setup(_, { attrs, slots }) {
|
|
9
|
+
return () => h(PackageShellLayout, attrs, slots);
|
|
21
10
|
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const { drawerOpen, toggleDrawer, resolvedSurface, resolvedSurfaceLabel } = useShellLayoutState(props);
|
|
11
|
+
};
|
|
25
12
|
</script>
|
|
26
|
-
|
|
27
|
-
<template>
|
|
28
|
-
<v-layout class="shell-layout border rounded-lg overflow-hidden">
|
|
29
|
-
<v-app-bar border density="comfortable" elevation="0" class="bg-surface">
|
|
30
|
-
<v-app-bar-nav-icon aria-label="Toggle navigation menu" @click="toggleDrawer" />
|
|
31
|
-
|
|
32
|
-
<slot name="top-left" :surface="resolvedSurface">
|
|
33
|
-
<div class="d-flex align-center ga-2">
|
|
34
|
-
<v-chip color="primary" size="small" label>{{ resolvedSurfaceLabel }}</v-chip>
|
|
35
|
-
<ShellOutlet target="shell-layout:top-left" />
|
|
36
|
-
</div>
|
|
37
|
-
</slot>
|
|
38
|
-
|
|
39
|
-
<v-spacer />
|
|
40
|
-
|
|
41
|
-
<slot name="top-right" :surface="resolvedSurface">
|
|
42
|
-
<div class="d-flex align-center ga-2">
|
|
43
|
-
<ShellOutlet target="shell-layout:top-right" />
|
|
44
|
-
</div>
|
|
45
|
-
</slot>
|
|
46
|
-
</v-app-bar>
|
|
47
|
-
|
|
48
|
-
<v-navigation-drawer v-model="drawerOpen" border class="bg-surface" :width="248">
|
|
49
|
-
<slot name="menu" :surface="resolvedSurface">
|
|
50
|
-
<v-list nav density="comfortable" class="pt-2">
|
|
51
|
-
<v-list-subheader class="text-uppercase text-caption">Navigation</v-list-subheader>
|
|
52
|
-
<ShellOutlet
|
|
53
|
-
target="shell-layout:primary-menu"
|
|
54
|
-
default
|
|
55
|
-
/>
|
|
56
|
-
<v-divider class="my-2" />
|
|
57
|
-
<ShellOutlet target="shell-layout:secondary-menu" />
|
|
58
|
-
</v-list>
|
|
59
|
-
</slot>
|
|
60
|
-
</v-navigation-drawer>
|
|
61
|
-
|
|
62
|
-
<v-main class="bg-background">
|
|
63
|
-
<v-container fluid class="shell-layout__content">
|
|
64
|
-
<h1 v-if="title" class="shell-layout__title text-h5">{{ title }}</h1>
|
|
65
|
-
<p v-if="subtitle" class="shell-layout__subtitle text-body-2 text-medium-emphasis">{{ subtitle }}</p>
|
|
66
|
-
<slot />
|
|
67
|
-
</v-container>
|
|
68
|
-
</v-main>
|
|
69
|
-
</v-layout>
|
|
70
|
-
</template>
|
|
71
|
-
|
|
72
|
-
<style scoped>
|
|
73
|
-
.shell-layout {
|
|
74
|
-
min-height: 72vh;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.shell-layout__content {
|
|
78
|
-
padding: 0.75rem 1rem 1rem;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.shell-layout__title {
|
|
82
|
-
margin-bottom: 0.25rem;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
.shell-layout__subtitle {
|
|
86
|
-
margin-bottom: 0.75rem;
|
|
87
|
-
}
|
|
88
|
-
</style>
|
package/templates/src/error.js
CHANGED
|
@@ -2,12 +2,18 @@ import { createDefaultErrorPolicy } from "@jskit-ai/shell-web/client/error";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* App-owned error handling contract.
|
|
5
|
+
* - intent chooses default presentation: resource-load, action-feedback, app-recoverable, blocking.
|
|
5
6
|
* - policy(event, ctx): decide channel + presenter + message.
|
|
6
7
|
* - defaultPresenterId: used when policy does not set presenterId.
|
|
7
8
|
* - presenters: optional custom presenters registered at boot.
|
|
8
9
|
*/
|
|
9
10
|
export default Object.freeze({
|
|
10
11
|
defaultPresenterId: "material.snackbar",
|
|
11
|
-
policy: createDefaultErrorPolicy(
|
|
12
|
+
policy: createDefaultErrorPolicy({
|
|
13
|
+
resourceLoadChannel: "silent",
|
|
14
|
+
actionFeedbackChannel: "snackbar",
|
|
15
|
+
appRecoverableChannel: "banner",
|
|
16
|
+
blockingChannel: "dialog"
|
|
17
|
+
}),
|
|
12
18
|
presenters: []
|
|
13
19
|
});
|