@k2b/cloud 0.40.0 → 0.42.0
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.json +9 -5
- package/src/_internal/bounded-json.ts +14 -2
- package/src/_internal/capabilities.ts +87 -8
- package/src/_internal/capability-reviews.ts +6 -0
- package/src/_internal/capability-sentences.ts +2 -1
- package/src/_internal/capability-timeout.ts +71 -0
- package/src/_internal/define-app.ts +158 -92
- package/src/_internal/heartbeat.ts +203 -14
- package/src/_internal/http-drain.ts +56 -0
- package/src/_internal/registry-validation.ts +3 -0
- package/src/_internal/registry.ts +28 -1
- package/src/_internal/runtime-context.ts +1 -0
- package/src/_internal/setup-lock.ts +103 -0
- package/src/_internal/status-preserving-ssr.ts +2 -0
- package/src/access/PermissionEditor.tsx +16 -17
- package/src/access/PrincipalPicker.tsx +45 -10
- package/src/access/messages.ts +8 -0
- package/src/access/principal-label.ts +18 -0
- package/src/access/ui.ts +1 -0
- package/src/account/EntitySearch.tsx +5 -4
- package/src/ai/action-review.ts +2 -1
- package/src/ai/app-skill-store.ts +1 -1
- package/src/ai/capabilities.ts +21 -3
- package/src/ai/capability-execution.ts +14 -12
- package/src/ai/chat/action-card.ts +76 -4
- package/src/ai/chat/action-messages.ts +14 -0
- package/src/ai/chat/action-question.browser-harness.tsx +12 -0
- package/src/ai/chat/action-question.tsx +214 -23
- package/src/ai/chat/action-text.ts +288 -0
- package/src/ai/chat/tool-action-words.ts +31 -0
- package/src/ai/chat/tool-action.tsx +1 -26
- package/src/ai/chat-tasks.ts +2 -1
- package/src/ai/client/controller.ts +150 -32
- package/src/ai/client/transport.ts +14 -2
- package/src/ai/code-capability-routes.ts +27 -23
- package/src/ai/code-runtime-tools.ts +14 -16
- package/src/ai/code-source-tools.ts +16 -18
- package/src/ai/executor.ts +64 -21
- package/src/ai/fetch-file-tool.ts +67 -76
- package/src/ai/firecrawl-tools.ts +15 -2
- package/src/ai/http.ts +1 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/migrate.ts +28 -0
- package/src/ai/open-questions.ts +168 -0
- package/src/ai/projects.ts +1 -2
- package/src/ai/protocol.ts +2 -2
- package/src/ai/question-text.ts +8 -0
- package/src/ai/recent-resources.ts +12 -5
- package/src/ai/review-content.ts +77 -0
- package/src/ai/routes.ts +119 -35
- package/src/ai/runtime.ts +1 -0
- package/src/ai/skill-tool.ts +17 -6
- package/src/ai/skills-routes.ts +72 -27
- package/src/ai/skills.ts +139 -60
- package/src/ai/store.ts +184 -31
- package/src/ai/testing.ts +36 -0
- package/src/ai/tool-sentences.ts +152 -34
- package/src/ai/tool-wording.ts +12 -6
- package/src/ai/tools.ts +8 -3
- package/src/ai/types.ts +25 -4
- package/src/ai/usage.ts +2 -2
- package/src/api/admin-core-settings.ts +15 -2
- package/src/api/admin-geo.ts +54 -0
- package/src/api/admin-linux-identities.ts +6 -3
- package/src/api/admin-outgoing-mail.ts +29 -15
- package/src/api/app-approval-actions.ts +31 -7
- package/src/api/approvals-loader.ts +3 -3
- package/src/api/approvals.ts +51 -0
- package/src/api/apps.ts +4 -2
- package/src/api/auth/schemas.ts +3 -0
- package/src/api/auth.ts +54 -18
- package/src/api/capabilities.ts +258 -181
- package/src/api/capability-streams.ts +21 -13
- package/src/api/cli-plugins.ts +4 -1
- package/src/api/geo-glyphs.ts +11 -0
- package/src/api/geo.ts +128 -0
- package/src/api/help.ts +12 -4
- package/src/api/index.ts +5 -0
- package/src/api/mcp.ts +73 -41
- package/src/api/me-panel-consent.ts +8 -5
- package/src/api/me.ts +79 -3
- package/src/api/search.ts +21 -14
- package/src/api/sync-ops.ts +19 -13
- package/src/api/widgets.ts +42 -18
- package/src/approvals/Approvals.tsx +30 -66
- package/src/approvals/approval-card.ts +68 -0
- package/src/browser/CloudResourceSearch.browser-harness.tsx +53 -1
- package/src/browser/CloudResourceSearch.tsx +3 -13
- package/src/browser/PanelLeaveNotice.tsx +18 -0
- package/src/browser/navigation-search.ts +78 -25
- package/src/browser/panel-leave.ts +69 -0
- package/src/browser/panel-messages.ts +12 -0
- package/src/browser/panel-slot.ts +173 -24
- package/src/browser/panel.ts +3 -0
- package/src/browser/resource-search-messages.ts +2 -0
- package/src/capabilities/client.ts +28 -1
- package/src/capabilities/executions.ts +22 -3
- package/src/capabilities/geo-client.ts +57 -0
- package/src/capabilities/response.ts +2 -1
- package/src/capabilities/review-storage.ts +24 -0
- package/src/capabilities/reviews.ts +17 -3
- package/src/capabilities/server.ts +36 -16
- package/src/cli/access.ts +15 -23
- package/src/cli/admin/linux.ts +13 -2
- package/src/cli/admin/outgoing-mail.ts +26 -4
- package/src/cli/capabilities.ts +19 -0
- package/src/config/env.ts +0 -6
- package/src/contracts/app-access.ts +48 -0
- package/src/contracts/app-approval.ts +3 -3
- package/src/contracts/app.ts +4 -1
- package/src/contracts/approvals.ts +2 -2
- package/src/contracts/avatar.ts +186 -0
- package/src/contracts/capabilities.ts +149 -55
- package/src/contracts/geo-admin.ts +127 -0
- package/src/contracts/geo-map.ts +19 -0
- package/src/contracts/geo-route.ts +168 -0
- package/src/contracts/geo.ts +248 -0
- package/src/contracts/index.ts +4 -0
- package/src/contracts/outgoing-mail.ts +17 -4
- package/src/contracts/registry.ts +2 -0
- package/src/contracts/shared.ts +3 -0
- package/src/events/app-access.ts +68 -0
- package/src/events/live-delivery.ts +128 -0
- package/src/events/live.ts +14 -130
- package/src/geo/CloudMap.tsx +95 -0
- package/src/geo/map-messages.ts +17 -0
- package/src/geo/ui.ts +1 -0
- package/src/index.ts +2 -0
- package/src/server/client-ip.ts +36 -0
- package/src/server/index.ts +3 -4
- package/src/server/middleware/app-access.ts +312 -0
- package/src/server/middleware/auth.ts +5 -3
- package/src/server/middleware/invocation.ts +11 -0
- package/src/server/middleware/rate-limit.ts +4 -4
- package/src/server/services/access.ts +13 -9
- package/src/server/services/index.ts +0 -2
- package/src/server/services/services.ts +0 -2
- package/src/services/accounts/app.ts +23 -3
- package/src/services/accounts/posix.ts +21 -7
- package/src/services/accounts/users.ts +12 -1
- package/src/services/allowed-network-fetch.ts +16 -10
- package/src/services/app-access-metrics.ts +24 -0
- package/src/services/app-access.ts +123 -0
- package/src/services/app-discovery.ts +27 -0
- package/src/services/approvals/content.ts +40 -0
- package/src/services/approvals/device.ts +49 -33
- package/src/services/approvals/index.ts +15 -6
- package/src/services/approvals/list.ts +6 -11
- package/src/services/approvals/question.ts +72 -0
- package/src/services/audit/index.ts +1 -0
- package/src/services/auth-flows/ipa.ts +39 -2
- package/src/services/auth-flows/password-reset.ts +32 -6
- package/src/services/downstream-budget.ts +87 -0
- package/src/services/geo/admin.ts +262 -0
- package/src/services/geo/bucket.ts +67 -0
- package/src/services/geo/cache.ts +57 -0
- package/src/services/geo/costs.ts +5 -0
- package/src/services/geo/errors.ts +41 -0
- package/src/services/geo/fixtures/README.md +17 -0
- package/src/services/geo/fixtures/along.json +355 -0
- package/src/services/geo/fixtures/autocomplete.json +218 -0
- package/src/services/geo/fixtures/geocode.json +129 -0
- package/src/services/geo/fixtures/places.json +355 -0
- package/src/services/geo/fixtures/reverse.json +61 -0
- package/src/services/geo/fixtures/routing.json +57 -0
- package/src/services/geo/geoapify-route.ts +93 -0
- package/src/services/geo/geoapify.ts +281 -0
- package/src/services/geo/ledger.ts +122 -0
- package/src/services/geo/map.ts +160 -0
- package/src/services/geo/migrate.ts +25 -0
- package/src/services/geo/polyline.ts +184 -0
- package/src/services/geo/route-chain.ts +145 -0
- package/src/services/geo/route-costs.ts +9 -0
- package/src/services/geo/route-geometry.ts +39 -0
- package/src/services/geo/route-plan.ts +172 -0
- package/src/services/geo/route-service.ts +433 -0
- package/src/services/geo/service.ts +300 -0
- package/src/services/geo/settings.ts +49 -0
- package/src/services/help/index.ts +16 -7
- package/src/services/help/types.ts +6 -1
- package/src/services/identity/invocation-actor.ts +20 -2
- package/src/services/identity/issue-invocation.ts +67 -0
- package/src/services/identity/key-ring.ts +5 -3
- package/src/services/identity/runtime-config.ts +23 -0
- package/src/services/index.ts +11 -0
- package/src/services/mandates/index.ts +40 -3
- package/src/services/network-security.ts +6 -3
- package/src/services/network-transport.ts +99 -0
- package/src/services/notifications/catalog.ts +3 -2
- package/src/services/notifications/runtime.ts +1 -0
- package/src/services/notifications/web-push-transport.ts +45 -38
- package/src/services/outgoing-mail/dispatcher.ts +6 -1
- package/src/services/outgoing-mail/index.ts +55 -1
- package/src/services/outgoing-mail/messages.ts +82 -3
- package/src/services/outgoing-mail/resolve-host.ts +2 -2
- package/src/services/outgoing-mail/retention.ts +13 -1
- package/src/services/outgoing-mail/store.ts +36 -6
- package/src/services/outgoing-mail/test-send.ts +34 -4
- package/src/services/outgoing-mail/transport.ts +1 -1
- package/src/services/pdf/gotenberg.ts +11 -38
- package/src/services/pdf/markdown.ts +160 -2
- package/src/services/pdf/template-preview.ts +1 -1
- package/src/services/public-http.ts +28 -40
- package/src/services/session/index.ts +203 -29
- package/src/services/settings/app.ts +4 -1
- package/src/services/settings/core-settings.ts +88 -0
- package/src/services/settings/defaults.ts +8 -2
- package/src/services/settings/errors.ts +11 -0
- package/src/services/settings/geo-budget.ts +40 -0
- package/src/services/settings/index.ts +4 -3
- package/src/services/settings/store.ts +25 -17
- package/src/services/weather/geo.ts +29 -111
- package/src/services/weather/index.ts +4 -3
- package/src/services/weather/locations.ts +8 -5
- package/src/services/weather/migrate.ts +1 -0
- package/src/services/weather/settings.ts +0 -16
- package/src/shared/__snapshots__/avatar-art.test.ts.snap +5 -0
- package/src/shared/avatar-art.ts +400 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/network-address.ts +22 -3
- package/src/shared/template-rendering.ts +36 -4
- package/src/shared-text/documents.ts +1 -1
- package/src/shared-text/index.ts +2 -0
- package/src/shared-text/log.ts +51 -3
- package/src/shared-text/snapshots.ts +340 -0
- package/src/ssr/AdminLayout.tsx +4 -2
- package/src/ssr/CloudShell.island.tsx +6 -0
- package/src/ssr/HelpPanelArticle.island.tsx +308 -0
- package/src/ssr/HelpPanelBack.island.tsx +32 -0
- package/src/ssr/HelpPanelMenu.island.tsx +133 -0
- package/src/ssr/HelpPanelSearch.island.tsx +123 -0
- package/src/ssr/HelpPanelShortcuts.island.tsx +22 -0
- package/src/ssr/HelpPanelView.island.tsx +27 -0
- package/src/ssr/Layout.tsx +65 -15
- package/src/ssr/LayoutHelp.browser-harness.tsx +1 -1
- package/src/ssr/LayoutHelp.tsx +185 -542
- package/src/ssr/LayoutHelpPanel.browser-harness.tsx +6 -7
- package/src/ssr/LayoutHelpPanel.server.browser-harness.tsx +131 -0
- package/src/ssr/LayoutHelpPanel.server.tsx +115 -0
- package/src/ssr/PanelRuntime.island.tsx +11 -7
- package/src/ssr/PwaLayout.tsx +2 -2
- package/src/ssr/RailApps.tsx +6 -0
- package/src/ssr/ShellRuntime.island.tsx +23 -7
- package/src/ssr/__snapshots__/Layout.unchanged.render.test.tsx.snap +151 -0
- package/src/ssr/__snapshots__/LayoutHelp.ssr.render.test.tsx.snap +29 -0
- package/src/ssr/admin-navigation.ts +1 -0
- package/src/ssr/app-badges.ts +6 -1
- package/src/ssr/compact-shell.browser-harness.ts +44 -5
- package/src/ssr/help.ts +7 -5
- package/src/ssr/index.ts +2 -1
- package/src/ssr/islands/index.ts +3 -4
- package/src/ssr/layout-context.ts +2 -0
- package/src/ssr/layout-help-panel-controls.tsx +43 -0
- package/src/ssr/layout-help-panel-document.ts +67 -0
- package/src/ssr/layout-help-panel-lifecycle.ts +32 -0
- package/src/ssr/layout-help-panel-menu.tsx +113 -0
- package/src/ssr/layout-help-panel-modules.ts +7 -0
- package/src/ssr/layout-help-panel-reader.tsx +159 -0
- package/src/ssr/layout-help-panel-search.tsx +69 -0
- package/src/ssr/layout-help-panel-shortcuts.ts +7 -0
- package/src/ssr/layout-help-panel-state.ts +141 -0
- package/src/ssr/layout-help-panel-types.ts +19 -0
- package/src/ssr/layout-help-views.tsx +484 -0
- package/src/ssr/mobile-menu-history.ts +4 -0
- package/src/ssr/page-swap-fixture/Column.island.tsx +15 -0
- package/src/ssr/page-swap-fixture/ProviderProbe.island.tsx +31 -0
- package/src/ssr/page-swap.browser-harness.tsx +177 -0
- package/src/ssr/page-swap.ts +28 -0
- package/src/ssr/page-trail-runtime.ts +32 -13
- package/src/ssr/panel-consent.ts +2 -2
- package/src/ssr/panel-idle.ts +9 -0
- package/src/ssr/panel-slot.browser-harness.tsx +3 -0
- package/src/ssr/platform-messages.ts +8 -0
- package/src/ssr/runtime.ts +17 -0
- package/src/ssr/shell-data.tsx +10 -2
- package/src/styles/approvals.css +6 -0
- package/src/styles/effects.css +15 -0
- package/src/styles/global.css +70 -6
- package/src/server/services/geo.ts +0 -215
|
@@ -3,49 +3,102 @@ import type { SearchItem } from "../api/search/schemas";
|
|
|
3
3
|
import type { AppMeta } from "../contracts/app";
|
|
4
4
|
|
|
5
5
|
/** UI-only rows. The synthetic ref is a row key, never a readable resource. */
|
|
6
|
-
export type NavigationSearchItem = SearchItem & { keywords?: readonly string[] };
|
|
6
|
+
export type NavigationSearchItem = SearchItem & { keywords?: readonly string[]; kind?: "app" };
|
|
7
7
|
|
|
8
8
|
export const navigationSearchItems = (
|
|
9
|
-
apps: readonly Pick<AppMeta, "id" | "name" | "icon" | "searchLinks">[],
|
|
9
|
+
apps: readonly (Pick<AppMeta, "id" | "name" | "icon" | "searchLinks"> & { nav?: Pick<NonNullable<AppMeta["nav"]>, "href"> })[],
|
|
10
10
|
visibleAppIds: readonly string[],
|
|
11
11
|
): NavigationSearchItem[] => {
|
|
12
12
|
const visible = new Set(visibleAppIds);
|
|
13
13
|
return apps
|
|
14
14
|
.filter((app) => visible.has(app.id))
|
|
15
|
-
.flatMap((app) =>
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
.flatMap((app) => {
|
|
16
|
+
const entryLinks = app.searchLinks?.filter((link) => link.href === app.nav?.href) ?? [];
|
|
17
|
+
const links = [
|
|
18
|
+
...(app.nav
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
label: app.name,
|
|
22
|
+
href: app.nav.href,
|
|
23
|
+
icon: app.icon,
|
|
24
|
+
kind: "app" as const,
|
|
25
|
+
keywords: entryLinks.flatMap((link) => [link.label, ...(link.keywords ?? [])]),
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
: []),
|
|
29
|
+
...(app.searchLinks ?? []),
|
|
30
|
+
];
|
|
31
|
+
return links.map(
|
|
32
|
+
(link): NavigationSearchItem => ({
|
|
33
|
+
appId: app.id,
|
|
34
|
+
appName: app.name,
|
|
35
|
+
appIcon: app.icon,
|
|
36
|
+
ref: { type: "cloud.navigation", id: `${app.id}:${link.href}` },
|
|
37
|
+
readable: false,
|
|
38
|
+
title: link.label,
|
|
39
|
+
preview: "description" in link ? link.description : undefined,
|
|
40
|
+
href: link.href,
|
|
41
|
+
icon: link.icon ?? app.icon,
|
|
42
|
+
priority: 0,
|
|
43
|
+
keywords: link.keywords,
|
|
44
|
+
kind: "kind" in link ? link.kind : undefined,
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const normalize = (text: string) => text.trim().toLowerCase().replace(/\s+/g, " ");
|
|
51
|
+
|
|
52
|
+
/** Compare individual names/aliases, never their combined length or number. */
|
|
53
|
+
const appMatchRank = (item: NavigationSearchItem, query: string): number => {
|
|
54
|
+
const names = [item.title, ...(item.keywords ?? [])].map(normalize);
|
|
55
|
+
if (names.includes(query)) return 0;
|
|
56
|
+
const words = query.split(" ");
|
|
57
|
+
if (names.some((name) => words.every((word) => name.split(/[^\p{L}\p{N}]+/u).some((part) => part.startsWith(word))))) return 1;
|
|
58
|
+
if (names.some((name) => words.every((word) => name.includes(word)))) return 2;
|
|
59
|
+
// Whole-name typos only, at least 80% similarity. No short abbreviations or description fuzzing globally.
|
|
60
|
+
if (query.length < 4) return Infinity;
|
|
61
|
+
const closest = fuzzy.closest(query, names, { maxDistance: query.length >= 8 ? 2 : 1 });
|
|
62
|
+
return closest && closest.similarity >= 0.8 ? 3 + closest.distance / 10 : Infinity;
|
|
30
63
|
};
|
|
31
64
|
|
|
32
65
|
export const matchNavigationSearchItems = (
|
|
33
66
|
items: readonly NavigationSearchItem[],
|
|
34
67
|
input: { query: string; tags: readonly string[]; appId?: string | null; requireReader?: boolean },
|
|
35
68
|
): NavigationSearchItem[] => {
|
|
36
|
-
const query = input.query
|
|
69
|
+
const query = normalize(input.query);
|
|
37
70
|
if (input.requireReader || input.tags.length > 0 || (!input.appId && query.length < 2)) return [];
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
const uniqueTarget = (item: NavigationSearchItem) => {
|
|
73
|
+
if (seen.has(item.href)) return false;
|
|
74
|
+
seen.add(item.href);
|
|
75
|
+
return true;
|
|
76
|
+
};
|
|
38
77
|
if (input.appId) {
|
|
39
|
-
|
|
78
|
+
// App contexts keep browsing their static pages; app launchers belong to global navigation only.
|
|
79
|
+
const scoped = items.filter((item) => item.appId === input.appId && item.kind !== "app");
|
|
40
80
|
return query
|
|
41
81
|
? fuzzy
|
|
42
82
|
.filter(query, scoped, { key: (item) => [item.title, item.preview ?? "", ...(item.keywords ?? [])].join(" ") })
|
|
43
83
|
.map((hit) => hit.item)
|
|
44
|
-
|
|
84
|
+
.filter(uniqueTarget)
|
|
85
|
+
: scoped.filter(uniqueTarget);
|
|
45
86
|
}
|
|
46
|
-
const words = query.split(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
87
|
+
const words = query.split(" ");
|
|
88
|
+
// Deduplicate only this global view. A cross-app page still belongs to its declaring app's own scope.
|
|
89
|
+
const appTargets = new Set(items.filter((item) => item.kind === "app").map((item) => item.href));
|
|
90
|
+
return items
|
|
91
|
+
.filter((item) => item.kind === "app" || !appTargets.has(item.href))
|
|
92
|
+
.flatMap((item) => {
|
|
93
|
+
const rank =
|
|
94
|
+
item.kind === "app"
|
|
95
|
+
? appMatchRank(item, query)
|
|
96
|
+
: words.every((word) => [item.title, item.preview ?? "", ...(item.keywords ?? [])].join(" ").toLowerCase().includes(word))
|
|
97
|
+
? 2
|
|
98
|
+
: Infinity;
|
|
99
|
+
return Number.isFinite(rank) ? [{ item, rank }] : [];
|
|
100
|
+
})
|
|
101
|
+
.sort((a, b) => a.rank - b.rank)
|
|
102
|
+
.map(({ item }) => item)
|
|
103
|
+
.filter(uniqueTarget);
|
|
51
104
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** Internal same-origin collection; no composer content crosses the provider boundary. */
|
|
2
|
+
const LEAVE = "cloud.panel.before-leave";
|
|
3
|
+
const CHECK = "cloud.panel.has-leave-guard";
|
|
4
|
+
type LeaveDecision = { allowed: boolean };
|
|
5
|
+
|
|
6
|
+
/** Ordinary same-origin links handled by the panel host; explicit browser actions stay native. */
|
|
7
|
+
export function panelLinkDestination(event: MouseEvent): URL | undefined {
|
|
8
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return;
|
|
9
|
+
const anchor = event.target instanceof Element ? event.target.closest<HTMLAnchorElement>("a[href]") : null;
|
|
10
|
+
if (!anchor || (anchor.target && anchor.target !== "_self") || anchor.hasAttribute("download")) return;
|
|
11
|
+
const url = new URL(anchor.getAttribute("href")!, document.baseURI);
|
|
12
|
+
if (url.origin !== location.origin) return;
|
|
13
|
+
if (url.pathname === location.pathname && url.search === location.search && url.hash) return;
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Register before enabling editing. Return true only when all provider work is recoverable.
|
|
19
|
+
* A veto keeps the current frame. Saving and retry belong to the provider; no transition is queued.
|
|
20
|
+
*/
|
|
21
|
+
export function onPanelBeforeLeave(guard: () => boolean): () => void {
|
|
22
|
+
const collect = (event: Event) => {
|
|
23
|
+
const decision = (event as CustomEvent<LeaveDecision>).detail;
|
|
24
|
+
try {
|
|
25
|
+
// Only explicit synchronous true permits leaving; throws and accidental Promises fail closed.
|
|
26
|
+
if (guard() !== true) decision.allowed = false;
|
|
27
|
+
} catch {
|
|
28
|
+
decision.allowed = false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const registered = (event: Event) => event.preventDefault();
|
|
32
|
+
// Islands start independently: editing and its links must be protected before PanelRuntime loads too.
|
|
33
|
+
const click = (event: MouseEvent) => {
|
|
34
|
+
if (window.parent === window || !document.querySelector("[data-cloud-panel-page]")) return;
|
|
35
|
+
const url = panelLinkDestination(event);
|
|
36
|
+
if (!url) return;
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
window.parent.postMessage({ type: "cloud.panel.navigate", v: 1, href: url.href }, location.origin);
|
|
39
|
+
};
|
|
40
|
+
window.addEventListener(LEAVE, collect);
|
|
41
|
+
window.addEventListener(CHECK, registered);
|
|
42
|
+
document.addEventListener("click", click);
|
|
43
|
+
return () => {
|
|
44
|
+
window.removeEventListener(LEAVE, collect);
|
|
45
|
+
window.removeEventListener(CHECK, registered);
|
|
46
|
+
document.removeEventListener("click", click);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function hasPanelLeaveGuard(): boolean {
|
|
51
|
+
return !window.dispatchEvent(new Event(CHECK, { cancelable: true }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function panelCanLeave(target: Window): boolean {
|
|
55
|
+
// A cross-origin navigation already destroyed the provider document and its guards.
|
|
56
|
+
// It cannot participate in this same-origin contract or keep the host's slot trapped.
|
|
57
|
+
try {
|
|
58
|
+
void target.document;
|
|
59
|
+
} catch {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const decision: LeaveDecision = { allowed: true };
|
|
63
|
+
try {
|
|
64
|
+
target.dispatchEvent(new CustomEvent<LeaveDecision>(LEAVE, { detail: decision }));
|
|
65
|
+
return decision.allowed;
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2
2
|
|
|
3
|
+
export function isPanelNavigateMessage(value: unknown): value is { type: "cloud.panel.navigate"; v: 1; href: string } {
|
|
4
|
+
return (
|
|
5
|
+
isRecord(value) &&
|
|
6
|
+
Reflect.ownKeys(value).length === 3 &&
|
|
7
|
+
value.type === "cloud.panel.navigate" &&
|
|
8
|
+
value.v === 1 &&
|
|
9
|
+
typeof value.href === "string" &&
|
|
10
|
+
value.href.length <= 8192 &&
|
|
11
|
+
/^https?:\/\//.test(value.href)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export function isPanelReadyMessage(value: unknown, provider: string): value is { type: "cloud.panel.ready"; v: 1; provider: string } {
|
|
4
16
|
return (
|
|
5
17
|
isRecord(value) &&
|
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { attachResizeSeparator, LocaleProvider, parkWorkspaceDetailDrawers } from "@k2b/ui";
|
|
2
|
-
import { createComponent } from "solid-js";
|
|
2
|
+
import { createComponent, createSignal } from "solid-js";
|
|
3
3
|
import { render } from "solid-js/web";
|
|
4
4
|
import { PANEL_MAX_WIDTH_REM, PANEL_MIN_WIDTH_REM, readPanelCookie, serializePanelCookie } from "../_internal/panel-cookie";
|
|
5
5
|
import { CloudResourceRefSchema } from "../contracts/capabilities";
|
|
6
6
|
import type { PanelPageContext } from "../contracts/panel-context";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
isPanelPageContext,
|
|
9
|
+
isPanelShortcut,
|
|
10
|
+
PANEL_CONTEXT_PARAM,
|
|
11
|
+
panelProviderNeedsConsent,
|
|
12
|
+
withPanelContext,
|
|
13
|
+
} from "../contracts/panel-context-plain";
|
|
8
14
|
import { helpShortcutGroups } from "../ssr/layout-help-start";
|
|
9
15
|
import { platformMessages } from "../ssr/platform-messages";
|
|
10
16
|
import { readShellData } from "../ssr/shell-data";
|
|
11
17
|
import { COMMANDS_CHANGED, contextCommandsWithShortcuts } from "./command-shortcuts";
|
|
18
|
+
import PanelLeaveNotice from "./PanelLeaveNotice";
|
|
12
19
|
import type { PanelRequest } from "./panel";
|
|
13
20
|
import {
|
|
14
21
|
PAGE_CONTEXT_CHANGED,
|
|
@@ -17,8 +24,9 @@ import {
|
|
|
17
24
|
type PanelConsentRequest,
|
|
18
25
|
type PanelState,
|
|
19
26
|
} from "./panel-context-bridge";
|
|
27
|
+
import { panelCanLeave } from "./panel-leave";
|
|
20
28
|
import PanelMenu from "./panel-menu";
|
|
21
|
-
import { isPanelCloseMessage, isPanelReadyMessage, isPanelSearchMessage } from "./panel-messages";
|
|
29
|
+
import { isPanelCloseMessage, isPanelNavigateMessage, isPanelReadyMessage, isPanelSearchMessage } from "./panel-messages";
|
|
22
30
|
import { importOnDemand } from "./reload";
|
|
23
31
|
|
|
24
32
|
export const PANEL_READY_TIMEOUT_MS = 10_000;
|
|
@@ -53,13 +61,100 @@ export function installPanelSlot(): () => void {
|
|
|
53
61
|
let provider = panel.providers.find((item) => item.id === slot.dataset.provider) ?? panel.providers[0]!;
|
|
54
62
|
let frame = slot.querySelector<HTMLIFrameElement>("[data-cloud-panel-frame]");
|
|
55
63
|
let disposed = false;
|
|
56
|
-
|
|
64
|
+
const [leaveBlocked, setLeaveBlocked] = createSignal(false);
|
|
65
|
+
const transitionTo = (action: () => void) => {
|
|
66
|
+
const target = frame?.contentWindow;
|
|
67
|
+
if (target && !panelCanLeave(target)) {
|
|
68
|
+
setLeaveBlocked(true);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
setLeaveBlocked(false);
|
|
72
|
+
action();
|
|
73
|
+
};
|
|
74
|
+
const leaveHost = document.createElement("div");
|
|
75
|
+
body.before(leaveHost);
|
|
76
|
+
const disposeLeave = render(
|
|
77
|
+
() =>
|
|
78
|
+
createComponent(LocaleProvider, {
|
|
79
|
+
locale: lang,
|
|
80
|
+
get children() {
|
|
81
|
+
return createComponent(PanelLeaveNotice, {
|
|
82
|
+
get blocked() {
|
|
83
|
+
return leaveBlocked();
|
|
84
|
+
},
|
|
85
|
+
locale: lang,
|
|
86
|
+
resume: () => {
|
|
87
|
+
setLeaveBlocked(false);
|
|
88
|
+
frame?.focus();
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
}),
|
|
93
|
+
leaveHost,
|
|
94
|
+
);
|
|
95
|
+
let base = panel.context ?? null;
|
|
96
|
+
let context = base;
|
|
97
|
+
let pageRoot = document.querySelector("[data-nav-root]");
|
|
98
|
+
let documentPageId = context?.pageId;
|
|
99
|
+
let lastSeq = context?.seq ?? 0;
|
|
100
|
+
// The retained provider may have locked onto this document before a swap and this lazy installer.
|
|
101
|
+
try {
|
|
102
|
+
const raw = frame && new URL(frame.src, location.href).searchParams.get(PANEL_CONTEXT_PARAM);
|
|
103
|
+
const initial: unknown = raw ? JSON.parse(raw) : null;
|
|
104
|
+
if (isPanelPageContext(initial)) {
|
|
105
|
+
documentPageId = initial.pageId;
|
|
106
|
+
lastSeq = context?.pageId === documentPageId ? Math.max(lastSeq, initial.seq) : initial.seq;
|
|
107
|
+
if (context && context.pageId !== documentPageId) context = { ...context, pageId: documentPageId, seq: ++lastSeq };
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// A malformed or absent URL context leaves the shown page's context in charge.
|
|
111
|
+
}
|
|
112
|
+
// Set when a swapped page's context has not been delivered yet; cleared by the next delivery.
|
|
113
|
+
let pageUnsent = false;
|
|
114
|
+
const followPage = () => {
|
|
115
|
+
const nextRoot = document.querySelector("[data-nav-root]");
|
|
116
|
+
if (nextRoot === pageRoot) return;
|
|
117
|
+
pageRoot = nextRoot;
|
|
118
|
+
pageUnsent = true;
|
|
119
|
+
const arriving = readShellData()?.panel;
|
|
120
|
+
// The arriving page carries the server's current consent answer, as a reload would; a newer revision wins.
|
|
121
|
+
const consent = arriving?.consent;
|
|
122
|
+
if (consent) {
|
|
123
|
+
// Page observations fence pending choices even when a read already learned this revision or the provider changed.
|
|
124
|
+
if (consent.revision > (observedRevisions.get(consent.provider) ?? 0)) observedRevisions.set(consent.provider, consent.revision);
|
|
125
|
+
if (consent.revision > (revisions.get(consent.provider) ?? 0)) {
|
|
126
|
+
answers.set(consent.provider, consent.allowed);
|
|
127
|
+
revisions.set(consent.provider, consent.revision);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (consent && consent.provider === provider.id) {
|
|
131
|
+
if (consent.allowed !== answers.get(consent.provider) || consent.revision !== revisions.get(consent.provider)) {
|
|
132
|
+
// An older or unreadable answer (`allowed: null`) is what a reload would show: stop and read again.
|
|
133
|
+
void revalidate();
|
|
134
|
+
}
|
|
135
|
+
} else if (provider.consent && isOpen()) {
|
|
136
|
+
// The server sends an answer only while the shared cookie shows this provider open; without it, read again.
|
|
137
|
+
void revalidate();
|
|
138
|
+
} else if (consentPending === 0) {
|
|
139
|
+
// A reload would read these answers again when a consent provider opens; so does the next open here.
|
|
140
|
+
for (const item of panel.providers) if (item.consent) answers.delete(item.id);
|
|
141
|
+
answersRead = undefined;
|
|
142
|
+
}
|
|
143
|
+
base = arriving?.context ?? null;
|
|
144
|
+
if (base) {
|
|
145
|
+
documentPageId ??= base.pageId;
|
|
146
|
+
context = { ...base, pageId: documentPageId, seq: ++lastSeq };
|
|
147
|
+
} else context = null;
|
|
148
|
+
};
|
|
57
149
|
let loadVersion = 0;
|
|
58
150
|
const answers = new Map<string, boolean | null>();
|
|
59
151
|
const revisions = new Map<string, number>();
|
|
152
|
+
// A newer server page supersedes choices made before it, including queued saves and conflict retries.
|
|
153
|
+
const observedRevisions = new Map<string, number>();
|
|
60
154
|
if (panel.consent) {
|
|
61
155
|
answers.set(panel.consent.provider, panel.consent.allowed);
|
|
62
156
|
revisions.set(panel.consent.provider, panel.consent.revision);
|
|
157
|
+
observedRevisions.set(panel.consent.provider, panel.consent.revision);
|
|
63
158
|
}
|
|
64
159
|
const saving = new Set<string>();
|
|
65
160
|
const errors = new Set<string>();
|
|
@@ -78,13 +173,13 @@ export function installPanelSlot(): () => void {
|
|
|
78
173
|
// Whether the current frame has seen a context, in its URL or by message; only such a frame needs a clearing null.
|
|
79
174
|
let disclosed = !!frame && new URL(frame.src, location.origin).searchParams.has(PANEL_CONTEXT_PARAM);
|
|
80
175
|
// The frame may have navigated on its own; only the selected provider's page below its panel path may receive context.
|
|
81
|
-
const showsProvider = () => {
|
|
176
|
+
const showsProvider = (requireReady = true) => {
|
|
82
177
|
try {
|
|
83
178
|
const shown = frame?.contentWindow?.location;
|
|
84
179
|
const scope = new URL(provider.href, location.origin).pathname.replace(/\/$/, "");
|
|
85
180
|
return (
|
|
86
181
|
!!shown &&
|
|
87
|
-
frame?.contentDocument?.documentElement.dataset.cloudPanelReady === provider.id &&
|
|
182
|
+
(!requireReady || frame?.contentDocument?.documentElement.dataset.cloudPanelReady === provider.id) &&
|
|
88
183
|
(shown.pathname === scope || shown.pathname.startsWith(`${scope}/`))
|
|
89
184
|
);
|
|
90
185
|
} catch {
|
|
@@ -106,6 +201,7 @@ export function installPanelSlot(): () => void {
|
|
|
106
201
|
let lastShortcutsJson: string | undefined;
|
|
107
202
|
const canSend = () => isOpen() && allowed() && !!context && slot.dataset.state === "ready" && !!frame?.contentWindow && showsProvider();
|
|
108
203
|
const sendContext = () => {
|
|
204
|
+
pageUnsent = false;
|
|
109
205
|
if (!canSend()) return;
|
|
110
206
|
const list = !panelProviderNeedsConsent(provider.id) ? shortcuts() : undefined;
|
|
111
207
|
frame?.contentWindow?.postMessage(
|
|
@@ -158,15 +254,16 @@ export function installPanelSlot(): () => void {
|
|
|
158
254
|
if (forgetUrlContext()) disclosed = false;
|
|
159
255
|
};
|
|
160
256
|
const collectContext = () => {
|
|
257
|
+
followPage();
|
|
161
258
|
if (!context) return;
|
|
162
259
|
const collection: PageContextCollection = { sources: [] };
|
|
163
260
|
window.dispatchEvent(new CustomEvent(PAGE_CONTEXT_COLLECT, { detail: collection }));
|
|
164
261
|
const source = collection.sources.findLast((source) => source.scope !== undefined);
|
|
165
262
|
const parsed = source ? CloudResourceRefSchema.safeParse(source.scope) : undefined;
|
|
166
|
-
const scope = source ? (parsed?.success ? parsed.data : undefined) :
|
|
263
|
+
const scope = source ? (parsed?.success ? parsed.data : undefined) : base?.scope;
|
|
167
264
|
if (scope?.type === context.scope?.type && scope?.id === context.scope?.id) return;
|
|
168
|
-
const { scope: _previous, ...
|
|
169
|
-
context = { ...
|
|
265
|
+
const { scope: _previous, ...previous } = context;
|
|
266
|
+
context = { ...previous, seq: ++lastSeq, ...(scope ? { scope } : {}) };
|
|
170
267
|
sendContext();
|
|
171
268
|
};
|
|
172
269
|
// Share or stop sharing after every consent step; an open question or its absence follows in update().
|
|
@@ -220,8 +317,13 @@ export function installPanelSlot(): () => void {
|
|
|
220
317
|
}
|
|
221
318
|
for (const item of panel.providers) {
|
|
222
319
|
if (!item.consent) continue;
|
|
320
|
+
const knownRevision = revisions.get(item.id) ?? 0;
|
|
321
|
+
const readRevision = nextRevisions.get(item.id);
|
|
322
|
+
// A page swap can observe a newer answer while this request is in flight. Never rewind that answer.
|
|
323
|
+
if (readRevision !== undefined && readRevision < knownRevision) continue;
|
|
223
324
|
answers.set(item.id, next.get(item.id) ?? null);
|
|
224
|
-
|
|
325
|
+
// An unreadable answer still stops sharing, but must not forget the newest observed revision.
|
|
326
|
+
revisions.set(item.id, readRevision ?? knownRevision);
|
|
225
327
|
}
|
|
226
328
|
answersStale = false;
|
|
227
329
|
};
|
|
@@ -330,7 +432,7 @@ export function installPanelSlot(): () => void {
|
|
|
330
432
|
clearTimer();
|
|
331
433
|
slot.dataset.state = "ready";
|
|
332
434
|
if (status) status.textContent = "";
|
|
333
|
-
if (allowed()) {
|
|
435
|
+
if (allowed() && context) {
|
|
334
436
|
sendContext();
|
|
335
437
|
return;
|
|
336
438
|
}
|
|
@@ -427,6 +529,8 @@ export function installPanelSlot(): () => void {
|
|
|
427
529
|
if (!item || typeof request.allowed !== "boolean" || saving.has(item.id)) return;
|
|
428
530
|
saving.add(item.id);
|
|
429
531
|
errors.delete(item.id);
|
|
532
|
+
const observedRevision = observedRevisions.get(item.id);
|
|
533
|
+
const current = () => observedRevisions.get(item.id) === observedRevision;
|
|
430
534
|
const questionHadFocus = slot.querySelector("[data-cloud-panel-consent]")?.contains(document.activeElement);
|
|
431
535
|
// Enqueueing blocks sharing at once, so a withdrawal stops disclosure before its save even starts.
|
|
432
536
|
// keepalive: leaving the page does not cancel the save. A save the browser gives up on (timeout) is rolled back
|
|
@@ -441,11 +545,14 @@ export function installPanelSlot(): () => void {
|
|
|
441
545
|
});
|
|
442
546
|
void enqueueConsent(async () => {
|
|
443
547
|
try {
|
|
548
|
+
if (!current()) return;
|
|
444
549
|
let response = await put();
|
|
550
|
+
if (!current()) return;
|
|
445
551
|
// Changed meanwhile (another tab, or a read that failed): the person's choice now is the newest one, so read
|
|
446
552
|
// the current revision and save it once more. A second conflict shows the question again.
|
|
447
553
|
if (response.status === 409) {
|
|
448
554
|
await readAnswersNow();
|
|
555
|
+
if (!current()) return;
|
|
449
556
|
response = await put();
|
|
450
557
|
}
|
|
451
558
|
if (!response.ok) throw new Error("Panel consent was not saved");
|
|
@@ -463,13 +570,17 @@ export function installPanelSlot(): () => void {
|
|
|
463
570
|
saved.revision <= 0
|
|
464
571
|
)
|
|
465
572
|
throw new Error("Invalid panel consent answer");
|
|
466
|
-
|
|
467
|
-
|
|
573
|
+
if (current() && saved.revision >= (revisions.get(item.id) ?? 0)) {
|
|
574
|
+
answers.set(item.id, request.allowed);
|
|
575
|
+
revisions.set(item.id, saved.revision);
|
|
576
|
+
}
|
|
468
577
|
} catch {
|
|
578
|
+
// A superseded request cannot invalidate the newer page's answer either.
|
|
579
|
+
if (!current()) return;
|
|
469
580
|
// Not saved: nothing is shared until the person answers again; the read learns the current revision.
|
|
470
581
|
errors.add(item.id);
|
|
471
582
|
await readAnswersNow();
|
|
472
|
-
answers.set(item.id, null);
|
|
583
|
+
if (current()) answers.set(item.id, null);
|
|
473
584
|
} finally {
|
|
474
585
|
saving.delete(item.id);
|
|
475
586
|
}
|
|
@@ -509,9 +620,10 @@ export function installPanelSlot(): () => void {
|
|
|
509
620
|
}
|
|
510
621
|
main.focus();
|
|
511
622
|
};
|
|
512
|
-
const
|
|
623
|
+
const closeNow = () => {
|
|
513
624
|
if (!isOpen()) return;
|
|
514
625
|
canvas.removeAttribute(attribute());
|
|
626
|
+
if (!isPhone()) canvas.removeAttribute("data-panel-sheet");
|
|
515
627
|
disposeResize();
|
|
516
628
|
disposeResize = attachResize();
|
|
517
629
|
unload();
|
|
@@ -520,7 +632,8 @@ export function installPanelSlot(): () => void {
|
|
|
520
632
|
update();
|
|
521
633
|
returnFocus();
|
|
522
634
|
};
|
|
523
|
-
const
|
|
635
|
+
const close = () => transitionTo(closeNow);
|
|
636
|
+
const openNow = (id: string, nextSearch?: string) => {
|
|
524
637
|
const next = panel.providers.find((item) => item.id === id);
|
|
525
638
|
if (!next) return;
|
|
526
639
|
parkWorkspaceDetailDrawers(document);
|
|
@@ -542,6 +655,7 @@ export function installPanelSlot(): () => void {
|
|
|
542
655
|
if (frame) focusPanel();
|
|
543
656
|
else focusPending = true;
|
|
544
657
|
};
|
|
658
|
+
const open = (id: string, nextSearch?: string) => transitionTo(() => openNow(id, nextSearch));
|
|
545
659
|
const request = (request: PanelRequest) => {
|
|
546
660
|
if (request.action === "open") open(request.provider, request.search);
|
|
547
661
|
else if (request.action === "close" || isOpen()) close();
|
|
@@ -551,7 +665,17 @@ export function installPanelSlot(): () => void {
|
|
|
551
665
|
if (!frame || event.source !== frame.contentWindow || event.origin !== location.origin) return;
|
|
552
666
|
if (isPanelReadyMessage(event.data, provider.id)) markReady();
|
|
553
667
|
else if (isPanelCloseMessage(event.data)) close();
|
|
554
|
-
else if (
|
|
668
|
+
else if (isPanelNavigateMessage(event.data)) {
|
|
669
|
+
if (!URL.canParse(event.data.href)) return;
|
|
670
|
+
const url = new URL(event.data.href, location.origin);
|
|
671
|
+
if (url.origin !== location.origin || !showsProvider(false)) return;
|
|
672
|
+
const scope = new URL(provider.href, location.origin).pathname.replace(/\/$/, "");
|
|
673
|
+
const internal = url.pathname === scope || url.pathname.startsWith(`${scope}/`);
|
|
674
|
+
transitionTo(() => {
|
|
675
|
+
if (internal) frame?.contentWindow?.location.assign(url.href);
|
|
676
|
+
else window.location.assign(url.href);
|
|
677
|
+
});
|
|
678
|
+
} else if (isPanelSearchMessage(event.data)) {
|
|
555
679
|
const command = contextCommandsWithShortcuts().find((command) => command.id === "cloud.search" && command.shortcut);
|
|
556
680
|
if (command) void importOnDemand(() => import("./commands")).then((commands) => commands?.runContextAwareCommand(command));
|
|
557
681
|
}
|
|
@@ -562,8 +686,10 @@ export function installPanelSlot(): () => void {
|
|
|
562
686
|
if (answer && isOpen()) void saveConsent({ provider: provider.id, allowed: answer.dataset.cloudPanelConsentAnswer === "allow" });
|
|
563
687
|
else if (event.target.closest("[data-cloud-panel-close]")) close();
|
|
564
688
|
else if (event.target.closest("[data-cloud-panel-retry]") && isOpen()) {
|
|
565
|
-
|
|
566
|
-
|
|
689
|
+
transitionTo(() => {
|
|
690
|
+
load(true);
|
|
691
|
+
frame?.focus();
|
|
692
|
+
});
|
|
567
693
|
} else if (event.target.closest("[data-cloud-panel-grow]") && isPhone()) {
|
|
568
694
|
const full = slot.dataset.panelSize !== "full";
|
|
569
695
|
if (full) slot.dataset.panelSize = "full";
|
|
@@ -599,17 +725,18 @@ export function installPanelSlot(): () => void {
|
|
|
599
725
|
if (isOpen()) {
|
|
600
726
|
seedWidth();
|
|
601
727
|
load();
|
|
728
|
+
} else if (frame) {
|
|
729
|
+
// A viewport change must not hide an editor while its leave decision is blocked.
|
|
730
|
+
canvas.setAttribute(attribute(), "open");
|
|
731
|
+
transitionTo(closeNow);
|
|
602
732
|
} else unload();
|
|
603
733
|
disposeResize = attachResize();
|
|
604
734
|
update();
|
|
605
735
|
};
|
|
606
736
|
const closeSheet = () => {
|
|
607
737
|
const wasOpen = canvas.dataset.panelSheet === "open";
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
unload();
|
|
611
|
-
update();
|
|
612
|
-
}
|
|
738
|
+
if (wasOpen && isPhone()) close();
|
|
739
|
+
else canvas.removeAttribute("data-panel-sheet");
|
|
613
740
|
};
|
|
614
741
|
const pageShow = (event: PageTransitionEvent) => {
|
|
615
742
|
if (!event.persisted) return;
|
|
@@ -657,6 +784,12 @@ export function installPanelSlot(): () => void {
|
|
|
657
784
|
committedWidth = value;
|
|
658
785
|
writeCookie();
|
|
659
786
|
},
|
|
787
|
+
onReset: () => {
|
|
788
|
+
committedWidth = undefined;
|
|
789
|
+
canvas.style.removeProperty("--cloud-panel-width-set");
|
|
790
|
+
writeCookie();
|
|
791
|
+
seedWidth();
|
|
792
|
+
},
|
|
660
793
|
onCollapseIntent: (armed) => {
|
|
661
794
|
if (armed) slot.dataset.closing = "true";
|
|
662
795
|
else delete slot.dataset.closing;
|
|
@@ -680,7 +813,18 @@ export function installPanelSlot(): () => void {
|
|
|
680
813
|
})
|
|
681
814
|
: () => {};
|
|
682
815
|
let disposeResize = attachResize();
|
|
816
|
+
// A column island may already have published its place while it mounted; each page's context goes out once.
|
|
817
|
+
const swapped = () => {
|
|
818
|
+
collectContext();
|
|
819
|
+
if (pageUnsent) {
|
|
820
|
+
pageUnsent = false;
|
|
821
|
+
if (context && allowed()) sendContext();
|
|
822
|
+
else stopSharing();
|
|
823
|
+
} else if (provider.consent && !allowed()) stopSharing();
|
|
824
|
+
update();
|
|
825
|
+
};
|
|
683
826
|
collectContext();
|
|
827
|
+
window.addEventListener("ssr:nav-swapped", swapped);
|
|
684
828
|
window.addEventListener(PAGE_CONTEXT_CHANGED, collectContext);
|
|
685
829
|
window.addEventListener(COMMANDS_CHANGED, commandsChanged);
|
|
686
830
|
window.addEventListener("cloud.panel.consent", consentRequest);
|
|
@@ -693,6 +837,7 @@ export function installPanelSlot(): () => void {
|
|
|
693
837
|
// Restored from the back/forward cache before this lazy module arrived: the server's answers may be outdated.
|
|
694
838
|
if (document.documentElement.hasAttribute("data-cloud-page-restored")) void revalidate();
|
|
695
839
|
window.addEventListener("popstate", closeSheet);
|
|
840
|
+
window.addEventListener("ssr:nav-popstate", closeSheet);
|
|
696
841
|
window.navigation?.addEventListener("currententrychange", entryChange);
|
|
697
842
|
window.addEventListener("resize", seedWidth);
|
|
698
843
|
// An SSR iframe can have posted its one-shot ready before this lazy module
|
|
@@ -730,6 +875,8 @@ export function installPanelSlot(): () => void {
|
|
|
730
875
|
const dispose = () => {
|
|
731
876
|
if (disposed) return;
|
|
732
877
|
disposed = true;
|
|
878
|
+
disposeLeave();
|
|
879
|
+
leaveHost.remove();
|
|
733
880
|
disposeMenu();
|
|
734
881
|
requests.abort();
|
|
735
882
|
++loadVersion;
|
|
@@ -737,6 +884,7 @@ export function installPanelSlot(): () => void {
|
|
|
737
884
|
clearTimer();
|
|
738
885
|
disposeResize();
|
|
739
886
|
removeFallbackFocus();
|
|
887
|
+
window.removeEventListener("ssr:nav-swapped", swapped);
|
|
740
888
|
window.removeEventListener(PAGE_CONTEXT_CHANGED, collectContext);
|
|
741
889
|
window.removeEventListener(COMMANDS_CHANGED, commandsChanged);
|
|
742
890
|
window.removeEventListener("cloud.panel.consent", consentRequest);
|
|
@@ -747,6 +895,7 @@ export function installPanelSlot(): () => void {
|
|
|
747
895
|
column.removeEventListener("change", modeChange);
|
|
748
896
|
window.removeEventListener("pageshow", pageShow);
|
|
749
897
|
window.removeEventListener("popstate", closeSheet);
|
|
898
|
+
window.removeEventListener("ssr:nav-popstate", closeSheet);
|
|
750
899
|
window.navigation?.removeEventListener("currententrychange", entryChange);
|
|
751
900
|
window.removeEventListener("resize", seedWidth);
|
|
752
901
|
installed = undefined;
|
package/src/browser/panel.ts
CHANGED
|
@@ -4,6 +4,7 @@ export const resourceSearchMessages = i18n.define({
|
|
|
4
4
|
baseLocale: "en",
|
|
5
5
|
messages: {
|
|
6
6
|
en: {
|
|
7
|
+
app: "App",
|
|
7
8
|
actions: "Actions",
|
|
8
9
|
contextActions: "On this page",
|
|
9
10
|
commandPlaceholder: "Find an action…",
|
|
@@ -54,6 +55,7 @@ export const resourceSearchMessages = i18n.define({
|
|
|
54
55
|
loadingTags: "Loading filters…",
|
|
55
56
|
},
|
|
56
57
|
de: {
|
|
58
|
+
app: "App",
|
|
57
59
|
actions: "Aktionen",
|
|
58
60
|
contextActions: "Auf dieser Seite",
|
|
59
61
|
commandPlaceholder: "Aktion finden…",
|