@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.
Files changed (29) hide show
  1. package/package.descriptor.mjs +74 -9
  2. package/package.json +8 -7
  3. package/src/client/components/ShellErrorHost.vue +88 -15
  4. package/src/client/components/ShellLayout.vue +551 -46
  5. package/src/client/components/ShellRouteTransition.vue +480 -0
  6. package/src/client/components/ShellTabLinkItem.vue +22 -6
  7. package/src/client/composables/useShellLayoutState.js +12 -1
  8. package/src/client/error/normalize.js +17 -0
  9. package/src/client/error/policy.js +25 -11
  10. package/src/client/error/runtime.js +2 -0
  11. package/src/client/index.js +1 -0
  12. package/src/client/providers/ShellWebClientProvider.js +163 -39
  13. package/src/client/stores/useShellLayoutStore.js +21 -1
  14. package/src/test/adaptiveShellSmoke.js +121 -0
  15. package/templates/expected-existing/src/pages/home/index.vue +40 -10
  16. package/templates/src/components/ShellLayout.vue +10 -86
  17. package/templates/src/components/menus/TabLinkItem.vue +4 -0
  18. package/templates/src/error.js +7 -1
  19. package/templates/src/pages/home/index.vue +64 -23
  20. package/templates/src/pages/home/settings/general/index.vue +12 -9
  21. package/templates/src/pages/home/settings.vue +68 -21
  22. package/templates/src/placementTopology.js +43 -2
  23. package/templates/tests/e2e/adaptive-shell.spec.ts +4 -0
  24. package/test/errorRuntime.test.js +42 -0
  25. package/test/linkItemScaffoldContract.test.js +9 -2
  26. package/test/placementRuntime.test.js +37 -0
  27. package/test/provider.test.js +97 -5
  28. package/test/settingsPlacementContract.test.js +205 -8
  29. 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
- const resolved = typeof exported === "function" ? exported() : exported;
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("shell.web.query-client", () => createShellWebQueryClient());
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
- <v-card class="mx-auto" max-width="960" rounded="xl" border elevation="1">
3
- <v-card-item class="px-6 py-5 px-md-8 py-md-7">
4
- <v-card-title class="text-h4">welcome</v-card-title>
5
- <v-card-subtitle class="text-subtitle-1 mt-2">starter app</v-card-subtitle>
6
- </v-card-item>
7
- <v-divider />
8
- <v-card-text class="px-6 py-5 px-md-8 py-md-7 text-body-1 text-medium-emphasis">
9
- Start by adding packages and pages to this app.
10
- </v-card-text>
11
- </v-card>
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 setup>
2
- import { useShellLayoutState } from "@jskit-ai/shell-web/client/composables/useShellLayoutState";
3
- import ShellOutlet from "@jskit-ai/shell-web/client/components/ShellOutlet";
4
-
5
- const props = defineProps({
6
- surface: {
7
- type: String,
8
- default: ""
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>
@@ -29,6 +29,10 @@ const props = defineProps({
29
29
  disabled: {
30
30
  type: Boolean,
31
31
  default: false
32
+ },
33
+ exact: {
34
+ type: Boolean,
35
+ default: false
32
36
  }
33
37
  });
34
38
  </script>
@@ -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
  });